24 Commits

Author SHA1 Message Date
ISA
001b237dd7 style: dark mode Modal KÜ Einstellungen 2025-09-08 15:38:55 +02:00
ISA
af21b180f1 WIP: dark mode Modale 2025-09-08 15:33:26 +02:00
ISA
fefff9419d WIP: dark mode Berichte 2025-09-08 15:22:06 +02:00
ISA
27c60c6742 WIP: dark mode Modale 2025-09-08 15:12:38 +02:00
ISA
c8ec763aac WIP: dark mode Baugrüppenträger sttus 2025-09-08 15:04:52 +02:00
ISA
d163df0d96 WIP: dark mode 2025-09-08 15:01:34 +02:00
ISA
12d3a17f60 fix: TDR 2 Minuten eingestellt laut eingabe 2025-09-08 13:31:32 +02:00
ISA
f3339ccafd fix: TDR 2 Minuten eingestellt laut eingaben 2025-09-08 13:30:23 +02:00
ISA
fab8a02ce9 fix: TDR 2 Minuten eingestellt laut eingaben 2025-09-08 13:28:59 +02:00
ISA
eb0585072d WIP: dark mode 2025-09-08 13:14:04 +02:00
ISA
a596422056 fix: Beim Aufruf der TDR-Detailseite erscheint im Hintergrund auf der KÜ ein Schleifenwiderstand von 0 KOhm. In der Daten Javascriptdatei steht jedoch der richtige Wert. 2025-09-08 12:09:30 +02:00
ISA
531fa93b70 fix: Beim Ausführen einer TDR-Messung (Klick auf blauen Button in der TDR-Detailseite) erscheint keine Rückmeldung. Dort müsste ein Hinweis erscheinen “TDR-Messung wird ausgeführt und kann bis zu zwei Minuten dauern” 2025-09-08 11:48:23 +02:00
ISA
72341abb23 fix: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt 2025-09-08 10:46:20 +02:00
ISA
9c218b2a1d WIP: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt 2025-09-08 10:41:46 +02:00
ISA
d38d3191c5 Test: Jenkinsfile 2025-09-08 08:49:48 +02:00
ISA
112f537904 test: Jenkinsfile 2025-09-08 08:45:06 +02:00
ISA
25b6c5c3b0 fix: Jenkinsfile 2025-09-08 08:30:09 +02:00
ISA
398d13bf1b fix: Vereinfacht: Jenkinsfile 2025-09-08 08:25:00 +02:00
ISA
91b7c8d40f fix. Jenkinsfile 2025-09-08 08:21:14 +02:00
ISA
7dfef4b16a test: Jenkinsfile 2025-09-08 08:04:55 +02:00
ISA
0397f23196 fix: Jenkinsfile 2025-09-08 07:46:38 +02:00
ISA
0865d61450 Jenkinsfile auf Woodpecker-Parität umgestellt: 2025-09-08 07:40:52 +02:00
ISA
2eb8f3a255 fix: Jenkinsfile 2025-09-08 07:28:43 +02:00
ISA
22321a7ac9 Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage) , automatisch abmelden 2025-09-08 07:08:59 +02:00
46 changed files with 1128 additions and 499 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.855 NEXT_PUBLIC_APP_VERSION=1.6.879
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.855 NEXT_PUBLIC_APP_VERSION=1.6.879
NEXT_PUBLIC_CPL_MODE=production NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,123 @@
## [1.6.879] 2025-09-08
- WIP: dark mode Modale
---
## [1.6.878] 2025-09-08
- WIP: dark mode Berichte
---
## [1.6.877] 2025-09-08
- WIP: dark mode Modale
---
## [1.6.876] 2025-09-08
- WIP: dark mode Baugrüppenträger sttus
---
## [1.6.875] 2025-09-08
- WIP: dark mode
---
## [1.6.874] 2025-09-08
- fix: TDR 2 Minuten eingestellt laut eingabe
---
## [1.6.873] 2025-09-08
- fix: TDR 2 Minuten eingestellt laut eingaben
---
## [1.6.872] 2025-09-08
- fix: TDR 2 Minuten eingestellt laut eingaben
---
## [1.6.871] 2025-09-08
- WIP: dark mode
---
## [1.6.870] 2025-09-08
- fix: Beim Aufruf der TDR-Detailseite erscheint im Hintergrund auf der KÜ ein Schleifenwiderstand von 0 KOhm. In der Daten Javascriptdatei steht jedoch der richtige Wert.
---
## [1.6.869] 2025-09-08
- fix: Beim Ausführen einer TDR-Messung (Klick auf blauen Button in der TDR-Detailseite) erscheint keine Rückmeldung. Dort müsste ein Hinweis erscheinen “TDR-Messung wird ausgeführt und kann bis zu zwei Minuten dauern”
---
## [1.6.868] 2025-09-08
- fix: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
---
## [1.6.867] 2025-09-08
- WIP: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
---
## [1.6.866] 2025-09-08
- Test: Jenkinsfile
---
## [1.6.865] 2025-09-08
- test: Jenkinsfile
---
## [1.6.864] 2025-09-08
- fix: Jenkinsfile
---
## [1.6.863] 2025-09-08
- fix: Vereinfacht: Jenkinsfile
---
## [1.6.862] 2025-09-08
- fix. Jenkinsfile
---
## [1.6.861] 2025-09-08
- test: Jenkinsfile
---
## [1.6.860] 2025-09-08
- fix: Jenkinsfile
---
## [1.6.859] 2025-09-08
- Jenkinsfile auf Woodpecker-Parität umgestellt:
---
## [1.6.858] 2025-09-08
- fix: Jenkinsfile
---
## [1.6.857] 2025-09-08
- Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage) , automatisch abmelden
---
## [1.6.856] 2025-09-08
- chore: Jenkinsfile
---
## [1.6.855] 2025-09-05 ## [1.6.855] 2025-09-05
- fix: allow scripts in woodpecker - fix: allow scripts in woodpecker

112
Jenkinsfile vendored
View File

@@ -1,65 +1,95 @@
pipeline { pipeline {
agent any agent any
tools { nodejs 'node20' } // exakt der Name aus "Manage Jenkins → Tools"
environment {
CI = "true"
NODE_ENV = "production"
NEXT_TELEMETRY_DISABLED = "1"
PORT = "3000"
}
options {
timestamps()
}
stages { stages {
stage('Versions') { stage('Checkout') {
steps { sh 'node -v && npm -v' }
}
stage('Verify mocks') {
steps { steps {
checkout scm
sh ''' sh '''
set -euxo pipefail set -eux
npm ci git status --short || true
echo "=== git ls-files ===" # Submodule & LFS falls vorhanden
git ls-files | grep -i "^mocks/device-cgi-simulator/SERVICE/systemMockData.js" || true git submodule update --init --recursive || true
echo "=== ls -la ===" git lfs install || true
ls -la mocks/device-cgi-simulator/SERVICE || true git lfs fetch || true
echo "=== file exists? ===" git lfs checkout || true
test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo "FOUND" || (echo "MISSING" && exit 1)
''' '''
} }
} }
stage('Build & E2E (chromium)') { stage('verify-mocks') {
environment {
CI = 'true'
NODE_ENV = 'production'
NEXT_TELEMETRY_DISABLED = '1'
PORT = '3000'
}
steps { steps {
sh ''' sh '''
set -euxo pipefail set -eux
# Install devDependencies as well (rimraf, cross-env, etc.) docker run --rm -v "$PWD":/ws -w /ws \
env npm_config_production=false npm ci mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
pwd
node -v && npm -v
npm ci --ignore-scripts
echo '=== git ls-files ==='
git ls-files | grep -i '^mocks/device-cgi-simulator/SERVICE/systemMockData.js' || true
echo '=== ls -la ==='
ls -la mocks/device-cgi-simulator/SERVICE || true
echo '=== file exists? ==='
test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo 'FOUND' || (echo 'MISSING' && exit 1)
"
'''
}
}
# Build Next.js stage('e2e-dev') {
npm run build steps {
sh '''
# Start local static simulator in background set -eux
npm run server:sim & docker run --rm -v "$PWD":/ws -w /ws -p 3000:3000 \
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
# Ensure Playwright browsers and OS deps are installed (best-effort) node -v && npm -v
npx playwright install-deps || true env npm_config_production=false npm ci
npx playwright install npm run build
npm run server:sim &
# Wait until simulator responds on port 3000 (no curl dependency) # Auf Port 3000 warten
node -e "const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();" node -e \\"const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();\\"
npx playwright test --project=chromium
# Run tests (chromium only to match Woodpecker) "
npx playwright test --project=chromium
''' '''
} }
} }
} }
post { post {
success { success {
sh 'curl -d "Tests erfolgreich in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35' sh '''
docker run --rm curlimages/curl:8.9.1 \
-d "Tests erfolgreich in Jenkins" \
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
'''
} }
failure { failure {
sh 'curl -d "Tests fehlgeschlagen in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35' sh '''
docker run --rm curlimages/curl:8.9.1 \
-d "Tests fehlgeschlagen in Jenkins" \
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9Z35
'''
}
always {
script {
if (fileExists('playwright-report')) {
archiveArtifacts artifacts: 'playwright-report/**', onlyIfSuccessful: false
} else {
echo 'Kein playwright-report gefunden.'
}
}
} }
} }
} }

View File

