Compare commits
24 Commits
c03802e97f
...
feat/scrol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
001b237dd7 | ||
|
|
af21b180f1 | ||
|
|
fefff9419d | ||
|
|
27c60c6742 | ||
|
|
c8ec763aac | ||
|
|
d163df0d96 | ||
|
|
12d3a17f60 | ||
|
|
f3339ccafd | ||
|
|
fab8a02ce9 | ||
|
|
eb0585072d | ||
|
|
a596422056 | ||
|
|
531fa93b70 | ||
|
|
72341abb23 | ||
|
|
9c218b2a1d | ||
|
|
d38d3191c5 | ||
|
|
112f537904 | ||
|
|
25b6c5c3b0 | ||
|
|
398d13bf1b | ||
|
|
91b7c8d40f | ||
|
|
7dfef4b16a | ||
|
|
0397f23196 | ||
|
|
0865d61450 | ||
|
|
2eb8f3a255 | ||
|
|
22321a7ac9 |
@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
|
||||
NEXT_PUBLIC_EXPORT_STATIC=false
|
||||
NEXT_PUBLIC_USE_CGI=false
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
|
||||
NEXT_PUBLIC_EXPORT_STATIC=true
|
||||
NEXT_PUBLIC_USE_CGI=true
|
||||
# App-Versionsnummer
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.855
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.879
|
||||
NEXT_PUBLIC_CPL_MODE=production
|
||||
120
CHANGELOG.md
120
CHANGELOG.md
@@ -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
|
||||
|
||||
- fix: allow scripts in woodpecker
|
||||
|
||||
112
Jenkinsfile
vendored
112
Jenkinsfile
vendored
@@ -1,65 +1,95 @@
|
||||
pipeline {
|
||||
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 {
|
||||
stage('Versions') {
|
||||
steps { sh 'node -v && npm -v' }
|
||||
}
|
||||
|
||||
stage('Verify mocks') {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
sh '''
|
||||
set -euxo pipefail
|
||||
npm ci
|
||||
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)
|
||||
set -eux
|
||||
git status --short || true
|
||||
# Submodule & LFS falls vorhanden
|
||||
git submodule update --init --recursive || true
|
||||
git lfs install || true
|
||||
git lfs fetch || true
|
||||
git lfs checkout || true
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build & E2E (chromium)') {
|
||||
environment {
|
||||
CI = 'true'
|
||||
NODE_ENV = 'production'
|
||||
NEXT_TELEMETRY_DISABLED = '1'
|
||||
PORT = '3000'
|
||||
}
|
||||
stage('verify-mocks') {
|
||||
steps {
|
||||
sh '''
|
||||
set -euxo pipefail
|
||||
# Install devDependencies as well (rimraf, cross-env, etc.)
|
||||
env npm_config_production=false npm ci
|
||||
set -eux
|
||||
docker run --rm -v "$PWD":/ws -w /ws \
|
||||
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
|
||||
npm run build
|
||||
|
||||
# Start local static simulator in background
|
||||
npm run server:sim &
|
||||
|
||||
# Ensure Playwright browsers and OS deps are installed (best-effort)
|
||||
npx playwright install-deps || true
|
||||
npx playwright install
|
||||
|
||||
# Wait until simulator responds on port 3000 (no curl dependency)
|
||||
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();"
|
||||
|
||||
# Run tests (chromium only to match Woodpecker)
|
||||
npx playwright test --project=chromium
|
||||
stage('e2e-dev') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eux
|
||||
docker run --rm -v "$PWD":/ws -w /ws -p 3000:3000 \
|
||||
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||
node -v && npm -v
|
||||
env npm_config_production=false npm ci
|
||||
npm run build
|
||||
npm run server:sim &
|
||||
# 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();\\"
|
||||
npx playwright test --project=chromium
|
||||
"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
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 {
|
||||
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.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
import { setEvents } from "@/redux/slices/deviceEventsSlice";
|
||||
import {
|
||||
setEvents,
|
||||
initPersistedTimings,
|
||||
} from "@/redux/slices/deviceEventsSlice";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
loopMeasurementEvent?: number[];
|
||||
tdrMeasurementEvent?: number[];
|
||||
alignmentEvent?: number[];
|
||||
comparisonEvent?: number[]; // renamed from alignmentEvent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +21,25 @@ export default function DeviceEventsBridge() {
|
||||
|
||||
React.useEffect(() => {
|
||||
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 ksx = Array.isArray(window.loopMeasurementEvent)
|
||||
? window.loopMeasurementEvent
|
||||
@@ -25,8 +47,8 @@ export default function DeviceEventsBridge() {
|
||||
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
||||
? window.tdrMeasurementEvent
|
||||
: undefined;
|
||||
const ksz = Array.isArray(window.alignmentEvent)
|
||||
? window.alignmentEvent
|
||||
const ksz = Array.isArray(window.comparisonEvent)
|
||||
? window.comparisonEvent
|
||||
: undefined;
|
||||
// Build a stable signature of first 32 values per array
|
||||
const to32 = (a?: number[]) => {
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useAppSelector } from "@/redux/store";
|
||||
export default function GlobalActivityOverlay() {
|
||||
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
||||
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 ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||
const alignmentStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.alignmentStartedAt
|
||||
const comparisonStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.comparisonStartedAt
|
||||
);
|
||||
|
||||
const fmt = (arr: number[]) =>
|
||||
@@ -24,13 +24,13 @@ export default function GlobalActivityOverlay() {
|
||||
// Simple 1s ticker so progress bars advance while overlay is shown
|
||||
const [now, setNow] = useState<number>(Date.now());
|
||||
useEffect(() => {
|
||||
const active = anyLoop || anyTdr || anyAlign;
|
||||
const active = anyLoop || anyTdr || anyCompare;
|
||||
if (!active) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [anyLoop, anyTdr, anyAlign]);
|
||||
}, [anyLoop, anyTdr, anyCompare]);
|
||||
|
||||
const active = anyLoop || anyTdr || anyAlign;
|
||||
const active = anyLoop || anyTdr || anyCompare;
|
||||
if (!active) return null;
|
||||
|
||||
const clamp = (v: number, min = 0, max = 1) =>
|
||||
@@ -102,13 +102,13 @@ export default function GlobalActivityOverlay() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{anyAlign && (
|
||||
{anyCompare && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-800 mb-1">
|
||||
Abgleich läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||
Comparison läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||
</div>
|
||||
{(() => {
|
||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
||||
const { pct } = compute(comparisonStartedAt, ALIGN_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
|
||||
@@ -55,45 +55,57 @@ function Footer() {
|
||||
}, [showSlider]);
|
||||
|
||||
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="flex flex-row space-x-2">
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Icon
|
||||
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 className="flex flex-row space-x-2">
|
||||
<Icon icon="charm:phone" className="text-xl text-blue-400" />
|
||||
<p className="text-sm">Telefon: 04402 972577-0</p>
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Icon icon="charm:phone" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">Telefon: 04402 972577-0</p>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Icon icon="mdi:email-outline" className="text-xl text-blue-400" />
|
||||
<p className="text-sm">kontakt@littwin-systemtechnik.de</p>
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Icon icon="mdi:email-outline" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">
|
||||
kontakt@littwin-systemtechnik.de
|
||||
</p>
|
||||
</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)}
|
||||
>
|
||||
<Icon icon="bi:book" className="text-xl text-blue-400" />
|
||||
<p className="text-sm">Handbücher</p>
|
||||
<Icon
|
||||
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
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4 flex justify-between items-center border-b">
|
||||
<h3 className="text-lg font-semibold">PDF Handbücher</h3>
|
||||
<div className="p-4 flex justify-between items-center border-b border-base">
|
||||
<h3 className="text-base font-semibold text-[var(--color-fg)]">
|
||||
PDF Handbücher
|
||||
</h3>
|
||||
<button
|
||||
className="text-gray-500 hover:text-gray-800"
|
||||
className="text-[var(--color-muted)] hover:text-[var(--color-fg)] transition"
|
||||
onClick={() => setShowSlider(false)}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<Icon icon="carbon:close" className="text-2xl" />
|
||||
<Icon icon="carbon:close" className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +114,7 @@ function Footer() {
|
||||
{pdfFiles.map((fileName) => (
|
||||
<li
|
||||
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)}
|
||||
>
|
||||
{fileName}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"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 Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -15,16 +15,18 @@ function Header() {
|
||||
const router = useRouter();
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
|
||||
const autoLogoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Removed duplicate declaration of deviceName
|
||||
|
||||
const handleCloseSettingsModal = () => setShowSettingsModal(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
const handleLogout = useCallback(() => {
|
||||
sessionStorage.removeItem("token"); // Token entfernen
|
||||
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
|
||||
localStorage.removeItem("adminLoginTime"); // Login-Zeitpunkt entfernen
|
||||
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
|
||||
router.push("/offline.html"); // Weiterleitung
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialer Check beim Laden der Komponente
|
||||
@@ -43,6 +45,56 @@ function Header() {
|
||||
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
|
||||
};
|
||||
}, [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>();
|
||||
|
||||
@@ -57,21 +109,43 @@ function Header() {
|
||||
}, [deviceName, dispatch]);
|
||||
//----------------------------------------------------------------
|
||||
|
||||
// Dark/Light Mode Toggle
|
||||
// Dark/Light Mode Toggle (persisted)
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
// Initial state from html class / localStorage (set by _document script before hydration)
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const html = document.documentElement;
|
||||
if (isDark) {
|
||||
html.classList.add("dark");
|
||||
} else {
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
if (typeof window === "undefined") return;
|
||||
const html = document.documentElement;
|
||||
const stored = localStorage.getItem("theme");
|
||||
const active = stored ? stored === "dark" : html.classList.contains("dark");
|
||||
setIsDark(active);
|
||||
}, []);
|
||||
|
||||
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]);
|
||||
|
||||
// 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 (
|
||||
<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
|
||||
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:
|
||||
@@ -102,10 +176,10 @@ function Header() {
|
||||
priority
|
||||
/>
|
||||
<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
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -117,7 +191,7 @@ function Header() {
|
||||
<button
|
||||
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
||||
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"}
|
||||
>
|
||||
{isDark ? (
|
||||
@@ -139,7 +213,8 @@ function Header() {
|
||||
<div className="flex items-center justify-end w-1/4 space-x-1">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -149,7 +224,7 @@ function Header() {
|
||||
|
||||
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
||||
{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
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -37,8 +37,8 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
|
||||
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 laptop:p-1 xl:p-1 ${
|
||||
loading ? "cursor-wait" : ""
|
||||
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 opacity-70" : ""
|
||||
}`}
|
||||
>
|
||||
<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" : ""
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
@@ -92,12 +92,12 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
loading
|
||||
? "cursor-wait"
|
||||
: analogInput.id === activeId
|
||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
||||
: "hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
? "bg-[var(--color-accent-soft)] dark:bg-[var(--color-surface-alt)]/60 text-[var(--color-fg)]"
|
||||
: "hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30"
|
||||
}`}
|
||||
>
|
||||
<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)}
|
||||
>
|
||||
<div className="flex items-center gap-1 ">
|
||||
@@ -109,7 +109,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
</div>
|
||||
</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)}
|
||||
>
|
||||
{typeof analogInput.value === "number"
|
||||
@@ -118,19 +118,19 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
</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)}
|
||||
>
|
||||
{analogInput.unit || "-"}
|
||||
</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)}
|
||||
>
|
||||
{analogInput.label || "----"}
|
||||
</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
|
||||
onClick={() => {
|
||||
handleSelect(analogInput.id!, analogInput);
|
||||
@@ -141,7 +141,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
<Icon icon={settingsIcon} className="text-xl" />
|
||||
</button>
|
||||
</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
|
||||
onClick={() => {
|
||||
handleSelect(analogInput.id!, analogInput);
|
||||
|
||||
@@ -36,8 +36,8 @@ function AnalogInputsView() {
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
<h2 className="text-xl font-semibold mb-4 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-[var(--color-fg)] tracking-wide">
|
||||
Messwerteingänge
|
||||
</h2>
|
||||
<AnalogInputsTable loading={loading} />
|
||||
|
||||
@@ -43,7 +43,7 @@ const Baugruppentraeger: React.FC = () => {
|
||||
baugruppen.push(
|
||||
<div
|
||||
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">
|
||||
{slots.map((version, index) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ const DashboardView: React.FC = () => {
|
||||
}, [dispatch]);
|
||||
//-------------------------------------
|
||||
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 */}
|
||||
<div className="flex justify-between items-center w-full lg:w-2/3">
|
||||
<div className="flex justify-between gap-1">
|
||||
@@ -33,7 +33,7 @@ const DashboardView: React.FC = () => {
|
||||
icon="ri:calendar-schedule-line"
|
||||
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
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -48,22 +48,22 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
||||
<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">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
||||
<table className="min-w-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<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>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
@@ -72,24 +72,24 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
{filteredMessages.slice(0, 20).map((msg, index) => (
|
||||
<tr
|
||||
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
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</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}
|
||||
</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}
|
||||
</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}
|
||||
</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}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -97,7 +97,7 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
</tbody>
|
||||
</table>
|
||||
{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.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<Image
|
||||
src="/images/IP-icon.svg"
|
||||
@@ -49,12 +49,8 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
IP-Adresse
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{ip}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,10 +64,8 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Subnet-Maske
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||
{subnet}
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,8 +81,8 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||
{gateway}
|
||||
</p>
|
||||
</div>
|
||||
@@ -97,8 +91,8 @@ const NetworkInfo: React.FC = () => {
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">Status</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||
{opcUaZustand}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,7 @@ import { Icon } from "@iconify/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store";
|
||||
|
||||
type VersionInfoProps = {
|
||||
className?: string;
|
||||
};
|
||||
type VersionInfoProps = { className?: string };
|
||||
|
||||
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||
const appVersion =
|
||||
@@ -17,26 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
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-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
||||
<div className={`card w-full p-3 laptop:p-2 ${className}`}>
|
||||
<h2 className="text-base font-semibold mb-2 text-[var(--color-fg)]">
|
||||
Versionsinformationen
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-row p-2 space-x-2">
|
||||
<Icon icon="bx:code-block" className="text-xl text-blue-400" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Applikationsversion: {appVersion}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row p-2 space-x-2">
|
||||
<Icon icon="mdi:web" className="text-xl text-blue-400" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Webversion: {webVersion}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="bx:code-block" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">
|
||||
Applikationsversion:{" "}
|
||||
<span className="text-[var(--color-fg)]">{appVersion}</span>
|
||||
</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="mdi:web" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">
|
||||
Webversion:{" "}
|
||||
<span className="text-[var(--color-fg)]">{webVersion}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
|
||||
//console.log("DigitalInputs", inputs);
|
||||
|
||||
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">
|
||||
<Icon
|
||||
icon={inputIcon}
|
||||
@@ -38,19 +38,19 @@ export default function DigitalInputsWidget({
|
||||
/>
|
||||
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
||||
</h2>
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)]">
|
||||
<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>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
@@ -59,9 +59,9 @@ export default function DigitalInputsWidget({
|
||||
{inputs.map((input) => (
|
||||
<tr
|
||||
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 ">
|
||||
<Icon
|
||||
icon={loginIcon}
|
||||
@@ -70,7 +70,7 @@ export default function DigitalInputsWidget({
|
||||
{input.id}
|
||||
</div>
|
||||
</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 ? (
|
||||
<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 ">
|
||||
@@ -91,10 +91,10 @@ export default function DigitalInputsWidget({
|
||||
</div>
|
||||
)}
|
||||
</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}
|
||||
</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={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"
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function DigitalOutputsWidget({
|
||||
};
|
||||
|
||||
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">
|
||||
<Icon
|
||||
icon={outputIcon}
|
||||
@@ -74,19 +74,19 @@ export default function DigitalOutputsWidget({
|
||||
/>
|
||||
Schaltausgänge
|
||||
</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">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
||||
<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-[var(--color-surface)] text-[var(--color-fg)] border-b border-base">
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
@@ -95,33 +95,33 @@ export default function DigitalOutputsWidget({
|
||||
{digitalOutputs.map((output) => (
|
||||
<tr
|
||||
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={outputIcon}
|
||||
className="text-gray-600 mr-1 text-base"
|
||||
className="text-[var(--color-muted)] mr-1 text-base"
|
||||
/>
|
||||
{output.id}
|
||||
</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}
|
||||
</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={switchIcon}
|
||||
className={`cursor-pointer text-base transition ${
|
||||
output.status
|
||||
? "text-littwin-blue"
|
||||
: "text-gray-500 scale-x-[-1]"
|
||||
} dark:hover:text-littwin-blue`}
|
||||
? "text-accent"
|
||||
: "text-[var(--color-muted)] scale-x-[-1]"
|
||||
} hover:text-accent`}
|
||||
onClick={() => handleToggle(output.id)}
|
||||
/>
|
||||
</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={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)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -119,19 +119,23 @@ function KabelueberwachungView() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
{[1, 2, 3, 4].map((rack) => (
|
||||
<button
|
||||
key={rack}
|
||||
onClick={() => changeRack(rack)}
|
||||
className={`mr-2 ${
|
||||
Number(activeRack) === Number(rack)
|
||||
? "bg-littwin-blue text-white p-1 rounded-sm"
|
||||
: "bg-gray-300 p-1 text-sm"
|
||||
}`}
|
||||
>
|
||||
Rack {rack}
|
||||
</button>
|
||||
))}
|
||||
{[1, 2, 3, 4].map((rack) => {
|
||||
const isActive = Number(activeRack) === Number(rack);
|
||||
return (
|
||||
<button
|
||||
key={rack}
|
||||
onClick={() => changeRack(rack)}
|
||||
aria-pressed={isActive}
|
||||
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"
|
||||
: "btn-muted text-fg opacity-90 hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
Rack {rack}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</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%]">
|
||||
{(
|
||||
|
||||
@@ -389,7 +389,8 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
||||
<div className="mb-4 text-center space-y-1">
|
||||
<p className="text-lg font-semibold">RSL Messung läuft</p>
|
||||
<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>
|
||||
</div>
|
||||
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
||||
|
||||
@@ -30,6 +30,36 @@ const TDRChartActionBar: React.FC = () => {
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
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)
|
||||
const handleSetReference = async () => {
|
||||
if (
|
||||
@@ -94,15 +124,19 @@ const TDRChartActionBar: React.FC = () => {
|
||||
try {
|
||||
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
|
||||
console.log("📡 CGI URL:", cgiUrl);
|
||||
|
||||
const response = await fetch(cgiUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`CGI-Fehler: ${response.status}`);
|
||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
|
||||
if (isDev) {
|
||||
// Dev / Simulator: sofort starten & Progress anzeigen
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
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);
|
||||
//alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
|
||||
startTdrProgress();
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Starten der TDR Messung:", err);
|
||||
//alert("❌ Fehler beim Starten der TDR Messung.");
|
||||
@@ -130,109 +164,141 @@ const TDRChartActionBar: React.FC = () => {
|
||||
}, [selectedSlot, dispatch]);
|
||||
|
||||
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="text-sm font-semibold">
|
||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
||||
</div>
|
||||
<>
|
||||
<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="text-sm font-semibold">
|
||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
||||
</div>
|
||||
|
||||
{/* ✅ Referenz setzen */}
|
||||
{selectedId !== null && (
|
||||
{/* ✅ Referenz setzen */}
|
||||
{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
|
||||
onClick={handleSetReference}
|
||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
||||
onClick={handleStartTDR}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 🚀 TDR starten */}
|
||||
<button
|
||||
onClick={handleStartTDR}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
||||
disabled={selectedSlot === null}
|
||||
>
|
||||
TDR-Messung starten
|
||||
</button>
|
||||
|
||||
{/* 🔽 Dropdown für Messungen */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Listbox
|
||||
value={selectedId}
|
||||
onChange={(id) => {
|
||||
setSelectedId(id);
|
||||
if (id !== null) {
|
||||
dispatch(getTDRChartDataByIdThunk(id));
|
||||
}
|
||||
}}
|
||||
disabled={idsForSlot.length === 0}
|
||||
>
|
||||
<div className="relative w-96">
|
||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedId
|
||||
? (() => {
|
||||
const selected = idsForSlot.find(
|
||||
(e) => e.id === selectedId
|
||||
);
|
||||
return selected
|
||||
? `${new Date(selected.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})} – Fehlerstelle: ${selected.d} m`
|
||||
: "Wähle Messung";
|
||||
})()
|
||||
: "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"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
{/* 🔽 Dropdown für Messungen */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Listbox
|
||||
value={selectedId}
|
||||
onChange={(id) => {
|
||||
setSelectedId(id);
|
||||
if (id !== null) {
|
||||
dispatch(getTDRChartDataByIdThunk(id));
|
||||
}
|
||||
}}
|
||||
disabled={idsForSlot.length === 0}
|
||||
>
|
||||
<div className="relative w-96">
|
||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedId
|
||||
? (() => {
|
||||
const selected = idsForSlot.find(
|
||||
(e) => e.id === selectedId
|
||||
);
|
||||
return selected
|
||||
? `${new Date(selected.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})} – Fehlerstelle: ${selected.d} m`
|
||||
: "Wähle Messung";
|
||||
})()
|
||||
: "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"
|
||||
>
|
||||
{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>
|
||||
<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", {
|
||||
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>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -174,18 +174,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
const openTdrModal = () => {
|
||||
setActiveButton("TDR");
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -272,30 +260,16 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
return () => window.removeEventListener("resize", measure);
|
||||
}, [moduleName48, scrollFeatureEnabled]);
|
||||
//---------------------------------
|
||||
const tdmChartData = useSelector(
|
||||
(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);
|
||||
// TDR Distanz wird im Display nicht angezeigt – Daten für Modal werden separat geladen
|
||||
|
||||
//---------------------------------
|
||||
|
||||
const loopValue =
|
||||
activeButton === "TDR"
|
||||
? latestTdrDistance
|
||||
: typeof schleifenwiderstand === "number"
|
||||
const rslValue =
|
||||
typeof schleifenwiderstand === "number"
|
||||
? schleifenwiderstand
|
||||
: Number(schleifenwiderstand);
|
||||
|
||||
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
|
||||
loopValue,
|
||||
rslValue,
|
||||
activeButton
|
||||
);
|
||||
|
||||
@@ -409,7 +383,7 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
.toFixed(2)
|
||||
.replace(".", ",")} MOhm`}
|
||||
</span>
|
||||
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
|
||||
{/* 3. Zeile: Schleifenwert (RSL) immer anzeigen, unabhängig von aktivem Button */}
|
||||
<span
|
||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
||||
|
||||
@@ -12,22 +12,48 @@ export default function SlotActivityOverlay({
|
||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||
const alignmentStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.alignmentStartedAt
|
||||
const comparisonStartedAt = useAppSelector(
|
||||
(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 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
|
||||
const [now, setNow] = useState<number>(Date.now());
|
||||
useEffect(() => {
|
||||
const any = loopActive || tdrActive || alignActive;
|
||||
const any = loopActive || tdrActive || comparisonActive;
|
||||
if (!any) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [loopActive, tdrActive, alignActive]);
|
||||
}, [loopActive, tdrActive, comparisonActive]);
|
||||
|
||||
const clamp = (v: number, min = 0, max = 1) =>
|
||||
Math.max(min, Math.min(max, v));
|
||||
@@ -40,10 +66,10 @@ export default function SlotActivityOverlay({
|
||||
|
||||
// Durations
|
||||
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
|
||||
|
||||
if (!loopActive && !tdrActive && !alignActive) return null;
|
||||
if (!loopActive && !tdrActive && !comparisonActive) return null;
|
||||
|
||||
return (
|
||||
<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 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 (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
@@ -77,7 +104,8 @@ export default function SlotActivityOverlay({
|
||||
<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 (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
@@ -94,11 +122,13 @@ export default function SlotActivityOverlay({
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{alignActive && (
|
||||
{comparisonActive && (
|
||||
<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 (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
|
||||
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 = (
|
||||
schleifenwiderstand: number,
|
||||
rslValue: number,
|
||||
activeButton: "Schleife" | "TDR" | "ISO"
|
||||
) => {
|
||||
const [loopDisplayValue, setLoopDisplayValue] =
|
||||
useState<number>(schleifenwiderstand);
|
||||
const [loopDisplayValue, setLoopDisplayValue] = useState<number>(rslValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeButton === "Schleife") {
|
||||
setLoopDisplayValue(schleifenwiderstand);
|
||||
setLoopDisplayValue(rslValue);
|
||||
}
|
||||
// For ISO and TDR, the value is set manually via setLoopDisplayValue
|
||||
}, [schleifenwiderstand, activeButton]);
|
||||
}, [rslValue, activeButton]);
|
||||
|
||||
return { loopDisplayValue, setLoopDisplayValue };
|
||||
};
|
||||
|
||||
@@ -69,14 +69,20 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="p-2 flex justify-between items-center rounded-t-md">
|
||||
<h2 className="text-base font-bold">Einstellungen KÜ {slot + 1}</h2>
|
||||
<button onClick={onClose} className="text-2xl hover:text-gray-200">
|
||||
<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 text-fg">
|
||||
Einstellungen KÜ {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>
|
||||
</button>
|
||||
</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: "TDR ", key: "tdr" as const },
|
||||
@@ -86,18 +92,17 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
<button
|
||||
key={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
|
||||
? "bg-white text-littwin-blue"
|
||||
: "text-gray-500 hover:text-black"
|
||||
? "bg-surface text-accent shadow-sm"
|
||||
: "text-fg-muted hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto">
|
||||
<div className="p-4 bg-surface rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto text-fg">
|
||||
{activeTab === "kue" && (
|
||||
<KueEinstellung
|
||||
slot={slot}
|
||||
|
||||
@@ -14,22 +14,22 @@ export default function MeldungenTabelle({
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-auto max-h-[80vh]">
|
||||
<table className="min-w-full border">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
||||
<table className="min-w-full border border-base table-surface text-fg">
|
||||
<thead className="text-left sticky top-0 z-10 bg-surface-alt/90 backdrop-blur supports-[backdrop-filter]:bg-surface-alt/70">
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
@@ -38,15 +38,15 @@ export default function MeldungenTabelle({
|
||||
{messages.map((msg, index) => (
|
||||
<tr
|
||||
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
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</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", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
@@ -56,13 +56,13 @@ export default function MeldungenTabelle({
|
||||
second: "2-digit",
|
||||
})}
|
||||
</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}
|
||||
</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}
|
||||
</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}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -70,7 +70,7 @@ export default function MeldungenTabelle({
|
||||
</tbody>
|
||||
</table>
|
||||
{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.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -60,14 +60,14 @@ export default function MeldungenView() {
|
||||
/>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
||||
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
||||
<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>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
@@ -83,19 +83,19 @@ export default function MeldungenView() {
|
||||
/>
|
||||
</svg>
|
||||
</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) => (
|
||||
<Listbox.Option
|
||||
key={src}
|
||||
value={src}
|
||||
className={({ selected, active, disabled }) =>
|
||||
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
`px-4 py-2 cursor-pointer text-[var(--color-fg)] transition-colors ${
|
||||
disabled
|
||||
? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
|
||||
: selected
|
||||
? "bg-accent text-white"
|
||||
: active
|
||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
||||
: disabled
|
||||
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
||||
? "bg-[var(--color-surface-alt)]"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
|
||||
const DatabaseSettings: React.FC = () => {
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
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>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -25,7 +25,7 @@ const DatabaseSettings: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -41,7 +41,7 @@ const DatabaseSettings: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -49,7 +49,7 @@ const DatabaseSettings: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -63,8 +63,11 @@ const GeneralSettings: React.FC = () => {
|
||||
setMac1(systemSettings.mac1 || "");
|
||||
}, [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 (
|
||||
<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">
|
||||
Allgemeine Einstellungen
|
||||
</h2>
|
||||
@@ -74,25 +77,18 @@ const GeneralSettings: React.FC = () => {
|
||||
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MAC Adresse */}
|
||||
<div>
|
||||
<label className="block text-xs md:text-sm font-medium">
|
||||
MAC Adresse 1:
|
||||
</label>
|
||||
<input
|
||||
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
|
||||
/>
|
||||
<input type="text" className={inputCls} value={mac1} disabled />
|
||||
</div>
|
||||
|
||||
{/* Systemzeit */}
|
||||
<div className="col-span-2">
|
||||
<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">
|
||||
<input
|
||||
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$/, "")}
|
||||
disabled
|
||||
/>
|
||||
<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()}
|
||||
>
|
||||
Systemzeit übernehmen
|
||||
@@ -120,7 +116,7 @@ const GeneralSettings: React.FC = () => {
|
||||
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => setIp(e.target.value)}
|
||||
/>
|
||||
@@ -131,7 +127,7 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => setSubnet(e.target.value)}
|
||||
/>
|
||||
@@ -142,7 +138,7 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
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}
|
||||
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">
|
||||
<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()}
|
||||
>
|
||||
Neustart CPL
|
||||
@@ -160,7 +156,7 @@ const GeneralSettings: React.FC = () => {
|
||||
{isAdminLoggedIn && (
|
||||
<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={() => {
|
||||
const confirmed = window.confirm(
|
||||
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
||||
|
||||
@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
<input
|
||||
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}
|
||||
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>
|
||||
<input
|
||||
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}
|
||||
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>
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => setNtp3(e.target.value)}
|
||||
/>
|
||||
@@ -65,7 +65,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">Zeitzone</label>
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => setNtpTimezone(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
);
|
||||
|
||||
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">
|
||||
<Image
|
||||
src="/images/OPCUA.jpg"
|
||||
@@ -44,9 +44,11 @@ export default function OPCUAInterfaceSettings() {
|
||||
<label className="mr-3 font-medium text-sm">Server Status:</label>
|
||||
<button
|
||||
onClick={() => dispatch(toggleOpcUaServer())}
|
||||
className={`px-3 py-1 rounded text-sm ${
|
||||
opcuaSettings.isEnabled ? "bg-littwin-blue" : "bg-gray-300"
|
||||
} text-white`}
|
||||
className={`px-3 py-1 rounded text-sm font-medium transition-colors text-white ${
|
||||
opcuaSettings.isEnabled
|
||||
? "bg-accent hover:brightness-110"
|
||||
: "bg-[var(--color-muted)] hover:bg-[var(--color-fg)]/20"
|
||||
}`}
|
||||
>
|
||||
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
|
||||
</button>
|
||||
@@ -62,7 +64,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
<select
|
||||
value={opcuaSettings.encryption}
|
||||
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="Basic256">Basic256</option>
|
||||
@@ -74,7 +76,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
{/* ✅ OPCUA Zustand */}
|
||||
<div className="mb-3">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
<div className="flex">
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => setNodesetName(e.target.value)}
|
||||
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">
|
||||
Aktuelle OPC-Clients
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,12 +122,12 @@ export default function OPCUAInterfaceSettings() {
|
||||
{opcuaSettings.users.map((user) => (
|
||||
<li
|
||||
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>
|
||||
<button
|
||||
onClick={() => dispatch(removeOpcUaUser(user.id))}
|
||||
className="text-red-500"
|
||||
className="text-danger hover:underline"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
@@ -140,18 +142,18 @@ export default function OPCUAInterfaceSettings() {
|
||||
placeholder="Benutzername"
|
||||
value={newUsername}
|
||||
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
|
||||
type="password"
|
||||
placeholder="Passwort"
|
||||
value={newPassword}
|
||||
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
|
||||
onClick={handleAddUser}
|
||||
className="bg-littwin-blue text-white p-1 rounded text-sm"
|
||||
className="btn-primary p-1 rounded text-sm"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
|
||||
@@ -22,6 +22,12 @@ const UserManagementSettings: React.FC = () => {
|
||||
() => {
|
||||
setLoginSuccess(true);
|
||||
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) => {
|
||||
setLoginSuccess(false);
|
||||
@@ -38,7 +44,7 @@ const UserManagementSettings: React.FC = () => {
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{/* Admin Login/Logout */}
|
||||
@@ -46,8 +52,15 @@ const UserManagementSettings: React.FC = () => {
|
||||
{isAdminLoggedIn ? (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={logoutAdmin}
|
||||
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={() => {
|
||||
try {
|
||||
localStorage.removeItem("adminLoginTime");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
logoutAdmin();
|
||||
}}
|
||||
>
|
||||
Admin abmelden
|
||||
</button>
|
||||
@@ -57,7 +70,7 @@ const UserManagementSettings: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
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}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -65,14 +78,14 @@ const UserManagementSettings: React.FC = () => {
|
||||
<input
|
||||
type="password"
|
||||
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}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<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}
|
||||
>
|
||||
Admin anmelden
|
||||
@@ -83,9 +96,9 @@ const UserManagementSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{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)
|
||||
|
||||
@@ -13,7 +13,7 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
Firmwareupdate
|
||||
@@ -26,9 +26,9 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
||||
</h2>
|
||||
Bitte Fenster nicht schließen
|
||||
<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
|
||||
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}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -454,7 +454,7 @@ export const DetailModal = ({
|
||||
<div className="absolute top-0 right-0 flex gap-3">
|
||||
<button
|
||||
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
|
||||
className={
|
||||
@@ -467,7 +467,7 @@ export const DetailModal = ({
|
||||
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
@@ -481,7 +481,7 @@ export const DetailModal = ({
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
<p>
|
||||
|
||||
@@ -71,8 +71,8 @@ const SystemPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white dark:bg-gray-900">
|
||||
<h1 className="text-xl font-bold mb-4">
|
||||
<div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||
<h1 className="text-xl font-bold mb-4 tracking-wide">
|
||||
System Spannungen & Temperaturen
|
||||
</h1>
|
||||
|
||||
@@ -80,7 +80,7 @@ const SystemPage = () => {
|
||||
<div className="flex justify-center items-center h-[400px]">
|
||||
<div className="text-center">
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,12 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
||||
];
|
||||
|
||||
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"}`}>
|
||||
{menuItems.map((item) => (
|
||||
<div key={item.name}>
|
||||
{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}
|
||||
</div>
|
||||
) : (
|
||||
@@ -49,11 +49,13 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
||||
href={formatPath(item.path)}
|
||||
prefetch={false}
|
||||
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)
|
||||
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white"
|
||||
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
? "bg-[var(--color-accent)] text-white shadow-sm xl:mr-4 xl:w-full"
|
||||
: "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}
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"win_da_state": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"win_da_bezeichnung": [
|
||||
|
||||
@@ -269,7 +269,12 @@ var tdrMeasurementEvent = [
|
||||
0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
//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,
|
||||
];
|
||||
// expose for browser simulation
|
||||
if (typeof window !== "undefined") {
|
||||
window.comparisonEvent = comparisonEvent;
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cpl-v4",
|
||||
"version": "1.6.855",
|
||||
"version": "1.6.879",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cpl-v4",
|
||||
"version": "1.6.855",
|
||||
"version": "1.6.879",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cpl-v4",
|
||||
"version": "1.6.855",
|
||||
"version": "1.6.879",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
|
||||
@@ -148,11 +148,11 @@ function AppContent({
|
||||
}, [pathname, dispatch]);
|
||||
|
||||
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 />
|
||||
<div className="flex flex-grow w-full">
|
||||
<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 && (
|
||||
<div className="bg-red-500 text-white p-4 text-center">
|
||||
❌ Ihre Sitzung ist abgelaufen oder die Verbindung ist
|
||||
|
||||
@@ -8,6 +8,13 @@ export default function Document() {
|
||||
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||
</Head>
|
||||
<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 */}
|
||||
<NextScript /> {/* Fügt Next.js-Skripte für die Seite hinzu */}
|
||||
</body>
|
||||
|
||||
@@ -35,8 +35,8 @@ var win_tdrLocation=[<%=KTF80%>,<%=KTF81%>,<%=KTF82%>,<%=KTF83%>];//Entfernung B
|
||||
var loopMeasurementEvent=[<%=KSX80%>, <%=KSX81%>, <%=KSX82%>, <%=KSX83%>];
|
||||
//Event TDR-Messung
|
||||
var tdrMeasurementEvent=[<%=KSY80%>, <%=KSY81%>, <%=KSY82%>, <%=KSY83%>];
|
||||
//Event Abgleich
|
||||
var alignmentEvent=[<%=KSZ80%>, <%=KSZ81%>, <%=KSZ82%>, <%=KSZ83%>];
|
||||
//Event Comparison (ehem. Abgleich)
|
||||
var comparisonEvent=[<%=KSZ80%>, <%=KSZ81%>, <%=KSZ82%>, <%=KSZ83%>];
|
||||
|
||||
//---------------------------------------------------
|
||||
|
||||
|
||||
@@ -3,13 +3,17 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
export interface DeviceEventsState {
|
||||
ksx: number[]; // 32 Slots: Schleifenmessung aktiv
|
||||
ksy: number[]; // 32 Slots: TDR-Messung aktiv
|
||||
ksz: number[]; // 32 Slots: Abgleich aktiv
|
||||
ksz: number[]; // 32 Slots: Comparison (ehem. Abgleich) aktiv
|
||||
anyLoopActive: boolean;
|
||||
anyTdrActive: boolean;
|
||||
anyAlignmentActive: boolean;
|
||||
anyComparisonActive: boolean; // renamed from anyAlignmentActive
|
||||
loopStartedAt: number | null; // unix ms timestamp when KSX 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);
|
||||
@@ -20,10 +24,13 @@ const initialState: DeviceEventsState = {
|
||||
ksz: ZERO32.slice(),
|
||||
anyLoopActive: false,
|
||||
anyTdrActive: false,
|
||||
anyAlignmentActive: false,
|
||||
anyComparisonActive: false,
|
||||
loopStartedAt: 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({
|
||||
@@ -36,7 +43,10 @@ export const deviceEventsSlice = createSlice({
|
||||
) {
|
||||
const prevLoop = state.anyLoopActive;
|
||||
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[]) => {
|
||||
if (!Array.isArray(arr)) return ZERO32.slice();
|
||||
const a = arr
|
||||
@@ -50,17 +60,43 @@ export const deviceEventsSlice = createSlice({
|
||||
state.ksz = to32(action.payload.ksz);
|
||||
state.anyLoopActive = state.ksx.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 = null;
|
||||
if (!prevTdr && state.anyTdrActive) state.tdrStartedAt = Date.now();
|
||||
if (prevTdr && !state.anyTdrActive) state.tdrStartedAt = null;
|
||||
if (!prevAlign && state.anyAlignmentActive)
|
||||
state.alignmentStartedAt = Date.now();
|
||||
if (prevAlign && !state.anyAlignmentActive)
|
||||
state.alignmentStartedAt = null;
|
||||
if (!prevCompare && state.anyComparisonActive)
|
||||
state.comparisonStartedAt = Date.now();
|
||||
if (prevCompare && !state.anyComparisonActive)
|
||||
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) {
|
||||
state.ksx = ZERO32.slice();
|
||||
@@ -68,13 +104,39 @@ export const deviceEventsSlice = createSlice({
|
||||
state.ksz = ZERO32.slice();
|
||||
state.anyLoopActive = false;
|
||||
state.anyTdrActive = false;
|
||||
state.anyAlignmentActive = false;
|
||||
state.anyComparisonActive = false;
|
||||
state.loopStartedAt = 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;
|
||||
|
||||
@@ -3,45 +3,255 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Light theme tokens */
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--background: #f1f5f9; /* slate-100 */
|
||||
--foreground: #0f172a; /* slate-900 */
|
||||
--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) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
/* Dark theme overrides (activated via .dark class on <html>) */
|
||||
.dark {
|
||||
--background: #0f1115;
|
||||
--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 {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
color: var(--color-fg);
|
||||
background: var(--color-background);
|
||||
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 {
|
||||
.text-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) {
|
||||
input {
|
||||
background-color: #333 !important; /* Dunkler Hintergrund im Darkmode */
|
||||
color: #fff !important; /* Weißer Text im Darkmode */
|
||||
border: 1px solid #555; /* Optional: etwas hellerer Rahmen */
|
||||
/* Component abstractions (no @apply to avoid processing issues in global layer) */
|
||||
.card {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-fg);
|
||||
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) {
|
||||
input {
|
||||
background-color: white !important; /* Heller Hintergrund im Lightmode */
|
||||
color: black !important; /* Schwarzer Text im Lightmode */
|
||||
border: 1px solid #ccc; /* Optional: heller Rahmen */
|
||||
}
|
||||
/* Form elements use tokens */
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
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,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
// Ermöglicht zusätzlich zu .dark auch [data-theme="dark"] als Schalter
|
||||
darkMode: ["class", '[data-theme="dark"]'],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
|
||||
Reference in New Issue
Block a user