@@ -1,13 +1,16 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { useAppDispatch } from "@/redux/store"; import { useAppDispatch } from "@/redux/store";
import { setEvents } from "@/redux/slices/deviceEventsSlice"; import {
setEvents,
initPersistedTimings,
} from "@/redux/slices/deviceEventsSlice";
declare global { declare global {
interface Window { interface Window {
loopMeasurementEvent?: number[]; loopMeasurementEvent?: number[];
tdrMeasurementEvent?: number[]; tdrMeasurementEvent?: number[];
alignmentEvent?: number[]; comparisonEvent?: number[]; // renamed from alignmentEvent
} }
} }
@@ -18,6 +21,25 @@ export default function DeviceEventsBridge() {
React.useEffect(() => { React.useEffect(() => {
let lastSig = ""; let lastSig = "";
// Hydrate persisted timings once
try {
const raw =
typeof window !== "undefined" &&
localStorage.getItem("deviceEventsTimingsV1");
if (raw) {
const parsed = JSON.parse(raw);
dispatch(
initPersistedTimings({
loop: parsed.loop,
tdr: parsed.tdr,
compare: parsed.compare || parsed.align,
})
);
}
} catch (e) {
// eslint-disable-next-line no-console
console.warn("DeviceEventsBridge hydration failed", e);
}
const readAndDispatch = () => { const readAndDispatch = () => {
const ksx = Array.isArray(window.loopMeasurementEvent) const ksx = Array.isArray(window.loopMeasurementEvent)
? window.loopMeasurementEvent ? window.loopMeasurementEvent
@@ -25,8 +47,8 @@ export default function DeviceEventsBridge() {
const ksy = Array.isArray(window.tdrMeasurementEvent) const ksy = Array.isArray(window.tdrMeasurementEvent)
? window.tdrMeasurementEvent ? window.tdrMeasurementEvent
: undefined; : undefined;
const ksz = Array.isArray(window.alignmentEvent) const ksz = Array.isArray(window.comparisonEvent)
? window.alignmentEvent ? window.comparisonEvent
: undefined; : undefined;
// Build a stable signature of first 32 values per array // Build a stable signature of first 32 values per array
const to32 = (a?: number[]) => { const to32 = (a?: number[]) => {

View File

@@ -5,14 +5,14 @@ import { useAppSelector } from "@/redux/store";
export default function GlobalActivityOverlay() { export default function GlobalActivityOverlay() {
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive); const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive); const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
const anyAlign = useAppSelector((s) => s.deviceEvents.anyAlignmentActive); const anyCompare = useAppSelector((s) => s.deviceEvents.anyComparisonActive);
const ksx = useAppSelector((s) => s.deviceEvents.ksx); const ksx = useAppSelector((s) => s.deviceEvents.ksx);
const ksy = useAppSelector((s) => s.deviceEvents.ksy); const ksy = useAppSelector((s) => s.deviceEvents.ksy);
const ksz = useAppSelector((s) => s.deviceEvents.ksz); const ksz = useAppSelector((s) => s.deviceEvents.ksz);
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt); const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt); const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
const alignmentStartedAt = useAppSelector( const comparisonStartedAt = useAppSelector(
(s) => s.deviceEvents.alignmentStartedAt (s) => s.deviceEvents.comparisonStartedAt
); );
const fmt = (arr: number[]) => const fmt = (arr: number[]) =>
@@ -24,13 +24,13 @@ export default function GlobalActivityOverlay() {
// Simple 1s ticker so progress bars advance while overlay is shown // Simple 1s ticker so progress bars advance while overlay is shown
const [now, setNow] = useState<number>(Date.now()); const [now, setNow] = useState<number>(Date.now());
useEffect(() => { useEffect(() => {
const active = anyLoop || anyTdr || anyAlign; const active = anyLoop || anyTdr || anyCompare;
if (!active) return; if (!active) return;
const id = setInterval(() => setNow(Date.now()), 1000); const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, [anyLoop, anyTdr, anyAlign]); }, [anyLoop, anyTdr, anyCompare]);
const active = anyLoop || anyTdr || anyAlign; const active = anyLoop || anyTdr || anyCompare;
if (!active) return null; if (!active) return null;
const clamp = (v: number, min = 0, max = 1) => const clamp = (v: number, min = 0, max = 1) =>
@@ -102,13 +102,13 @@ export default function GlobalActivityOverlay() {
</div> </div>
)} )}
{anyAlign && ( {anyCompare && (
<div> <div>
<div className="text-sm text-gray-800 mb-1"> <div className="text-sm text-gray-800 mb-1">
Abgleich läuft (: {fmt(ksz)}) kann bis zu 10 Minuten dauern Comparison läuft (: {fmt(ksz)}) kann bis zu 10 Minuten dauern
</div> </div>
{(() => { {(() => {
const { pct } = compute(alignmentStartedAt, ALIGN_MS); const { pct } = compute(comparisonStartedAt, ALIGN_MS);
return ( return (
<div> <div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden"> <div className="h-2 w-full bg-gray-200 rounded overflow-hidden">

View File

@@ -55,45 +55,57 @@ function Footer() {
}, [showSlider]); }, [showSlider]);
return ( return (
<footer className="relative bg-gray-300 p-4 xl:p-2 2xl:p-4 overflow-hidden text-black laptop:h-[5vh] "> <footer className="relative bg-[var(--color-surface-alt)] border-t border-base p-4 xl:p-2 2xl:p-4 overflow-hidden text-[var(--color-fg)] laptop:h-[5vh] theme-transition">
<div className="container mx-auto flex justify-between"> <div className="container mx-auto flex justify-between">
<div className="flex flex-row space-x-2"> <div className="flex flex-row space-x-2 items-center">
<Icon <Icon
icon="material-symbols:factory-outline" icon="material-symbols:factory-outline"
className="text-xl text-blue-400" className="text-xl text-accent"
/> />
<p className="text-sm">Littwin Systemtechnik GmbH & Co. KG</p> <p className="text-sm text-fg-muted">
Littwin Systemtechnik GmbH & Co. KG
</p>
</div> </div>
<div className="flex flex-row space-x-2"> <div className="flex flex-row space-x-2 items-center">
<Icon icon="charm:phone" className="text-xl text-blue-400" /> <Icon icon="charm:phone" className="text-xl text-accent" />
<p className="text-sm">Telefon: 04402 972577-0</p> <p className="text-sm text-fg-muted">Telefon: 04402 972577-0</p>
</div> </div>
<div className="flex flex-row space-x-2"> <div className="flex flex-row space-x-2 items-center">
<Icon icon="mdi:email-outline" className="text-xl text-blue-400" /> <Icon icon="mdi:email-outline" className="text-xl text-accent" />
<p className="text-sm">kontakt@littwin-systemtechnik.de</p> <p className="text-sm text-fg-muted">
kontakt@littwin-systemtechnik.de
</p>
</div> </div>
<div <div
className="flex flex-row space-x-2 cursor-pointer" className="flex flex-row space-x-2 cursor-pointer items-center group"
onClick={() => setShowSlider(true)} onClick={() => setShowSlider(true)}
> >
<Icon icon="bi:book" className="text-xl text-blue-400" /> <Icon
<p className="text-sm">Handbücher</p> icon="bi:book"
className="text-xl text-accent group-hover:brightness-110 transition"
/>
<p className="text-sm text-fg-muted group-hover:text-[var(--color-fg)] transition">
Handbücher
</p>
</div> </div>
</div> </div>
<div <div
ref={sliderRef} ref={sliderRef}
className={`fixed top-0 right-0 w-64 h-full bg-white shadow-lg transform transition-transform duration-300 ${ className={`fixed top-0 right-0 w-64 h-full bg-[var(--color-surface)] border-l border-base shadow-lg transform transition-transform duration-300 ${
showSlider ? "translate-x-0" : "translate-x-full" showSlider ? "translate-x-0" : "translate-x-full"
}`} }`}
> >
<div className="p-4 flex justify-between items-center border-b"> <div className="p-4 flex justify-between items-center border-b border-base">
<h3 className="text-lg font-semibold">PDF Handbücher</h3> <h3 className="text-base font-semibold text-[var(--color-fg)]">
PDF Handbücher
</h3>
<button <button
className="text-gray-500 hover:text-gray-800" className="text-[var(--color-muted)] hover:text-[var(--color-fg)] transition"
onClick={() => setShowSlider(false)} onClick={() => setShowSlider(false)}
aria-label="Schließen"
> >
<Icon icon="carbon:close" className="text-2xl" /> <Icon icon="carbon:close" className="text-xl" />
</button> </button>
</div> </div>
@@ -102,7 +114,7 @@ function Footer() {
{pdfFiles.map((fileName) => ( {pdfFiles.map((fileName) => (
<li <li
key={fileName} key={fileName}
className="cursor-pointer text-blue-500 hover:underline mb-2" className="cursor-pointer text-accent hover:underline mb-2 text-sm"
onClick={() => loadPDF(fileName)} onClick={() => loadPDF(fileName)}
> >
{fileName} {fileName}

View File

@@ -1,5 +1,5 @@
"use client"; // components/Header.jsx "use client"; // components/Header.jsx
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -15,16 +15,18 @@ function Header() {
const router = useRouter(); const router = useRouter();
const [showSettingsModal, setShowSettingsModal] = useState(false); const [showSettingsModal, setShowSettingsModal] = useState(false);
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false); const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
const autoLogoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Removed duplicate declaration of deviceName // Removed duplicate declaration of deviceName
const handleCloseSettingsModal = () => setShowSettingsModal(false); const handleCloseSettingsModal = () => setShowSettingsModal(false);
const handleLogout = () => { const handleLogout = useCallback(() => {
sessionStorage.removeItem("token"); // Token entfernen sessionStorage.removeItem("token"); // Token entfernen
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
localStorage.removeItem("adminLoginTime"); // Login-Zeitpunkt entfernen
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
router.push("/offline.html"); // Weiterleitung router.push("/offline.html"); // Weiterleitung
}; }, [router]);
useEffect(() => { useEffect(() => {
// Initialer Check beim Laden der Komponente // Initialer Check beim Laden der Komponente
@@ -43,6 +45,56 @@ function Header() {
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
}; };
}, [isAdminLoggedIn]); }, [isAdminLoggedIn]);
// Auto-Logout nach 1 Minute (Test): nutzt adminLoginTime aus localStorage
useEffect(() => {
// Timer bereinigen, wenn sich der Status ändert
if (autoLogoutTimerRef.current) {
clearTimeout(autoLogoutTimerRef.current);
autoLogoutTimerRef.current = null;
}
if (!isAdminLoggedIn) return;
const iso = localStorage.getItem("adminLoginTime");
const loginTime = iso ? new Date(iso).getTime() : Date.now();
if (!iso) {
// Falls älterer Login ohne Zeitstempel, setze jetzt
try {
localStorage.setItem(
"adminLoginTime",
new Date(loginTime).toISOString()
);
} catch {
void 0; // ignore write errors (e.g., storage disabled)
}
}
// 1 Minute ab Login (60_000 ms), eine Stunde (3_600_000 ms) im Produktivbetrieb
const target = loginTime + 3_600_000;
const delay = Math.max(0, target - Date.now());
// Fallback: wenn Datum in Vergangenheit (z.B. Uhrzeit geändert), sofort abmelden
autoLogoutTimerRef.current = setTimeout(() => {
// Versuche den Button zu klicken, falls vorhanden
const btn = document.querySelector<HTMLButtonElement>(
'button[aria-label="Abmelden"]'
);
if (btn) {
btn.click();
} else {
// Fallback direkt
handleLogout();
}
}, delay);
return () => {
if (autoLogoutTimerRef.current) {
clearTimeout(autoLogoutTimerRef.current);
autoLogoutTimerRef.current = null;
}
};
}, [isAdminLoggedIn, handleLogout]);
//---------------------------------------------------------------- //----------------------------------------------------------------
const dispatch = useDispatch<AppDispatch>(); const dispatch = useDispatch<AppDispatch>();
@@ -57,21 +109,43 @@ function Header() {
}, [deviceName, dispatch]); }, [deviceName, dispatch]);
//---------------------------------------------------------------- //----------------------------------------------------------------
// Dark/Light Mode Toggle // Dark/Light Mode Toggle (persisted)
const [isDark, setIsDark] = useState(false); const [isDark, setIsDark] = useState(false);
// Initial state from html class / localStorage (set by _document script before hydration)
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window === "undefined") return;
const html = document.documentElement; const html = document.documentElement;
if (isDark) { const stored = localStorage.getItem("theme");
html.classList.add("dark"); const active = stored ? stored === "dark" : html.classList.contains("dark");
} else { setIsDark(active);
html.classList.remove("dark"); }, []);
}
useEffect(() => {
if (typeof window === "undefined") return;
const html = document.documentElement;
if (isDark) {
html.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
html.classList.remove("dark");
localStorage.setItem("theme", "light");
} }
}, [isDark]); }, [isDark]);
// Keyboard shortcut Alt + D to toggle theme
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.altKey && (e.key === "d" || e.key === "D")) {
e.preventDefault();
setIsDark((d) => !d);
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);
return ( return (
<header className="bg-gray-300 dark:bg-gray-800 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black dark:text-white "> <header className="bg-[var(--color-surface)] dark:bg-[var(--color-surface)]/90 backdrop-blur flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-[var(--color-fg)] dark:text-[var(--color-fg)] shadow-sm border-b border-[var(--color-border)]">
<div <div
className="absolute transform -translate-y-1/2 className="absolute transform -translate-y-1/2
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop: left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
@@ -102,10 +176,10 @@ function Header() {
priority priority
/> />
<div className="flex flex-col leading-tight whitespace-nowrap"> <div className="flex flex-col leading-tight whitespace-nowrap">
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-gray-900 dark:text-gray-100"> <h2 className="text-xl laptop:text-base xl:text-lg font-bold text-[var(--color-fg)]">
Meldestation Meldestation
</h2> </h2>
<p className="text-gray-600 dark:text-gray-300 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]"> <p className="text-[var(--color-fg-muted)] text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
{deviceName} {deviceName}
</p> </p>
</div> </div>
@@ -117,7 +191,7 @@ function Header() {
<button <button
aria-label={isDark ? "Light Mode" : "Dark Mode"} aria-label={isDark ? "Light Mode" : "Dark Mode"}
onClick={() => setIsDark((d) => !d)} onClick={() => setIsDark((d) => !d)}
className="rounded-full p-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition" className="rounded-full p-2 bg-[var(--color-surface-alt)]/80 hover:bg-[var(--color-surface-alt)] dark:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)] transition border border-[var(--color-border)]"
title={isDark ? "Light Mode" : "Dark Mode"} title={isDark ? "Light Mode" : "Dark Mode"}
> >
{isDark ? ( {isDark ? (
@@ -139,7 +213,8 @@ function Header() {
<div className="flex items-center justify-end w-1/4 space-x-1"> <div className="flex items-center justify-end w-1/4 space-x-1">
<button <button
onClick={handleLogout} onClick={handleLogout}
className="bg-littwin-blue text-white px-4 py-2 rounded" aria-label="Abmelden"
className="px-4 py-2 rounded bg-[var(--color-accent)] text-white hover:brightness-110 shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-ring)] focus:ring-offset-2 focus:ring-offset-[var(--color-background)] transition"
> >
Abmelden Abmelden
</button> </button>
@@ -149,7 +224,7 @@ function Header() {
{/* Warnhinweis, wenn der Admin angemeldet ist */} {/* Warnhinweis, wenn der Admin angemeldet ist */}
{isAdminLoggedIn && ( {isAdminLoggedIn && (
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-yellow-400 text-center py-2 text-black font-bold"> <div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-[var(--color-warning)] text-center py-2 text-black font-bold tracking-wide">
Admin-Modus aktiv Admin-Modus aktiv
</div> </div>
)} )}

View File

@@ -37,8 +37,8 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
return ( return (
<div <div
className={`bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg laptop:p-1 xl:p-1 ${ className={`text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg laptop:p-1 xl:p-1 ${
loading ? "cursor-wait" : "" loading ? "cursor-wait opacity-70" : ""
}`} }`}
> >
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center"> <h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
@@ -54,24 +54,24 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
loading ? "cursor-wait" : "" loading ? "cursor-wait" : ""
}`} }`}
> >
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b items-center"> <thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/30 text-[var(--color-fg)] border-b border-[var(--color-border)] items-center">
<tr> <tr>
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Eingang Eingang
</th> </th>
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Messwert Messwert
</th> </th>
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Einheit Einheit
</th> </th>
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Bezeichnung Bezeichnung
</th> </th>
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Einstellungen Einstellungen
</th> </th>
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
Messkurve Messkurve
</th> </th>
</tr> </tr>
@@ -92,12 +92,12 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
loading loading
? "cursor-wait" ? "cursor-wait"
: analogInput.id === activeId : analogInput.id === activeId
? "bg-blue-100 dark:bg-gray-700 dark:text-white" ? "bg-[var(--color-accent-soft)] dark:bg-[var(--color-surface-alt)]/60 text-[var(--color-fg)]"
: "hover:bg-gray-100 dark:hover:bg-gray-800" : "hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30"
}`} }`}
> >
<td <td
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100" className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
onClick={() => handleSelect(analogInput.id!, analogInput)} onClick={() => handleSelect(analogInput.id!, analogInput)}
> >
<div className="flex items-center gap-1 "> <div className="flex items-center gap-1 ">
@@ -109,7 +109,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
</div> </div>
</td> </td>
<td <td
className="border p-2 text-right bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100" className="border p-2 text-right bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
onClick={() => handleSelect(analogInput.id!, analogInput)} onClick={() => handleSelect(analogInput.id!, analogInput)}
> >
{typeof analogInput.value === "number" {typeof analogInput.value === "number"
@@ -118,19 +118,19 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
</td> </td>
<td <td
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100" className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
onClick={() => handleSelect(analogInput.id!, analogInput)} onClick={() => handleSelect(analogInput.id!, analogInput)}
> >
{analogInput.unit || "-"} {analogInput.unit || "-"}
</td> </td>
<td <td
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100" className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
onClick={() => handleSelect(analogInput.id!, analogInput)} onClick={() => handleSelect(analogInput.id!, analogInput)}
> >
{analogInput.label || "----"} {analogInput.label || "----"}
</td> </td>
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
<button <button
onClick={() => { onClick={() => {
handleSelect(analogInput.id!, analogInput); handleSelect(analogInput.id!, analogInput);
@@ -141,7 +141,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
<Icon icon={settingsIcon} className="text-xl" /> <Icon icon={settingsIcon} className="text-xl" />
</button> </button>
</td> </td>
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
<button <button
onClick={() => { onClick={() => {
handleSelect(analogInput.id!, analogInput); handleSelect(analogInput.id!, analogInput);

View File

@@ -36,8 +36,8 @@ function AnalogInputsView() {
}`} }`}
> >
<div className="grid grid-cols-1 gap-4 justify-items-start"> <div className="grid grid-cols-1 gap-4 justify-items-start">
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 max-w-3xl text-gray-900 dark:text-gray-100"> <div className="rounded-lg p-4 max-w-3xl text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] shadow-sm">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100"> <h2 className="text-xl font-semibold mb-4 text-[var(--color-fg)] tracking-wide">
Messwerteingänge Messwerteingänge
</h2> </h2>
<AnalogInputsTable loading={loading} /> <AnalogInputsTable loading={loading} />

View File

@@ -43,7 +43,7 @@ const Baugruppentraeger: React.FC = () => {
baugruppen.push( baugruppen.push(
<div <div
key={i} key={i}
className="flex bg-white shadow-md rounded-lg mb-4 xl:mb-0 lg:mb-0 border border-gray-200 w-full laptop:scale-y-75 xl:scale-y-90" className="flex card mb-4 xl:mb-0 lg:mb-0 w-full laptop:scale-y-75 xl:scale-y-90"
> >
<div className="flex gap-1"> <div className="flex gap-1">
{slots.map((version, index) => { {slots.map((version, index) => {

View File

@@ -25,7 +25,7 @@ const DashboardView: React.FC = () => {
}, [dispatch]); }, [dispatch]);
//------------------------------------- //-------------------------------------
return ( return (
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"> <div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-[var(--color-background)] text-[var(--color-fg)]">
{/* Header */} {/* Header */}
<div className="flex justify-between items-center w-full lg:w-2/3"> <div className="flex justify-between items-center w-full lg:w-2/3">
<div className="flex justify-between gap-1"> <div className="flex justify-between gap-1">
@@ -33,7 +33,7 @@ const DashboardView: React.FC = () => {
icon="ri:calendar-schedule-line" icon="ri:calendar-schedule-line"
className="text-littwin-blue text-4xl xl:text-2xl" className="text-littwin-blue text-4xl xl:text-2xl"
/> />
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100 xl:text-base"> <h1 className="text-xl font-bold xl:text-base text-[var(--color-fg)] tracking-wide">
Letzten 20 Meldungen Letzten 20 Meldungen
</h1> </h1>
</div> </div>

View File

@@ -48,22 +48,22 @@ export default function Last20MessagesTable({ className }: Props) {
return ( return (
<div className={`flex flex-col gap-3 p-4 ${className}`}> <div className={`flex flex-col gap-3 p-4 ${className}`}>
<div className="overflow-auto max-h-[80vh]"> <div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <table className="min-w-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10"> <thead className="text-left sticky top-0 z-10 bg-[var(--color-surface-alt)]/70 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)]">
<tr> <tr>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Prio Prio
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Zeitstempel Zeitstempel
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Quelle Quelle
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Meldung Meldung
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
Status Status
</th> </th>
</tr> </tr>
@@ -72,24 +72,24 @@ export default function Last20MessagesTable({ className }: Props) {
{filteredMessages.slice(0, 20).map((msg, index) => ( {filteredMessages.slice(0, 20).map((msg, index) => (
<tr <tr
key={index} key={index}
className="hover:bg-gray-100 dark:hover:bg-gray-800" className="hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
> >
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
<div <div
className="w-4 h-4 rounded" className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }} style={{ backgroundColor: msg.c }}
></div> ></div>
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.t} {msg.t}
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.i} {msg.i}
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.m} {msg.m}
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
{msg.v} {msg.v}
</td> </td>
</tr> </tr>
@@ -97,7 +97,7 @@ export default function Last20MessagesTable({ className }: Props) {
</tbody> </tbody>
</table> </table>
{messages.length === 0 && ( {messages.length === 0 && (
<div className="mt-4 text-center text-gray-500 italic dark:text-gray-400"> <div className="mt-4 text-center italic text-[var(--color-fg-muted)]">
Keine Meldungen im gewählten Zeitraum vorhanden. Keine Meldungen im gewählten Zeitraum vorhanden.
</div> </div>
)} )}

View File

@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
return ( return (
<div className="w-full flex-direction: row flex"> <div className="w-full flex-direction: row flex">
<div className=" flex-grow flex justify-between items-center mt-1 bg-white dark:bg-gray-800 p-2 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75"> <div className=" flex-grow flex justify-between items-center mt-1 p-2 rounded-lg shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Image <Image
src="/images/IP-icon.svg" src="/images/IP-icon.svg"
@@ -49,12 +49,8 @@ const NetworkInfo: React.FC = () => {
priority priority
/> />
<div> <div>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
IP-Adresse <p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{ip}
</p>
</div> </div>
</div> </div>
@@ -68,10 +64,8 @@ const NetworkInfo: React.FC = () => {
priority priority
/> />
<div> <div>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
Subnet-Maske <p className="text-sm font-medium text-[var(--color-fg)]">
</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
{subnet} {subnet}
</p> </p>
</div> </div>
@@ -87,8 +81,8 @@ const NetworkInfo: React.FC = () => {
priority priority
/> />
<div> <div>
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p> <p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200"> <p className="text-sm font-medium text-[var(--color-fg)]">
{gateway} {gateway}
</p> </p>
</div> </div>
@@ -97,8 +91,8 @@ const NetworkInfo: React.FC = () => {
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div> <div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
<div> <div>
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p> <p className="text-xs text-[var(--color-fg-muted)]">Status</p>
<p className="text-sm font-medium text-gray-700 dark:text-gray-200"> <p className="text-sm font-medium text-[var(--color-fg)]">
{opcUaZustand} {opcUaZustand}
</p> </p>
</div> </div>

View File

@@ -4,9 +4,7 @@ import { Icon } from "@iconify/react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../redux/store"; import { RootState } from "../../../redux/store";
type VersionInfoProps = { type VersionInfoProps = { className?: string };
className?: string;
};
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => { const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
const appVersion = const appVersion =
@@ -17,26 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
); );
return ( return (
<div <div className={`card w-full p-3 laptop:p-2 ${className}`}>
className={`bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 w-full laptop:p-2 ${className}`} <h2 className="text-base font-semibold mb-2 text-[var(--color-fg)]">
>
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
Versionsinformationen Versionsinformationen
</h2> </h2>
<ul className="space-y-1">
<div className="flex flex-row p-2 space-x-2"> <li className="flex items-start gap-2">
<Icon icon="bx:code-block" className="text-xl text-blue-400" /> <Icon icon="bx:code-block" className="text-xl text-accent" />
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-fg-muted">
Applikationsversion: {appVersion} Applikationsversion:{" "}
</p> <span className="text-[var(--color-fg)]">{appVersion}</span>
</div> </p>
</li>
<div className="flex flex-row p-2 space-x-2"> <li className="flex items-start gap-2">
<Icon icon="mdi:web" className="text-xl text-blue-400" /> <Icon icon="mdi:web" className="text-xl text-accent" />
<p className="text-sm text-gray-600 dark:text-gray-300"> <p className="text-sm text-fg-muted">
Webversion: {webVersion} Webversion:{" "}
</p> <span className="text-[var(--color-fg)]">{webVersion}</span>
</div> </p>
</li>
</ul>
</div> </div>
); );
}; };

View File

@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
//console.log("DigitalInputs", inputs); //console.log("DigitalInputs", inputs);
return ( return (
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full laptop:p-1 xl:p-1"> <div className="text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg w-full laptop:p-1 xl:p-1">
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center"> <h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
<Icon <Icon
icon={inputIcon} icon={inputIcon}
@@ -38,19 +38,19 @@ export default function DigitalInputsWidget({
/> />
Meldungseingänge {inputRange.start + 1} {inputRange.end} Meldungseingänge {inputRange.start + 1} {inputRange.end}
</h2> </h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900"> <table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)]">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b"> <thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)] border-b border-[var(--color-border)]">
<tr> <tr>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Eingang Eingang
</th> </th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Zustand Zustand
</th> </th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Bezeichnung Bezeichnung
</th> </th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Aktion Aktion
</th> </th>
</tr> </tr>
@@ -59,9 +59,9 @@ export default function DigitalInputsWidget({
{inputs.map((input) => ( {inputs.map((input) => (
<tr <tr
key={input.id} key={input.id}
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800" className="border-b border-[var(--color-border)] hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
> >
<td className="px-1 py-0 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-0 bg-[var(--color-surface)] text-[var(--color-fg)]">
<div className="flex items-center gap-1 "> <div className="flex items-center gap-1 ">
<Icon <Icon
icon={loginIcon} icon={loginIcon}
@@ -70,7 +70,7 @@ export default function DigitalInputsWidget({
{input.id} {input.id}
</div> </div>
</td> </td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
{input.eingangOffline ? ( {input.eingangOffline ? (
<div className="relative group inline-block"> <div className="relative group inline-block">
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm "> <span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
@@ -91,10 +91,10 @@ export default function DigitalInputsWidget({
</div> </div>
)} )}
</td> </td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
{input.label} {input.label}
</td> </td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
<Icon <Icon
icon={settingsIcon} icon={settingsIcon}
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white" className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white"

View File

@@ -66,7 +66,7 @@ export default function DigitalOutputsWidget({
}; };
return ( return (
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto"> <div className="bg-[var(--color-surface)] text-[var(--color-fg)] shadow-md border border-base p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center"> <h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
<Icon <Icon
icon={outputIcon} icon={outputIcon}
@@ -74,19 +74,19 @@ export default function DigitalOutputsWidget({
/> />
Schaltausgänge Schaltausgänge
</h2> </h2>
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900 rounded-lg"> <table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)] rounded-lg">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b"> <thead className="bg-[var(--color-surface)] text-[var(--color-fg)] border-b border-base">
<tr> <tr>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Ausgang Ausgang
</th> </th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Bezeichnung Bezeichnung
</th> </th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Schalter Schalter
</th> </th>
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
Aktion Aktion
</th> </th>
</tr> </tr>
@@ -95,33 +95,33 @@ export default function DigitalOutputsWidget({
{digitalOutputs.map((output) => ( {digitalOutputs.map((output) => (
<tr <tr
key={output.id} key={output.id}
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800" className="border-b border-base hover:bg-[var(--color-surface-alt)] transition-colors"
> >
<td className="flex items-center px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="flex items-center px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
<Icon <Icon
icon={outputIcon} icon={outputIcon}
className="text-gray-600 mr-1 text-base" className="text-[var(--color-muted)] mr-1 text-base"
/> />
{output.id} {output.id}
</td> </td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
{output.label} {output.label}
</td> </td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
<Icon <Icon
icon={switchIcon} icon={switchIcon}
className={`cursor-pointer text-base transition ${ className={`cursor-pointer text-base transition ${
output.status output.status
? "text-littwin-blue" ? "text-accent"
: "text-gray-500 scale-x-[-1]" : "text-[var(--color-muted)] scale-x-[-1]"
} dark:hover:text-littwin-blue`} } hover:text-accent`}
onClick={() => handleToggle(output.id)} onClick={() => handleToggle(output.id)}
/> />
</td> </td>
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
<Icon <Icon
icon={settingsIcon} icon={settingsIcon}
className="text-gray-400 text-base cursor-pointer dark:text-gray-300 dark:hover:text-white" className="text-[var(--color-muted)] text-base cursor-pointer hover:text-[var(--color-fg)]"
onClick={() => openOutputModal(output)} onClick={() => openOutputModal(output)}
/> />
</td> </td>

View File

@@ -119,19 +119,23 @@ function KabelueberwachungView() {
return ( return (
<div> <div>
<div className="mb-4"> <div className="mb-4">
{[1, 2, 3, 4].map((rack) => ( {[1, 2, 3, 4].map((rack) => {
<button const isActive = Number(activeRack) === Number(rack);
key={rack} return (
onClick={() => changeRack(rack)} <button
className={`mr-2 ${ key={rack}
Number(activeRack) === Number(rack) onClick={() => changeRack(rack)}
? "bg-littwin-blue text-white p-1 rounded-sm" aria-pressed={isActive}
: "bg-gray-300 p-1 text-sm" className={`mr-2 px-2 py-1 rounded-sm text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent/50 ${
}`} isActive
> ? "btn-primary"
Rack {rack} : "btn-muted text-fg opacity-90 hover:opacity-100"
</button> }`}
))} >
Rack {rack}
</button>
);
})}
</div> </div>
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]"> <div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
{( {(

View File

@@ -389,7 +389,8 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
<div className="mb-4 text-center space-y-1"> <div className="mb-4 text-center space-y-1">
<p className="text-lg font-semibold">RSL Messung läuft</p> <p className="text-lg font-semibold">RSL Messung läuft</p>
<p className="text-sm text-gray-700"> <p className="text-sm text-gray-700">
Bitte warten (noch {TOTAL_DURATION - rslProgress}s) Bitte warten{" "}
{Math.min(100, Math.round((rslProgress / TOTAL_DURATION) * 100))}%
</p> </p>
</div> </div>
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner"> <div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">

View File

@@ -30,6 +30,36 @@ const TDRChartActionBar: React.FC = () => {
const [selectedId, setSelectedId] = useState<number | null>(null); const [selectedId, setSelectedId] = useState<number | null>(null);
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : []; const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
// ▶ Fortschrittsanzeige für laufende TDR-Messung (max. 120s bzw. konfigurierbar)
const TDR_TOTAL_DURATION = parseInt(
process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120",
10
);
const [tdrRunning, setTdrRunning] = useState(false);
const [tdrProgress, setTdrProgress] = useState(0); // Sekunden
useEffect(() => {
if (!tdrRunning) return;
setTdrProgress(0);
const startedAt = Date.now();
const interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
if (elapsed >= TDR_TOTAL_DURATION) {
setTdrProgress(TDR_TOTAL_DURATION);
setTdrRunning(false);
clearInterval(interval);
} else {
setTdrProgress(elapsed);
}
}, 1000);
return () => clearInterval(interval);
}, [tdrRunning, TDR_TOTAL_DURATION]);
const startTdrProgress = () => {
setTdrRunning(true);
setTdrProgress(0);
};
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API) // 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
const handleSetReference = async () => { const handleSetReference = async () => {
if ( if (
@@ -94,15 +124,19 @@ const TDRChartActionBar: React.FC = () => {
try { try {
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot); console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
console.log("📡 CGI URL:", cgiUrl); console.log("📡 CGI URL:", cgiUrl);
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
const response = await fetch(cgiUrl); if (isDev) {
// Dev / Simulator: sofort starten & Progress anzeigen
if (!response.ok) { await new Promise((r) => setTimeout(r, 150));
throw new Error(`CGI-Fehler: ${response.status}`); console.log("✅ [DEV] TDR Mock-Start ok für Slot", selectedSlot);
startTdrProgress();
return;
} }
const response = await fetch(cgiUrl);
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
console.log("✅ TDR Messung gestartet für Slot", selectedSlot); console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
//alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`); startTdrProgress();
} catch (err) { } catch (err) {
console.error("❌ Fehler beim Starten der TDR Messung:", err); console.error("❌ Fehler beim Starten der TDR Messung:", err);
//alert("❌ Fehler beim Starten der TDR Messung."); //alert("❌ Fehler beim Starten der TDR Messung.");
@@ -130,109 +164,141 @@ const TDRChartActionBar: React.FC = () => {
}, [selectedSlot, dispatch]); }, [selectedSlot, dispatch]);
return ( return (
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4"> <>
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */} <div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
<div className="text-sm font-semibold"> {/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
{selectedSlot !== null ? `${selectedSlot + 1}` : "Kein KÜ gewählt"} <div className="text-sm font-semibold">
</div> {selectedSlot !== null ? `${selectedSlot + 1}` : "Kein KÜ gewählt"}
</div>
{/* ✅ Referenz setzen */} {/* ✅ Referenz setzen */}
{selectedId !== null && ( {selectedId !== null && (
<button
onClick={handleSetReference}
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
>
TDR-Kurve als Referenz speichern
</button>
)}
{/* 🚀 TDR starten */}
<button <button
onClick={handleSetReference} onClick={handleStartTDR}
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200" className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
disabled={selectedSlot === null || tdrRunning}
> >
TDR-Kurve als Referenz speichern {tdrRunning
? `TDR läuft... (${Math.min(
100,
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
)}%)`
: "TDR-Messung starten"}
</button> </button>
)}
{/* 🚀 TDR starten */} {/* 🔽 Dropdown für Messungen */}
<button <div className="flex items-center space-x-2">
onClick={handleStartTDR} <Listbox
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap " value={selectedId}
disabled={selectedSlot === null} onChange={(id) => {
> setSelectedId(id);
TDR-Messung starten if (id !== null) {
</button> dispatch(getTDRChartDataByIdThunk(id));
}
{/* 🔽 Dropdown für Messungen */} }}
<div className="flex items-center space-x-2"> disabled={idsForSlot.length === 0}
<Listbox >
value={selectedId} <div className="relative w-96">
onChange={(id) => { <Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
setSelectedId(id); <span className="whitespace-nowrap overflow-hidden text-ellipsis">
if (id !== null) { {selectedId
dispatch(getTDRChartDataByIdThunk(id)); ? (() => {
} const selected = idsForSlot.find(
}} (e) => e.id === selectedId
disabled={idsForSlot.length === 0} );
> return selected
<div className="relative w-96"> ? `${new Date(selected.t).toLocaleString("de-DE", {
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm"> day: "2-digit",
<span className="whitespace-nowrap overflow-hidden text-ellipsis"> month: "2-digit",
{selectedId year: "numeric",
? (() => { hour: "2-digit",
const selected = idsForSlot.find( minute: "2-digit",
(e) => e.id === selectedId second: "2-digit",
); })} Fehlerstelle: ${selected.d} m`
return selected : "Wähle Messung";
? `${new Date(selected.t).toLocaleString("de-DE", { })()
day: "2-digit", : "Wähle Messung"}
month: "2-digit", </span>
year: "numeric", <svg
hour: "2-digit", className="w-5 h-5 text-gray-400"
minute: "2-digit", xmlns="http://www.w3.org/2000/svg"
second: "2-digit", viewBox="0 0 20 20"
})} Fehlerstelle: ${selected.d} m` fill="currentColor"
: "Wähle Messung"; aria-hidden="true"
})()
: "Wähle Messung"}
</span>
<svg
className="w-5 h-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
{idsForSlot.map((entry) => (
<Listbox.Option
key={entry.id}
value={entry.id}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
> >
{new Date(entry.t).toLocaleString("de-DE", { <path
day: "2-digit", fillRule="evenodd"
month: "2-digit", d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
year: "numeric", clipRule="evenodd"
hour: "2-digit", />
minute: "2-digit", </svg>
second: "2-digit", </Listbox.Button>
})}{" "} <Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
Fehlerstelle: {entry.d} m {idsForSlot.map((entry) => (
</Listbox.Option> <Listbox.Option
))} key={entry.id}
</Listbox.Options> value={entry.id}
</div> className={({ selected, active }) =>
</Listbox> `px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
>
{new Date(entry.t).toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}{" "}
Fehlerstelle: {entry.d} m
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
</div>
</div> </div>
</div> {tdrRunning && (
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
<div className="mb-4 text-center space-y-1">
<p className="text-lg font-semibold">
TDR Messung läuft... kann bis zu zwei Minuten dauern
</p>
<p className="text-sm text-gray-700">
Bitte warten{" "}
{Math.min(
100,
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
)}
%
</p>
</div>
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
<div
className="h-full bg-littwin-blue transition-all ease-linear"
style={{
width: `${(tdrProgress / TDR_TOTAL_DURATION) * 100}%`,
}}
/>
</div>
</div>
)}
</>
); );
}; };

View File

@@ -174,18 +174,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
const openTdrModal = () => { const openTdrModal = () => {
setActiveButton("TDR"); setActiveButton("TDR");
setloopTitleText("Entfernung [km]"); setloopTitleText("Entfernung [km]");
const latestTdrDistanceMeters =
Array.isArray(tdmChartData?.[slotIndex]) &&
tdmChartData[slotIndex].length > 0 &&
typeof tdmChartData[slotIndex][0].d === "number"
? tdmChartData[slotIndex][0].d
: 0;
const latestTdrDistance = Number(
(latestTdrDistanceMeters / 1000).toFixed(3)
);
setLoopDisplayValue(latestTdrDistance);
setShowTdrModal(true); setShowTdrModal(true);
}; };
@@ -272,30 +260,16 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
return () => window.removeEventListener("resize", measure); return () => window.removeEventListener("resize", measure);
}, [moduleName48, scrollFeatureEnabled]); }, [moduleName48, scrollFeatureEnabled]);
//--------------------------------- //---------------------------------
const tdmChartData = useSelector( // TDR Distanz wird im Display nicht angezeigt Daten für Modal werden separat geladen
(state: RootState) => state.tdmChartSlice.data
);
const latestTdrDistanceMeters =
Array.isArray(tdmChartData?.[slotIndex]) &&
tdmChartData[slotIndex].length > 0 &&
typeof tdmChartData[slotIndex][0].d === "number"
? tdmChartData[slotIndex][0].d
: 0;
const latestTdrDistance = Number((latestTdrDistanceMeters / 1000).toFixed(3));
//setLoopDisplayValue(latestTdrDistance);
//--------------------------------- //---------------------------------
const loopValue = const rslValue =
activeButton === "TDR" typeof schleifenwiderstand === "number"
? latestTdrDistance
: typeof schleifenwiderstand === "number"
? schleifenwiderstand ? schleifenwiderstand
: Number(schleifenwiderstand); : Number(schleifenwiderstand);
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay( const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
loopValue, rslValue,
activeButton activeButton
); );
@@ -409,7 +383,7 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
.toFixed(2) .toFixed(2)
.replace(".", ",")} MOhm`} .replace(".", ",")} MOhm`}
</span> </span>
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */} {/* 3. Zeile: Schleifenwert (RSL) immer anzeigen, unabhängig von aktivem Button */}
<span <span
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${ className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : "" Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""

View File

@@ -12,22 +12,48 @@ export default function SlotActivityOverlay({
const ksz = useAppSelector((s) => s.deviceEvents.ksz); const ksz = useAppSelector((s) => s.deviceEvents.ksz);
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt); const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt); const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
const alignmentStartedAt = useAppSelector( const comparisonStartedAt = useAppSelector(
(s) => s.deviceEvents.alignmentStartedAt (s) => s.deviceEvents.comparisonStartedAt
);
const loopStartedAtBySlot = useAppSelector(
(s) => s.deviceEvents.loopStartedAtBySlot
);
const tdrStartedAtBySlot = useAppSelector(
(s) => s.deviceEvents.tdrStartedAtBySlot
);
const comparisonStartedAtBySlot = useAppSelector(
(s) => s.deviceEvents.comparisonStartedAtBySlot
); );
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1; const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1; const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
const alignActive = Array.isArray(ksz) && ksz[slotIndex] === 1; const comparisonActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
// Persist whenever arrays change
useEffect(() => {
try {
localStorage.setItem(
"deviceEventsTimingsV1",
JSON.stringify({
loop: loopStartedAtBySlot,
tdr: tdrStartedAtBySlot,
compare: comparisonStartedAtBySlot,
})
);
} catch (e) {
// eslint-disable-next-line no-console
console.warn("Failed to persist timings", e);
}
}, [loopStartedAtBySlot, tdrStartedAtBySlot, comparisonStartedAtBySlot]);
// Progress ticker // Progress ticker
const [now, setNow] = useState<number>(Date.now()); const [now, setNow] = useState<number>(Date.now());
useEffect(() => { useEffect(() => {
const any = loopActive || tdrActive || alignActive; const any = loopActive || tdrActive || comparisonActive;
if (!any) return; if (!any) return;
const id = setInterval(() => setNow(Date.now()), 1000); const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id); return () => clearInterval(id);
}, [loopActive, tdrActive, alignActive]); }, [loopActive, tdrActive, comparisonActive]);
const clamp = (v: number, min = 0, max = 1) => const clamp = (v: number, min = 0, max = 1) =>
Math.max(min, Math.min(max, v)); Math.max(min, Math.min(max, v));
@@ -40,10 +66,10 @@ export default function SlotActivityOverlay({
// Durations // Durations
const LOOP_MS = 2 * 60 * 1000; // ~2 min const LOOP_MS = 2 * 60 * 1000; // ~2 min
const TDR_MS = 30 * 1000; // ~30 s const TDR_MS = 110 * 1000; // ~2 min laut die Eigaben
const ALIGN_MS = 10 * 60 * 1000; // ~10 min const ALIGN_MS = 10 * 60 * 1000; // ~10 min
if (!loopActive && !tdrActive && !alignActive) return null; if (!loopActive && !tdrActive && !comparisonActive) return null;
return ( return (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm"> <div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
@@ -56,7 +82,8 @@ export default function SlotActivityOverlay({
<div> <div>
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div> <div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
{(() => { {(() => {
const { pct } = compute(loopStartedAt, LOOP_MS); const started = loopStartedAtBySlot[slotIndex] ?? loopStartedAt;
const { pct } = compute(started, LOOP_MS);
return ( return (
<div> <div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden"> <div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
@@ -77,7 +104,8 @@ export default function SlotActivityOverlay({
<div> <div>
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div> <div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
{(() => { {(() => {
const { pct } = compute(tdrStartedAt, TDR_MS); const started = tdrStartedAtBySlot[slotIndex] ?? tdrStartedAt;
const { pct } = compute(started, TDR_MS);
return ( return (
<div> <div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden"> <div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
@@ -94,11 +122,13 @@ export default function SlotActivityOverlay({
})()} })()}
</div> </div>
)} )}
{alignActive && ( {comparisonActive && (
<div> <div>
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div> <div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
{(() => { {(() => {
const { pct } = compute(alignmentStartedAt, ALIGN_MS); const started =
comparisonStartedAtBySlot[slotIndex] ?? comparisonStartedAt;
const { pct } = compute(started, ALIGN_MS);
return ( return (
<div> <div>
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden"> <div className="h-2 w-full bg-gray-200 rounded overflow-hidden">

View File

@@ -1,19 +1,19 @@
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts // components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
// Keeps and updates the loop (RSL) display value only when "Schleife" active.
// For ISO or TDR views we do not overwrite the displayed RSL value.
const useLoopDisplay = ( const useLoopDisplay = (
schleifenwiderstand: number, rslValue: number,
activeButton: "Schleife" | "TDR" | "ISO" activeButton: "Schleife" | "TDR" | "ISO"
) => { ) => {
const [loopDisplayValue, setLoopDisplayValue] = const [loopDisplayValue, setLoopDisplayValue] = useState<number>(rslValue);
useState<number>(schleifenwiderstand);
useEffect(() => { useEffect(() => {
if (activeButton === "Schleife") { if (activeButton === "Schleife") {
setLoopDisplayValue(schleifenwiderstand); setLoopDisplayValue(rslValue);
} }
// For ISO and TDR, the value is set manually via setLoopDisplayValue }, [rslValue, activeButton]);
}, [schleifenwiderstand, activeButton]);
return { loopDisplayValue, setLoopDisplayValue }; return { loopDisplayValue, setLoopDisplayValue };
}; };

View File

@@ -69,14 +69,20 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
}, },
}} }}
> >
<div className="p-2 flex justify-between items-center rounded-t-md"> <div className="p-2 flex justify-between items-center rounded-t-md bg-surface-alt border-b border-base">
<h2 className="text-base font-bold">Einstellungen {slot + 1}</h2> <h2 className="text-base font-bold text-fg">
<button onClick={onClose} className="text-2xl hover:text-gray-200"> Einstellungen {slot + 1}
</h2>
<button
onClick={onClose}
className="text-2xl text-fg-muted hover:text-fg transition-colors focus:outline-none focus:ring-2 focus:ring-accent/50 rounded"
aria-label="Modal schließen"
>
<i className="bi bi-x-circle-fill"></i> <i className="bi bi-x-circle-fill"></i>
</button> </button>
</div> </div>
<div className="flex justify-start bg-gray-100 space-x-2 p-2"> <div className="flex justify-start bg-surface-alt space-x-2 p-2 border-b border-base">
{[ {[
{ label: "Allgemein", key: "kue" as const }, { label: "Allgemein", key: "kue" as const },
{ label: "TDR ", key: "tdr" as const }, { label: "TDR ", key: "tdr" as const },
@@ -86,18 +92,17 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
<button <button
key={key} key={key}
onClick={() => setActiveTab(key)} onClick={() => setActiveTab(key)}
className={`px-4 py-1 rounded-t font-bold text-sm ${ className={`px-4 py-1 rounded-t font-semibold text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
activeTab === key activeTab === key
? "bg-white text-littwin-blue" ? "bg-surface text-accent shadow-sm"
: "text-gray-500 hover:text-black" : "text-fg-muted hover:text-fg"
}`} }`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<div className="p-4 bg-surface rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto text-fg">
<div className="p-4 bg-white rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto">
{activeTab === "kue" && ( {activeTab === "kue" && (
<KueEinstellung <KueEinstellung
slot={slot} slot={slot}

View File

@@ -14,22 +14,22 @@ export default function MeldungenTabelle({
}) { }) {
return ( return (
<div className="overflow-auto max-h-[80vh]"> <div className="overflow-auto max-h-[80vh]">
<table className="min-w-full border"> <table className="min-w-full border border-base table-surface text-fg">
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10"> <thead className="text-left sticky top-0 z-10 bg-surface-alt/90 backdrop-blur supports-[backdrop-filter]:bg-surface-alt/70">
<tr> <tr>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Prio Prio
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Zeitstempel Zeitstempel
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Quelle Quelle
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Meldung Meldung
</th> </th>
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <th className="p-2 border border-base bg-surface-alt text-fg font-medium">
Status Status
</th> </th>
</tr> </tr>
@@ -38,15 +38,15 @@ export default function MeldungenTabelle({
{messages.map((msg, index) => ( {messages.map((msg, index) => (
<tr <tr
key={index} key={index}
className="hover:bg-gray-100 dark:hover:bg-gray-700" className="transition-colors hover:bg-surface-alt/60"
> >
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border border-base p-2 bg-surface text-fg">
<div <div
className="w-4 h-4 rounded" className="w-4 h-4 rounded"
style={{ backgroundColor: msg.c }} style={{ backgroundColor: msg.c }}
></div> ></div>
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border border-base p-2 bg-surface text-fg">
{new Date(msg.t).toLocaleString("de-DE", { {new Date(msg.t).toLocaleString("de-DE", {
day: "2-digit", day: "2-digit",
month: "2-digit", month: "2-digit",
@@ -56,13 +56,13 @@ export default function MeldungenTabelle({
second: "2-digit", second: "2-digit",
})} })}
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border border-base p-2 bg-surface text-fg">
{msg.i} {msg.i}
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border border-base p-2 bg-surface text-fg">
{msg.m} {msg.m}
</td> </td>
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"> <td className="border border-base p-2 bg-surface text-fg">
{msg.v} {msg.v}
</td> </td>
</tr> </tr>
@@ -70,7 +70,7 @@ export default function MeldungenTabelle({
</tbody> </tbody>
</table> </table>
{messages.length === 0 && ( {messages.length === 0 && (
<div className="mt-4 text-center text-gray-500 italic"> <div className="mt-4 text-center text-fg-muted italic">
Keine Meldungen im gewählten Zeitraum vorhanden. Keine Meldungen im gewählten Zeitraum vorhanden.
</div> </div>
)} )}

View File

@@ -60,14 +60,14 @@ export default function MeldungenView() {
/> />
<button <button
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))} onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
className="bg-littwin-blue text-white px-4 py-2 rounded h-fit" className="btn-primary px-4 py-2 h-fit"
> >
Anzeigen Anzeigen
</button> </button>
<Listbox value={sourceFilter} onChange={setSourceFilter}> <Listbox value={sourceFilter} onChange={setSourceFilter}>
<div className="relative ml-6 w-64"> <div className="relative ml-6 w-64">
<Listbox.Button className="bg-white text-gray-900 w-full border px-4 py-2 rounded text-left flex justify-between items-center dark:bg-gray-900 dark:text-gray-100"> <Listbox.Button className="bg-[var(--color-surface)] text-[var(--color-fg)] w-full border border-base px-4 py-2 rounded text-left flex justify-between items-center">
<span>{sourceFilter}</span> <span>{sourceFilter}</span>
<svg <svg
className="w-5 h-5 text-gray-400" className="w-5 h-5 text-gray-400"
@@ -83,19 +83,19 @@ export default function MeldungenView() {
/> />
</svg> </svg>
</Listbox.Button> </Listbox.Button>
<Listbox.Options className="bg-white absolute z-50 mt-1 w-full border rounded dark:bg-gray-900"> <Listbox.Options className="bg-[var(--color-surface)] absolute z-50 mt-1 w-full border border-base rounded shadow-sm">
{sources.map((src) => ( {sources.map((src) => (
<Listbox.Option <Listbox.Option
key={src} key={src}
value={src} value={src}
className={({ selected, active, disabled }) => className={({ selected, active, disabled }) =>
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${ `px-4 py-2 cursor-pointer text-[var(--color-fg)] transition-colors ${
selected disabled
? "bg-littwin-blue text-white" ? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
: selected
? "bg-accent text-white"
: active : active
? "bg-blue-100 dark:bg-gray-700 dark:text-white" ? "bg-[var(--color-surface-alt)]"
: disabled
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
: "" : ""
}` }`
} }

View File

@@ -10,14 +10,14 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
const DatabaseSettings: React.FC = () => { const DatabaseSettings: React.FC = () => {
const { isAdminLoggedIn } = useAdminAuth(true); const { isAdminLoggedIn } = useAdminAuth(true);
return ( return (
<div className="p-6 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto rounded shadow text-gray-900 dark:text-gray-100"> <div className="p-6 bg-[var(--color-surface-alt)] max-w-5xl mr-auto rounded shadow text-[var(--color-fg)]">
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2> <h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<button <button
type="button" type="button"
onClick={handleClearMessages} onClick={handleClearMessages}
className="bg-littwin-blue text-white px-4 py-2 rounded shadow " className="btn-accent px-4 py-2 rounded shadow"
> >
Meldungen löschen Meldungen löschen
</button> </button>
@@ -25,7 +25,7 @@ const DatabaseSettings: React.FC = () => {
<button <button
type="button" type="button"
onClick={handleClearLogger} onClick={handleClearLogger}
className="bg-littwin-blue text-white px-4 py-2 rounded shadow " className="btn-accent px-4 py-2 rounded shadow"
> >
Messwerte Logger löschen Messwerte Logger löschen
</button> </button>
@@ -41,7 +41,7 @@ const DatabaseSettings: React.FC = () => {
<button <button
type="button" type="button"
onClick={handleClearDatabase} onClick={handleClearDatabase}
className="bg-littwin-blue text-white px-4 py-2 rounded shadow " className="btn-accent px-4 py-2 rounded shadow"
> >
Datenbank vollständig leeren Datenbank vollständig leeren
</button> </button>
@@ -49,7 +49,7 @@ const DatabaseSettings: React.FC = () => {
<button <button
type="button" type="button"
onClick={handleClearConfig} onClick={handleClearConfig}
className="bg-littwin-blue text-white px-4 py-2 rounded shadow " className="btn-accent px-4 py-2 rounded shadow"
> >
Konfiguration löschen Konfiguration löschen
</button> </button>

View File

@@ -63,8 +63,11 @@ const GeneralSettings: React.FC = () => {
setMac1(systemSettings.mac1 || ""); setMac1(systemSettings.mac1 || "");
}, [systemSettings]); }, [systemSettings]);
const inputCls =
"border border-base focus:border-accent rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none";
return ( return (
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] dark:text-gray-100 "> <div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] text-[var(--color-fg)]">
<h2 className="text-sm md:text-md font-bold mb-2"> <h2 className="text-sm md:text-md font-bold mb-2">
Allgemeine Einstellungen Allgemeine Einstellungen
</h2> </h2>
@@ -74,25 +77,18 @@ const GeneralSettings: React.FC = () => {
<label className="block text-xs md:text-sm font-medium">Name:</label> <label className="block text-xs md:text-sm font-medium">Name:</label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className={inputCls}
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
/> />
</div> </div>
{/* MAC Adresse */} {/* MAC Adresse */}
<div> <div>
<label className="block text-xs md:text-sm font-medium"> <label className="block text-xs md:text-sm font-medium">
MAC Adresse 1: MAC Adresse 1:
</label> </label>
<input <input type="text" className={inputCls} value={mac1} disabled />
type="text"
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
value={mac1}
disabled
/>
</div> </div>
{/* Systemzeit */} {/* Systemzeit */}
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-xs md:text-sm font-medium mb-1"> <label className="block text-xs md:text-sm font-medium mb-1">
@@ -101,13 +97,13 @@ const GeneralSettings: React.FC = () => {
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className={inputCls}
value={systemUhr.replace(/\s*Uhr$/, "")} value={systemUhr.replace(/\s*Uhr$/, "")}
disabled disabled
/> />
<button <button
type="button" type="button"
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap" className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={() => handleSetDateTime()} onClick={() => handleSetDateTime()}
> >
Systemzeit übernehmen Systemzeit übernehmen
@@ -120,7 +116,7 @@ const GeneralSettings: React.FC = () => {
<label className="block text-xs md:text-sm font-medium">IP:</label> <label className="block text-xs md:text-sm font-medium">IP:</label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className={inputCls}
value={ip} value={ip}
onChange={(e) => setIp(e.target.value)} onChange={(e) => setIp(e.target.value)}
/> />
@@ -131,7 +127,7 @@ const GeneralSettings: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className={inputCls}
value={subnet} value={subnet}
onChange={(e) => setSubnet(e.target.value)} onChange={(e) => setSubnet(e.target.value)}
/> />
@@ -142,7 +138,7 @@ const GeneralSettings: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className={inputCls}
value={gateway} value={gateway}
onChange={(e) => setGateway(e.target.value)} onChange={(e) => setGateway(e.target.value)}
/> />
@@ -152,7 +148,7 @@ const GeneralSettings: React.FC = () => {
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2"> <div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
<button <button
type="button" type="button"
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap" className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={() => handleReboot()} onClick={() => handleReboot()}
> >
Neustart CPL Neustart CPL
@@ -160,7 +156,7 @@ const GeneralSettings: React.FC = () => {
{isAdminLoggedIn && ( {isAdminLoggedIn && (
<button <button
type="button" type="button"
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap" className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={() => { onClick={() => {
const confirmed = window.confirm( const confirmed = window.confirm(
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?" "⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"

View File

@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
} }
return ( return (
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100"> <div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2> <h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
<div className="grid md:grid-cols-2 gap-3"> <div className="grid md:grid-cols-2 gap-3">
@@ -35,7 +35,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 1</label> <label className="block text-xs font-medium">NTP Server 1</label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={ntp1} value={ntp1}
onChange={(e) => setNtp1(e.target.value)} onChange={(e) => setNtp1(e.target.value)}
/> />
@@ -45,7 +45,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 2</label> <label className="block text-xs font-medium">NTP Server 2</label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={ntp2} value={ntp2}
onChange={(e) => setNtp2(e.target.value)} onChange={(e) => setNtp2(e.target.value)}
/> />
@@ -55,7 +55,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">NTP Server 3</label> <label className="block text-xs font-medium">NTP Server 3</label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={ntp3} value={ntp3}
onChange={(e) => setNtp3(e.target.value)} onChange={(e) => setNtp3(e.target.value)}
/> />
@@ -65,7 +65,7 @@ const NTPSettings: React.FC = () => {
<label className="block text-xs font-medium">Zeitzone</label> <label className="block text-xs font-medium">Zeitzone</label>
<input <input
type="text" type="text"
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={ntpTimezone} value={ntpTimezone}
onChange={(e) => setNtpTimezone(e.target.value)} onChange={(e) => setNtpTimezone(e.target.value)}
/> />

View File

@@ -21,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
); );
return ( return (
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100 "> <div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<Image <Image
src="/images/OPCUA.jpg" src="/images/OPCUA.jpg"
@@ -44,9 +44,11 @@ export default function OPCUAInterfaceSettings() {
<label className="mr-3 font-medium text-sm">Server Status:</label> <label className="mr-3 font-medium text-sm">Server Status:</label>
<button <button
onClick={() => dispatch(toggleOpcUaServer())} onClick={() => dispatch(toggleOpcUaServer())}
className={`px-3 py-1 rounded text-sm ${ className={`px-3 py-1 rounded text-sm font-medium transition-colors text-white ${
opcuaSettings.isEnabled ? "bg-littwin-blue" : "bg-gray-300" opcuaSettings.isEnabled
} text-white`} ? "bg-accent hover:brightness-110"
: "bg-[var(--color-muted)] hover:bg-[var(--color-fg)]/20"
}`}
> >
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"} {opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
</button> </button>
@@ -62,7 +64,7 @@ export default function OPCUAInterfaceSettings() {
<select <select
value={opcuaSettings.encryption} value={opcuaSettings.encryption}
onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))} onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
className="w-full p-1 border border-gray-300 rounded-md text-sm" className="w-full p-1 border border-base rounded-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)]"
> >
<option value="None">Keine</option> <option value="None">Keine</option>
<option value="Basic256">Basic256</option> <option value="Basic256">Basic256</option>
@@ -74,7 +76,7 @@ export default function OPCUAInterfaceSettings() {
{/* ✅ OPCUA Zustand */} {/* ✅ OPCUA Zustand */}
<div className="mb-3"> <div className="mb-3">
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label> <label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100"> <div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
{opcuaSettings.opcUaZustand} {opcuaSettings.opcUaZustand}
</div> </div>
</div> </div>
@@ -85,7 +87,7 @@ export default function OPCUAInterfaceSettings() {
<div className="flex"> <div className="flex">
<input <input
type="text" type="text"
className="flex-grow p-1 border border-gray-300 dark:border-gray-700 rounded-l-md text-sm !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="flex-grow p-1 border border-base rounded-l-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={nodesetName} value={nodesetName}
onChange={(e) => setNodesetName(e.target.value)} onChange={(e) => setNodesetName(e.target.value)}
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
@@ -106,7 +108,7 @@ export default function OPCUAInterfaceSettings() {
<label className="block font-medium text-sm mb-1"> <label className="block font-medium text-sm mb-1">
Aktuelle OPC-Clients Aktuelle OPC-Clients
</label> </label>
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100"> <div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
{opcUaActiveClientCount} {opcUaActiveClientCount}
</div> </div>
</div> </div>
@@ -120,12 +122,12 @@ export default function OPCUAInterfaceSettings() {
{opcuaSettings.users.map((user) => ( {opcuaSettings.users.map((user) => (
<li <li
key={user.id} key={user.id}
className="p-1 bg-white shadow-sm rounded-md flex justify-between items-center text-sm" className="p-1 bg-[var(--color-surface)] border border-base rounded-md flex justify-between items-center text-sm text-[var(--color-fg)]"
> >
<span className="font-medium">{user.username}</span> <span className="font-medium">{user.username}</span>
<button <button
onClick={() => dispatch(removeOpcUaUser(user.id))} onClick={() => dispatch(removeOpcUaUser(user.id))}
className="text-red-500" className="text-danger hover:underline"
> >
Löschen Löschen
</button> </button>
@@ -140,18 +142,18 @@ export default function OPCUAInterfaceSettings() {
placeholder="Benutzername" placeholder="Benutzername"
value={newUsername} value={newUsername}
onChange={(e) => setNewUsername(e.target.value)} onChange={(e) => setNewUsername(e.target.value)}
className="p-1 border rounded flex-grow text-sm" className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
/> />
<input <input
type="password" type="password"
placeholder="Passwort" placeholder="Passwort"
value={newPassword} value={newPassword}
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
className="p-1 border rounded flex-grow text-sm" className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
/> />
<button <button
onClick={handleAddUser} onClick={handleAddUser}
className="bg-littwin-blue text-white p-1 rounded text-sm" className="btn-primary p-1 rounded text-sm"
> >
Hinzufügen Hinzufügen
</button> </button>

View File

@@ -22,6 +22,12 @@ const UserManagementSettings: React.FC = () => {
() => { () => {
setLoginSuccess(true); setLoginSuccess(true);
setError(""); setError("");
// Speichere die System-Uhrzeit (Login-Zeitpunkt) im localStorage
try {
localStorage.setItem("adminLoginTime", new Date().toISOString());
} catch {
// Ignoriere Speicherfehler (z. B. in Private Mode)
}
}, },
(errorMsg) => { (errorMsg) => {
setLoginSuccess(false); setLoginSuccess(false);
@@ -38,7 +44,7 @@ const UserManagementSettings: React.FC = () => {
}; };
return ( return (
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100"> <div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2> <h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
{/* Admin Login/Logout */} {/* Admin Login/Logout */}
@@ -46,8 +52,15 @@ const UserManagementSettings: React.FC = () => {
{isAdminLoggedIn ? ( {isAdminLoggedIn ? (
<button <button
type="button" type="button"
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap" className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={logoutAdmin} onClick={() => {
try {
localStorage.removeItem("adminLoginTime");
} catch {
// ignore
}
logoutAdmin();
}}
> >
Admin abmelden Admin abmelden
</button> </button>
@@ -57,7 +70,7 @@ const UserManagementSettings: React.FC = () => {
<input <input
type="text" type="text"
placeholder="Benutzername" placeholder="Benutzername"
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -65,14 +78,14 @@ const UserManagementSettings: React.FC = () => {
<input <input
type="password" type="password"
placeholder="Passwort" placeholder="Passwort"
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500" className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
<button <button
type="button" type="button"
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap" className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
onClick={handleLogin} onClick={handleLogin}
> >
Admin anmelden Admin anmelden
@@ -83,9 +96,9 @@ const UserManagementSettings: React.FC = () => {
</div> </div>
{loginSuccess && ( {loginSuccess && (
<p className="text-green-600 text-xs mt-2">Login erfolgreich!</p> <p className="text-success text-xs mt-2">Login erfolgreich!</p>
)} )}
{error && <p className="text-red-500 text-xs mt-2">{error}</p>} {error && <p className="text-danger text-xs mt-2">{error}</p>}
{/* {/*
// Benutzerverwaltungstabelle (kommt später) // Benutzerverwaltungstabelle (kommt später)

View File

@@ -13,7 +13,7 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 "> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-6 rounded shadow-md text-center w-80"> <div className="p-6 rounded shadow-sm text-center w-80 bg-[var(--color-surface)] dark:bg-[var(--color-surface)] text-[var(--color-fg)] border border-[var(--color-border)]">
{/* {/*
<h2 className="text-lg font-bold mb-4"> <h2 className="text-lg font-bold mb-4">
Firmwareupdate Firmwareupdate
@@ -26,9 +26,9 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
</h2> </h2>
Bitte Fenster nicht schließen Bitte Fenster nicht schließen
<h2></h2> <h2></h2>
<div className="w-full bg-gray-200 rounded-full h-4"> <div className="w-full h-4 rounded-full bg-[var(--color-surface-alt)]/80 dark:bg-[var(--color-surface-alt)]/30 border border-[var(--color-border)] overflow-hidden">
<div <div
className="bg-littwin-blue h-4 rounded-full transition-all duration-100" className="h-full rounded-full transition-all duration-200 bg-[var(--color-accent)]"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
></div> ></div>
</div> </div>

View File

@@ -454,7 +454,7 @@ export const DetailModal = ({
<div className="absolute top-0 right-0 flex gap-3"> <div className="absolute top-0 right-0 flex gap-3">
<button <button
onClick={toggleFullScreen} onClick={toggleFullScreen}
className="text-2xl text-gray-600 hover:text-gray-800" className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] transition"
> >
<i <i
className={ className={
@@ -467,7 +467,7 @@ export const DetailModal = ({
<button <button
onClick={handleClose} onClick={handleClose}
className="text-2xl text-gray-600 hover:text-gray-800" className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-danger)] transition"
> >
<i className="bi bi-x-circle-fill"></i> <i className="bi bi-x-circle-fill"></i>
</button> </button>
@@ -481,7 +481,7 @@ export const DetailModal = ({
isLoading={isLoading} isLoading={isLoading}
/> />
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2"> <div className="h-[85%] rounded shadow border p-2 bg-[var(--color-surface)] border-[var(--color-border)]">
<Line ref={chartRef} data={chartData} options={chartOptions} /> <Line ref={chartRef} data={chartData} options={chartOptions} />
</div> </div>
</div> </div>

View File

@@ -16,7 +16,7 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
return ( return (
<div <div
key={key} key={key}
className="p-4 border rounded shadow bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100" className="p-4 border rounded shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-[var(--color-border)] text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
> >
<h2 className="font-semibold">{key}</h2> <h2 className="font-semibold">{key}</h2>
<p> <p>

View File

@@ -71,8 +71,8 @@ const SystemPage = () => {
}; };
return ( return (
<div className="p-4 bg-white dark:bg-gray-900"> <div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
<h1 className="text-xl font-bold mb-4"> <h1 className="text-xl font-bold mb-4 tracking-wide">
System Spannungen & Temperaturen System Spannungen & Temperaturen
</h1> </h1>
@@ -80,7 +80,7 @@ const SystemPage = () => {
<div className="flex justify-center items-center h-[400px]"> <div className="flex justify-center items-center h-[400px]">
<div className="text-center"> <div className="text-center">
<ClipLoader size={50} color="#3B82F6" /> <ClipLoader size={50} color="#3B82F6" />
<p className="mt-4 text-gray-500"> <p className="mt-4 text-[var(--color-fg-muted)]">
Lade Systemdaten bitte warten Lade Systemdaten bitte warten
</p> </p>
</div> </div>

View File

@@ -36,12 +36,12 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
]; ];
return ( return (
<aside className="bg-white dark:bg-gray-900 h-full"> <aside className="h-full bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-r border-[var(--color-border)]">
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}> <nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
{menuItems.map((item) => ( {menuItems.map((item) => (
<div key={item.name}> <div key={item.name}>
{item.disabled ? ( {item.disabled ? (
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 dark:text-gray-600 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg"> <div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-[var(--color-fg-muted)] opacity-60 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
{item.name} {item.name}
</div> </div>
) : ( ) : (
@@ -49,11 +49,13 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
href={formatPath(item.path)} href={formatPath(item.path)}
prefetch={false} prefetch={false}
onClick={() => setActiveLink(item.path)} onClick={() => setActiveLink(item.path)}
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${ className={`block px-4 py-2 mb-4 font-semibold whitespace-nowrap transition duration-200 rounded-r-full pr-6 relative text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg
${
activeLink.startsWith(item.path) activeLink.startsWith(item.path)
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white" ? "bg-[var(--color-accent)] text-white shadow-sm xl:mr-4 xl:w-full"
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800" : "text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/80 dark:hover:bg-[var(--color-surface-alt)]/40"
}`} }
`}
> >
{item.name} {item.name}
</Link> </Link>

View File

@@ -2,7 +2,7 @@
"win_da_state": [ "win_da_state": [
1, 1,
1, 1,
1, 0,
1 1
], ],
"win_da_bezeichnung": [ "win_da_bezeichnung": [

View File

@@ -269,7 +269,12 @@ var tdrMeasurementEvent = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]; ];
//Event Abgleich //Event Abgleich
var alignmentEvent = [ var comparisonEvent = [
// renamed from alignmentEvent
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]; ];
// expose for browser simulation
if (typeof window !== "undefined") {
window.comparisonEvent = comparisonEvent;
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.855", "version": "1.6.879",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.855", "version": "1.6.879",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.1.0",
"@headlessui/react": "^2.2.4", "@headlessui/react": "^2.2.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.855", "version": "1.6.879",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",

View File

@@ -148,11 +148,11 @@ function AppContent({
}, [pathname, dispatch]); }, [pathname, dispatch]);
return ( return (
<div className="flex flex-col h-screen overflow-hidden bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100"> <div className="flex flex-col h-screen overflow-hidden bg-[var(--color-background)] text-[var(--color-fg)]">
<Header /> <Header />
<div className="flex flex-grow w-full"> <div className="flex flex-grow w-full">
<Navigation className="w-56" /> <Navigation className="w-56" />
<main className="w-full flex-grow bg-white dark:bg-gray-900"> <main className="w-full flex-grow bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-l border-[var(--color-border)]">
{sessionExpired && ( {sessionExpired && (
<div className="bg-red-500 text-white p-4 text-center"> <div className="bg-red-500 text-white p-4 text-center">
Ihre Sitzung ist abgelaufen oder die Verbindung ist Ihre Sitzung ist abgelaufen oder die Verbindung ist

View File

@@ -8,6 +8,13 @@ export default function Document() {
<link rel="icon" href="/favicon.png" type="image/png" /> <link rel="icon" href="/favicon.png" type="image/png" />
</Head> </Head>
<body> <body>
{/* Theme init (executed before React hydration) */}
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: `(() => { try { const ls = localStorage.getItem('theme'); const mql = window.matchMedia('(prefers-color-scheme: dark)'); const wantDark = ls === 'dark' || (!ls && mql.matches); if (wantDark) document.documentElement.classList.add('dark'); } catch(e) {} })();`,
}}
/>
<Main /> {/* Hier wird der Seiteninhalt eingebettet */} <Main /> {/* Hier wird der Seiteninhalt eingebettet */}
<NextScript /> {/* Fügt Next.js-Skripte für die Seite hinzu */} <NextScript /> {/* Fügt Next.js-Skripte für die Seite hinzu */}
</body> </body>

View File

@@ -35,8 +35,8 @@ var win_tdrLocation=[<%=KTF80%>,<%=KTF81%>,<%=KTF82%>,<%=KTF83%>];//Entfernung B
var loopMeasurementEvent=[<%=KSX80%>, <%=KSX81%>, <%=KSX82%>, <%=KSX83%>]; var loopMeasurementEvent=[<%=KSX80%>, <%=KSX81%>, <%=KSX82%>, <%=KSX83%>];
//Event TDR-Messung //Event TDR-Messung
var tdrMeasurementEvent=[<%=KSY80%>, <%=KSY81%>, <%=KSY82%>, <%=KSY83%>]; var tdrMeasurementEvent=[<%=KSY80%>, <%=KSY81%>, <%=KSY82%>, <%=KSY83%>];
//Event Abgleich //Event Comparison (ehem. Abgleich)
var alignmentEvent=[<%=KSZ80%>, <%=KSZ81%>, <%=KSZ82%>, <%=KSZ83%>]; var comparisonEvent=[<%=KSZ80%>, <%=KSZ81%>, <%=KSZ82%>, <%=KSZ83%>];
//--------------------------------------------------- //---------------------------------------------------

View File

@@ -3,13 +3,17 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface DeviceEventsState { export interface DeviceEventsState {
ksx: number[]; // 32 Slots: Schleifenmessung aktiv ksx: number[]; // 32 Slots: Schleifenmessung aktiv
ksy: number[]; // 32 Slots: TDR-Messung aktiv ksy: number[]; // 32 Slots: TDR-Messung aktiv
ksz: number[]; // 32 Slots: Abgleich aktiv ksz: number[]; // 32 Slots: Comparison (ehem. Abgleich) aktiv
anyLoopActive: boolean; anyLoopActive: boolean;
anyTdrActive: boolean; anyTdrActive: boolean;
anyAlignmentActive: boolean; anyComparisonActive: boolean; // renamed from anyAlignmentActive
loopStartedAt: number | null; // unix ms timestamp when KSX became active loopStartedAt: number | null; // unix ms timestamp when KSX became active
tdrStartedAt: number | null; // unix ms timestamp when KSY became active tdrStartedAt: number | null; // unix ms timestamp when KSY became active
alignmentStartedAt: number | null; // unix ms timestamp when KSZ became active comparisonStartedAt: number | null; // renamed from alignmentStartedAt
// per-slot start timestamps (persistable)
loopStartedAtBySlot: (number | null)[];
tdrStartedAtBySlot: (number | null)[];
comparisonStartedAtBySlot: (number | null)[]; // renamed from alignmentStartedAtBySlot
} }
const ZERO32 = Array.from({ length: 32 }, () => 0); const ZERO32 = Array.from({ length: 32 }, () => 0);
@@ -20,10 +24,13 @@ const initialState: DeviceEventsState = {
ksz: ZERO32.slice(), ksz: ZERO32.slice(),
anyLoopActive: false, anyLoopActive: false,
anyTdrActive: false, anyTdrActive: false,
anyAlignmentActive: false, anyComparisonActive: false,
loopStartedAt: null, loopStartedAt: null,
tdrStartedAt: null, tdrStartedAt: null,
alignmentStartedAt: null, comparisonStartedAt: null,
loopStartedAtBySlot: Array.from({ length: 32 }, () => null),
tdrStartedAtBySlot: Array.from({ length: 32 }, () => null),
comparisonStartedAtBySlot: Array.from({ length: 32 }, () => null),
}; };
export const deviceEventsSlice = createSlice({ export const deviceEventsSlice = createSlice({
@@ -36,7 +43,10 @@ export const deviceEventsSlice = createSlice({
) { ) {
const prevLoop = state.anyLoopActive; const prevLoop = state.anyLoopActive;
const prevTdr = state.anyTdrActive; const prevTdr = state.anyTdrActive;
const prevAlign = state.anyAlignmentActive; const prevCompare = state.anyComparisonActive;
const prevKsx = state.ksx.slice();
const prevKsy = state.ksy.slice();
const prevKsz = state.ksz.slice();
const to32 = (arr?: number[]) => { const to32 = (arr?: number[]) => {
if (!Array.isArray(arr)) return ZERO32.slice(); if (!Array.isArray(arr)) return ZERO32.slice();
const a = arr const a = arr
@@ -50,17 +60,43 @@ export const deviceEventsSlice = createSlice({
state.ksz = to32(action.payload.ksz); state.ksz = to32(action.payload.ksz);
state.anyLoopActive = state.ksx.some((v) => v === 1); state.anyLoopActive = state.ksx.some((v) => v === 1);
state.anyTdrActive = state.ksy.some((v) => v === 1); state.anyTdrActive = state.ksy.some((v) => v === 1);
state.anyAlignmentActive = state.ksz.some((v) => v === 1); state.anyComparisonActive = state.ksz.some((v) => v === 1);
// Transition detection to set/reset startedAt timestamps // Global transition detection
if (!prevLoop && state.anyLoopActive) state.loopStartedAt = Date.now(); if (!prevLoop && state.anyLoopActive) state.loopStartedAt = Date.now();
if (prevLoop && !state.anyLoopActive) state.loopStartedAt = null; if (prevLoop && !state.anyLoopActive) state.loopStartedAt = null;
if (!prevTdr && state.anyTdrActive) state.tdrStartedAt = Date.now(); if (!prevTdr && state.anyTdrActive) state.tdrStartedAt = Date.now();
if (prevTdr && !state.anyTdrActive) state.tdrStartedAt = null; if (prevTdr && !state.anyTdrActive) state.tdrStartedAt = null;
if (!prevAlign && state.anyAlignmentActive) if (!prevCompare && state.anyComparisonActive)
state.alignmentStartedAt = Date.now(); state.comparisonStartedAt = Date.now();
if (prevAlign && !state.anyAlignmentActive) if (prevCompare && !state.anyComparisonActive)
state.alignmentStartedAt = null; state.comparisonStartedAt = null;
// Per-slot transition detection
for (let i = 0; i < 32; i++) {
if (prevKsx[i] === 0 && state.ksx[i] === 1) {
// Only set if no existing (hydrated) timestamp
if (!state.loopStartedAtBySlot[i]) {
state.loopStartedAtBySlot[i] = Date.now();
}
} else if (prevKsx[i] === 1 && state.ksx[i] === 0) {
state.loopStartedAtBySlot[i] = null;
}
if (prevKsy[i] === 0 && state.ksy[i] === 1) {
if (!state.tdrStartedAtBySlot[i]) {
state.tdrStartedAtBySlot[i] = Date.now();
}
} else if (prevKsy[i] === 1 && state.ksy[i] === 0) {
state.tdrStartedAtBySlot[i] = null;
}
if (prevKsz[i] === 0 && state.ksz[i] === 1) {
if (!state.comparisonStartedAtBySlot[i]) {
state.comparisonStartedAtBySlot[i] = Date.now();
}
} else if (prevKsz[i] === 1 && state.ksz[i] === 0) {
state.comparisonStartedAtBySlot[i] = null;
}
}
}, },
resetEvents(state) { resetEvents(state) {
state.ksx = ZERO32.slice(); state.ksx = ZERO32.slice();
@@ -68,13 +104,39 @@ export const deviceEventsSlice = createSlice({
state.ksz = ZERO32.slice(); state.ksz = ZERO32.slice();
state.anyLoopActive = false; state.anyLoopActive = false;
state.anyTdrActive = false; state.anyTdrActive = false;
state.anyAlignmentActive = false; state.anyComparisonActive = false;
state.loopStartedAt = null; state.loopStartedAt = null;
state.tdrStartedAt = null; state.tdrStartedAt = null;
state.alignmentStartedAt = null; state.comparisonStartedAt = null;
state.loopStartedAtBySlot = Array.from({ length: 32 }, () => null);
state.tdrStartedAtBySlot = Array.from({ length: 32 }, () => null);
state.comparisonStartedAtBySlot = Array.from({ length: 32 }, () => null);
},
initPersistedTimings(
state,
action: PayloadAction<{
loop?: (number | null)[];
tdr?: (number | null)[];
compare?: (number | null)[]; // renamed key
}>
) {
const normalize = (arr?: (number | null)[]) => {
const out: (number | null)[] = Array.isArray(arr)
? arr.slice(0, 32).map((v) => (typeof v === "number" ? v : null))
: [];
while (out.length < 32) out.push(null);
return out;
};
if (action.payload.loop)
state.loopStartedAtBySlot = normalize(action.payload.loop);
if (action.payload.tdr)
state.tdrStartedAtBySlot = normalize(action.payload.tdr);
if (action.payload.compare)
state.comparisonStartedAtBySlot = normalize(action.payload.compare);
}, },
}, },
}); });
export const { setEvents, resetEvents } = deviceEventsSlice.actions; export const { setEvents, resetEvents, initPersistedTimings } =
deviceEventsSlice.actions;
export default deviceEventsSlice.reducer; export default deviceEventsSlice.reducer;

View File

@@ -3,45 +3,255 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Light theme tokens */
:root { :root {
--background: #ffffff; --background: #f1f5f9; /* slate-100 */
--foreground: #171717; --foreground: #0f172a; /* slate-900 */
--littwin-blue: #00aeef; --littwin-blue: #00aeef;
--color-background: #f1f5f9;
--color-surface: #ffffff;
--color-surface-alt: #f8fafc; /* slate-50 */
--color-border: #e2e8f0; /* slate-200 */
--color-fg: #0f172a; /* slate-900 */
--color-fg-muted: #475569; /* slate-600 */
/* Alias for convenience (used in components) */
--color-muted: var(--color-fg-muted);
--color-accent: #00aeef;
--color-accent-soft: #e0f7ff;
--color-danger: #dc2626; /* red-600 */
--color-warning: #f59e0b; /* amber-500 */
--color-success: #16a34a; /* green-600 */
--color-info: #0ea5e9; /* sky-500 */
--color-ring: #38bdf8; /* sky-400 */
--color-input-bg: #ffffff;
--color-input-border: #cbd5e1; /* slate-300 */
} }
@media (prefers-color-scheme: dark) { /* Dark theme overrides (activated via .dark class on <html>) */
:root { .dark {
--background: #0a0a0a; --background: #0f1115;
--foreground: #ededed; --foreground: #e2e8f0;
} --color-background: #0f1115; /* near slate-950 */
--color-surface: #1c232d; /* custom dark surface */
--color-surface-alt: #243140; /* slightly elevated */
--color-border: #334155; /* slate-600 */
--color-fg: #e2e8f0; /* slate-200 */
--color-fg-muted: #94a3b8; /* slate-400 */
--color-muted: var(--color-fg-muted);
--color-accent: #00aeef;
--color-accent-soft: #06394d; /* soft accent background */
--color-danger: #ef4444; /* red-500 */
--color-warning: #fbbf24; /* amber-400 */
--color-success: #22c55e; /* green-500 */
--color-info: #38bdf8; /* sky-400 */
--color-ring: #0ea5e9;
--color-input-bg: #1c232d;
--color-input-border: #475569;
} }
body { body {
color: var(--foreground); color: var(--color-fg);
background: var(--background); background: var(--color-background);
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
accent-color: var(--color-accent);
}
/* Smooth theme transitions (respect reduced motion) */
@media (prefers-reduced-motion: no-preference) {
html,
body {
transition: background-color 0.25s ease, color 0.25s ease;
}
.theme-transition {
transition: background-color 0.25s ease, color 0.25s ease,
border-color 0.25s ease, box-shadow 0.25s ease;
}
} }
@layer utilities { @layer utilities {
.text-balance { .text-balance {
text-wrap: balance; text-wrap: balance;
} }
} /* Semantic shortcut utilities */
.bg-background {
background-color: var(--color-background);
}
.bg-surface {
background-color: var(--color-surface);
}
.bg-surface-alt {
background-color: var(--color-surface-alt);
}
.text-fg {
color: var(--color-fg);
}
.text-fg-muted {
color: var(--color-fg-muted);
}
.text-muted {
color: var(--color-muted);
}
.border-base {
border-color: var(--color-border);
}
.ring-accent {
--tw-ring-color: var(--color-ring);
}
.bg-accent {
background-color: var(--color-accent);
}
.bg-accent-soft {
background-color: var(--color-accent-soft);
}
.text-success {
color: var(--color-success);
}
.text-danger {
color: var(--color-danger);
}
@media (prefers-color-scheme: dark) { /* Component abstractions (no @apply to avoid processing issues in global layer) */
input { .card {
background-color: #333 !important; /* Dunkler Hintergrund im Darkmode */ background: var(--color-surface);
color: #fff !important; /* Weißer Text im Darkmode */ color: var(--color-fg);
border: 1px solid #555; /* Optional: etwas hellerer Rahmen */ border: 1px solid var(--color-border);
border-radius: 0.375rem; /* rounded-md */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.card-elevated {
background: var(--color-surface);
color: var(--color-fg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: 0 4px 10px -2px rgba(0, 0, 0, 0.25),
0 2px 4px rgba(0, 0, 0, 0.15);
}
.table-surface {
background: var(--color-surface);
color: var(--color-fg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
overflow: hidden;
}
.table-head {
background: var(--color-surface-alt);
color: var(--color-fg);
font-weight: 500;
border-bottom: 1px solid var(--color-border);
}
.table-row-hover:hover {
background-color: color-mix(
in srgb,
var(--color-surface-alt) 85%,
transparent
);
}
.btn-accent {
background: var(--color-accent);
color: #fff;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: filter 0.2s ease, box-shadow 0.2s ease;
}
.btn-accent:hover {
filter: brightness(1.1);
}
.btn-accent:focus {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
/* Button Variants */
.btn-primary {
@media (prefers-reduced-motion: no-preference) {
transition: background-color 0.2s ease, filter 0.2s ease;
}
background: var(--color-accent);
color: #fff;
border-radius: 0.375rem;
font-weight: 500;
padding: 0.5rem 1rem;
}
.btn-primary:hover {
filter: brightness(1.1);
}
.btn-primary:focus {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
.btn-muted {
background: var(--color-muted);
color: var(--color-fg);
border-radius: 0.375rem;
font-weight: 500;
padding: 0.5rem 1rem;
}
.btn-muted:hover {
background: var(--color-fg);
color: var(--color-background);
}
.btn-outline {
background: transparent;
color: var(--color-fg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
font-weight: 500;
padding: 0.5rem 1rem;
}
.btn-outline:hover {
background: var(--color-surface-alt);
}
.btn-danger {
background: var(--color-danger);
color: #fff;
border-radius: 0.375rem;
font-weight: 500;
padding: 0.5rem 1rem;
}
.btn-danger:hover {
filter: brightness(1.1);
}
.input-base {
background: var(--color-input-bg);
color: var(--color-fg);
border: 1px solid var(--color-input-border);
border-radius: 0.375rem;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.input-base:focus {
border-color: var(--color-ring);
outline: 1px solid var(--color-ring);
outline-offset: 2px;
}
.input-base:disabled {
opacity: 0.6;
cursor: not-allowed;
} }
} }
@media (prefers-color-scheme: light) { /* Form elements use tokens */
input { input,
background-color: white !important; /* Heller Hintergrund im Lightmode */ select,
color: black !important; /* Schwarzer Text im Lightmode */ textarea {
border: 1px solid #ccc; /* Optional: heller Rahmen */ background-color: var(--color-input-bg) !important;
} color: var(--color-fg) !important;
border: 1px solid var(--color-input-border);
}
input::placeholder,
textarea::placeholder {
color: var(--color-muted);
opacity: 1; /* ensure consistent visibility */
}
input:focus,
select:focus,
textarea:focus {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
border-color: var(--color-ring);
} }
.react-datepicker__day--selected, .react-datepicker__day--selected,

View File

@@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: "class", // Ermöglicht zusätzlich zu .dark auch [data-theme="dark"] als Schalter
darkMode: ["class", '[data-theme="dark"]'],
content: [ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",