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_EXPORT_STATIC=false
|
||||||
NEXT_PUBLIC_USE_CGI=false
|
NEXT_PUBLIC_USE_CGI=false
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.6.855
|
NEXT_PUBLIC_APP_VERSION=1.6.879
|
||||||
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
|
|||||||
NEXT_PUBLIC_EXPORT_STATIC=true
|
NEXT_PUBLIC_EXPORT_STATIC=true
|
||||||
NEXT_PUBLIC_USE_CGI=true
|
NEXT_PUBLIC_USE_CGI=true
|
||||||
# App-Versionsnummer
|
# App-Versionsnummer
|
||||||
NEXT_PUBLIC_APP_VERSION=1.6.855
|
NEXT_PUBLIC_APP_VERSION=1.6.879
|
||||||
NEXT_PUBLIC_CPL_MODE=production
|
NEXT_PUBLIC_CPL_MODE=production
|
||||||
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
|
## [1.6.855] – 2025-09-05
|
||||||
|
|
||||||
- fix: allow scripts in woodpecker
|
- fix: allow scripts in woodpecker
|
||||||
|
|||||||
112
Jenkinsfile
vendored
112
Jenkinsfile
vendored
@@ -1,65 +1,95 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
tools { nodejs 'node20' } // exakt der Name aus "Manage Jenkins → Tools"
|
|
||||||
|
environment {
|
||||||
|
CI = "true"
|
||||||
|
NODE_ENV = "production"
|
||||||
|
NEXT_TELEMETRY_DISABLED = "1"
|
||||||
|
PORT = "3000"
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
stage('Versions') {
|
stage('Checkout') {
|
||||||
steps { sh 'node -v && npm -v' }
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Verify mocks') {
|
|
||||||
steps {
|
steps {
|
||||||
|
checkout scm
|
||||||
sh '''
|
sh '''
|
||||||
set -euxo pipefail
|
set -eux
|
||||||
npm ci
|
git status --short || true
|
||||||
echo "=== git ls-files ==="
|
# Submodule & LFS falls vorhanden
|
||||||
git ls-files | grep -i "^mocks/device-cgi-simulator/SERVICE/systemMockData.js" || true
|
git submodule update --init --recursive || true
|
||||||
echo "=== ls -la ==="
|
git lfs install || true
|
||||||
ls -la mocks/device-cgi-simulator/SERVICE || true
|
git lfs fetch || true
|
||||||
echo "=== file exists? ==="
|
git lfs checkout || true
|
||||||
test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo "FOUND" || (echo "MISSING" && exit 1)
|
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Build & E2E (chromium)') {
|
stage('verify-mocks') {
|
||||||
environment {
|
|
||||||
CI = 'true'
|
|
||||||
NODE_ENV = 'production'
|
|
||||||
NEXT_TELEMETRY_DISABLED = '1'
|
|
||||||
PORT = '3000'
|
|
||||||
}
|
|
||||||
steps {
|
steps {
|
||||||
sh '''
|
sh '''
|
||||||
set -euxo pipefail
|
set -eux
|
||||||
# Install devDependencies as well (rimraf, cross-env, etc.)
|
docker run --rm -v "$PWD":/ws -w /ws \
|
||||||
env npm_config_production=false npm ci
|
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||||
|
pwd
|
||||||
|
node -v && npm -v
|
||||||
|
npm ci --ignore-scripts
|
||||||
|
echo '=== git ls-files ==='
|
||||||
|
git ls-files | grep -i '^mocks/device-cgi-simulator/SERVICE/systemMockData.js' || true
|
||||||
|
echo '=== ls -la ==='
|
||||||
|
ls -la mocks/device-cgi-simulator/SERVICE || true
|
||||||
|
echo '=== file exists? ==='
|
||||||
|
test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo 'FOUND' || (echo 'MISSING' && exit 1)
|
||||||
|
"
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Build Next.js
|
stage('e2e-dev') {
|
||||||
npm run build
|
steps {
|
||||||
|
sh '''
|
||||||
# Start local static simulator in background
|
set -eux
|
||||||
npm run server:sim &
|
docker run --rm -v "$PWD":/ws -w /ws -p 3000:3000 \
|
||||||
|
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||||
# Ensure Playwright browsers and OS deps are installed (best-effort)
|
node -v && npm -v
|
||||||
npx playwright install-deps || true
|
env npm_config_production=false npm ci
|
||||||
npx playwright install
|
npm run build
|
||||||
|
npm run server:sim &
|
||||||
# Wait until simulator responds on port 3000 (no curl dependency)
|
# Auf Port 3000 warten
|
||||||
node -e "const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();"
|
node -e \\"const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();\\"
|
||||||
|
npx playwright test --project=chromium
|
||||||
# Run tests (chromium only to match Woodpecker)
|
"
|
||||||
npx playwright test --project=chromium
|
|
||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
success {
|
success {
|
||||||
sh 'curl -d "Tests erfolgreich in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35'
|
sh '''
|
||||||
|
docker run --rm curlimages/curl:8.9.1 \
|
||||||
|
-d "Tests erfolgreich in Jenkins" \
|
||||||
|
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||||
|
'''
|
||||||
}
|
}
|
||||||
failure {
|
failure {
|
||||||
sh 'curl -d "Tests fehlgeschlagen in Jenkins" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35'
|
sh '''
|
||||||
|
docker run --rm curlimages/curl:8.9.1 \
|
||||||
|
-d "Tests fehlgeschlagen in Jenkins" \
|
||||||
|
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9Z35
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
always {
|
||||||
|
script {
|
||||||
|
if (fileExists('playwright-report')) {
|
||||||
|
archiveArtifacts artifacts: 'playwright-report/**', onlyIfSuccessful: false
|
||||||
|
} else {
|
||||||
|
echo 'Kein playwright-report gefunden.'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useAppDispatch } from "@/redux/store";
|
import { useAppDispatch } from "@/redux/store";
|
||||||
import { setEvents } from "@/redux/slices/deviceEventsSlice";
|
import {
|
||||||
|
setEvents,
|
||||||
|
initPersistedTimings,
|
||||||
|
} from "@/redux/slices/deviceEventsSlice";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
loopMeasurementEvent?: number[];
|
loopMeasurementEvent?: number[];
|
||||||
tdrMeasurementEvent?: number[];
|
tdrMeasurementEvent?: number[];
|
||||||
alignmentEvent?: number[];
|
comparisonEvent?: number[]; // renamed from alignmentEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +21,25 @@ export default function DeviceEventsBridge() {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let lastSig = "";
|
let lastSig = "";
|
||||||
|
// Hydrate persisted timings once
|
||||||
|
try {
|
||||||
|
const raw =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
localStorage.getItem("deviceEventsTimingsV1");
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
dispatch(
|
||||||
|
initPersistedTimings({
|
||||||
|
loop: parsed.loop,
|
||||||
|
tdr: parsed.tdr,
|
||||||
|
compare: parsed.compare || parsed.align,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("DeviceEventsBridge hydration failed", e);
|
||||||
|
}
|
||||||
const readAndDispatch = () => {
|
const readAndDispatch = () => {
|
||||||
const ksx = Array.isArray(window.loopMeasurementEvent)
|
const ksx = Array.isArray(window.loopMeasurementEvent)
|
||||||
? window.loopMeasurementEvent
|
? window.loopMeasurementEvent
|
||||||
@@ -25,8 +47,8 @@ export default function DeviceEventsBridge() {
|
|||||||
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
||||||
? window.tdrMeasurementEvent
|
? window.tdrMeasurementEvent
|
||||||
: undefined;
|
: undefined;
|
||||||
const ksz = Array.isArray(window.alignmentEvent)
|
const ksz = Array.isArray(window.comparisonEvent)
|
||||||
? window.alignmentEvent
|
? window.comparisonEvent
|
||||||
: undefined;
|
: undefined;
|
||||||
// Build a stable signature of first 32 values per array
|
// Build a stable signature of first 32 values per array
|
||||||
const to32 = (a?: number[]) => {
|
const to32 = (a?: number[]) => {
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { useAppSelector } from "@/redux/store";
|
|||||||
export default function GlobalActivityOverlay() {
|
export default function GlobalActivityOverlay() {
|
||||||
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
||||||
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
|
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
|
||||||
const anyAlign = useAppSelector((s) => s.deviceEvents.anyAlignmentActive);
|
const anyCompare = useAppSelector((s) => s.deviceEvents.anyComparisonActive);
|
||||||
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
||||||
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
||||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||||
const alignmentStartedAt = useAppSelector(
|
const comparisonStartedAt = useAppSelector(
|
||||||
(s) => s.deviceEvents.alignmentStartedAt
|
(s) => s.deviceEvents.comparisonStartedAt
|
||||||
);
|
);
|
||||||
|
|
||||||
const fmt = (arr: number[]) =>
|
const fmt = (arr: number[]) =>
|
||||||
@@ -24,13 +24,13 @@ export default function GlobalActivityOverlay() {
|
|||||||
// Simple 1s ticker so progress bars advance while overlay is shown
|
// Simple 1s ticker so progress bars advance while overlay is shown
|
||||||
const [now, setNow] = useState<number>(Date.now());
|
const [now, setNow] = useState<number>(Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const active = anyLoop || anyTdr || anyAlign;
|
const active = anyLoop || anyTdr || anyCompare;
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [anyLoop, anyTdr, anyAlign]);
|
}, [anyLoop, anyTdr, anyCompare]);
|
||||||
|
|
||||||
const active = anyLoop || anyTdr || anyAlign;
|
const active = anyLoop || anyTdr || anyCompare;
|
||||||
if (!active) return null;
|
if (!active) return null;
|
||||||
|
|
||||||
const clamp = (v: number, min = 0, max = 1) =>
|
const clamp = (v: number, min = 0, max = 1) =>
|
||||||
@@ -102,13 +102,13 @@ export default function GlobalActivityOverlay() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{anyAlign && (
|
{anyCompare && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-800 mb-1">
|
<div className="text-sm text-gray-800 mb-1">
|
||||||
Abgleich läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
Comparison läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||||
</div>
|
</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
const { pct } = compute(comparisonStartedAt, ALIGN_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
|
|||||||
@@ -55,45 +55,57 @@ function Footer() {
|
|||||||
}, [showSlider]);
|
}, [showSlider]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative bg-gray-300 p-4 xl:p-2 2xl:p-4 overflow-hidden text-black laptop:h-[5vh] ">
|
<footer className="relative bg-[var(--color-surface-alt)] border-t border-base p-4 xl:p-2 2xl:p-4 overflow-hidden text-[var(--color-fg)] laptop:h-[5vh] theme-transition">
|
||||||
<div className="container mx-auto flex justify-between">
|
<div className="container mx-auto flex justify-between">
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
<Icon
|
<Icon
|
||||||
icon="material-symbols:factory-outline"
|
icon="material-symbols:factory-outline"
|
||||||
className="text-xl text-blue-400"
|
className="text-xl text-accent"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm">Littwin Systemtechnik GmbH & Co. KG</p>
|
<p className="text-sm text-fg-muted">
|
||||||
|
Littwin Systemtechnik GmbH & Co. KG
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
<Icon icon="charm:phone" className="text-xl text-blue-400" />
|
<Icon icon="charm:phone" className="text-xl text-accent" />
|
||||||
<p className="text-sm">Telefon: 04402 972577-0</p>
|
<p className="text-sm text-fg-muted">Telefon: 04402 972577-0</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
<Icon icon="mdi:email-outline" className="text-xl text-blue-400" />
|
<Icon icon="mdi:email-outline" className="text-xl text-accent" />
|
||||||
<p className="text-sm">kontakt@littwin-systemtechnik.de</p>
|
<p className="text-sm text-fg-muted">
|
||||||
|
kontakt@littwin-systemtechnik.de
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row space-x-2 cursor-pointer"
|
className="flex flex-row space-x-2 cursor-pointer items-center group"
|
||||||
onClick={() => setShowSlider(true)}
|
onClick={() => setShowSlider(true)}
|
||||||
>
|
>
|
||||||
<Icon icon="bi:book" className="text-xl text-blue-400" />
|
<Icon
|
||||||
<p className="text-sm">Handbücher</p>
|
icon="bi:book"
|
||||||
|
className="text-xl text-accent group-hover:brightness-110 transition"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-fg-muted group-hover:text-[var(--color-fg)] transition">
|
||||||
|
Handbücher
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className={`fixed top-0 right-0 w-64 h-full bg-white shadow-lg transform transition-transform duration-300 ${
|
className={`fixed top-0 right-0 w-64 h-full bg-[var(--color-surface)] border-l border-base shadow-lg transform transition-transform duration-300 ${
|
||||||
showSlider ? "translate-x-0" : "translate-x-full"
|
showSlider ? "translate-x-0" : "translate-x-full"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="p-4 flex justify-between items-center border-b">
|
<div className="p-4 flex justify-between items-center border-b border-base">
|
||||||
<h3 className="text-lg font-semibold">PDF Handbücher</h3>
|
<h3 className="text-base font-semibold text-[var(--color-fg)]">
|
||||||
|
PDF Handbücher
|
||||||
|
</h3>
|
||||||
<button
|
<button
|
||||||
className="text-gray-500 hover:text-gray-800"
|
className="text-[var(--color-muted)] hover:text-[var(--color-fg)] transition"
|
||||||
onClick={() => setShowSlider(false)}
|
onClick={() => setShowSlider(false)}
|
||||||
|
aria-label="Schließen"
|
||||||
>
|
>
|
||||||
<Icon icon="carbon:close" className="text-2xl" />
|
<Icon icon="carbon:close" className="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,7 +114,7 @@ function Footer() {
|
|||||||
{pdfFiles.map((fileName) => (
|
{pdfFiles.map((fileName) => (
|
||||||
<li
|
<li
|
||||||
key={fileName}
|
key={fileName}
|
||||||
className="cursor-pointer text-blue-500 hover:underline mb-2"
|
className="cursor-pointer text-accent hover:underline mb-2 text-sm"
|
||||||
onClick={() => loadPDF(fileName)}
|
onClick={() => loadPDF(fileName)}
|
||||||
>
|
>
|
||||||
{fileName}
|
{fileName}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client"; // components/Header.jsx
|
"use client"; // components/Header.jsx
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -15,16 +15,18 @@ function Header() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||||
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
|
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
|
||||||
|
const autoLogoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
// Removed duplicate declaration of deviceName
|
// Removed duplicate declaration of deviceName
|
||||||
|
|
||||||
const handleCloseSettingsModal = () => setShowSettingsModal(false);
|
const handleCloseSettingsModal = () => setShowSettingsModal(false);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = useCallback(() => {
|
||||||
sessionStorage.removeItem("token"); // Token entfernen
|
sessionStorage.removeItem("token"); // Token entfernen
|
||||||
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
|
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
|
||||||
|
localStorage.removeItem("adminLoginTime"); // Login-Zeitpunkt entfernen
|
||||||
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
|
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
|
||||||
router.push("/offline.html"); // Weiterleitung
|
router.push("/offline.html"); // Weiterleitung
|
||||||
};
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialer Check beim Laden der Komponente
|
// Initialer Check beim Laden der Komponente
|
||||||
@@ -43,6 +45,56 @@ function Header() {
|
|||||||
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
|
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
|
||||||
};
|
};
|
||||||
}, [isAdminLoggedIn]);
|
}, [isAdminLoggedIn]);
|
||||||
|
|
||||||
|
// Auto-Logout nach 1 Minute (Test): nutzt adminLoginTime aus localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
// Timer bereinigen, wenn sich der Status ändert
|
||||||
|
if (autoLogoutTimerRef.current) {
|
||||||
|
clearTimeout(autoLogoutTimerRef.current);
|
||||||
|
autoLogoutTimerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdminLoggedIn) return;
|
||||||
|
|
||||||
|
const iso = localStorage.getItem("adminLoginTime");
|
||||||
|
const loginTime = iso ? new Date(iso).getTime() : Date.now();
|
||||||
|
if (!iso) {
|
||||||
|
// Falls älterer Login ohne Zeitstempel, setze jetzt
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
"adminLoginTime",
|
||||||
|
new Date(loginTime).toISOString()
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
void 0; // ignore write errors (e.g., storage disabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 Minute ab Login (60_000 ms), eine Stunde (3_600_000 ms) im Produktivbetrieb
|
||||||
|
const target = loginTime + 3_600_000;
|
||||||
|
const delay = Math.max(0, target - Date.now());
|
||||||
|
|
||||||
|
// Fallback: wenn Datum in Vergangenheit (z.B. Uhrzeit geändert), sofort abmelden
|
||||||
|
autoLogoutTimerRef.current = setTimeout(() => {
|
||||||
|
// Versuche den Button zu klicken, falls vorhanden
|
||||||
|
const btn = document.querySelector<HTMLButtonElement>(
|
||||||
|
'button[aria-label="Abmelden"]'
|
||||||
|
);
|
||||||
|
if (btn) {
|
||||||
|
btn.click();
|
||||||
|
} else {
|
||||||
|
// Fallback direkt
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoLogoutTimerRef.current) {
|
||||||
|
clearTimeout(autoLogoutTimerRef.current);
|
||||||
|
autoLogoutTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isAdminLoggedIn, handleLogout]);
|
||||||
//----------------------------------------------------------------
|
//----------------------------------------------------------------
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
@@ -57,21 +109,43 @@ function Header() {
|
|||||||
}, [deviceName, dispatch]);
|
}, [deviceName, dispatch]);
|
||||||
//----------------------------------------------------------------
|
//----------------------------------------------------------------
|
||||||
|
|
||||||
// Dark/Light Mode Toggle
|
// Dark/Light Mode Toggle (persisted)
|
||||||
const [isDark, setIsDark] = useState(false);
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
// Initial state from html class / localStorage (set by _document script before hydration)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window === "undefined") return;
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
if (isDark) {
|
const stored = localStorage.getItem("theme");
|
||||||
html.classList.add("dark");
|
const active = stored ? stored === "dark" : html.classList.contains("dark");
|
||||||
} else {
|
setIsDark(active);
|
||||||
html.classList.remove("dark");
|
}, []);
|
||||||
}
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const html = document.documentElement;
|
||||||
|
if (isDark) {
|
||||||
|
html.classList.add("dark");
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
} else {
|
||||||
|
html.classList.remove("dark");
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
}
|
}
|
||||||
}, [isDark]);
|
}, [isDark]);
|
||||||
|
|
||||||
|
// Keyboard shortcut Alt + D to toggle theme
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.altKey && (e.key === "d" || e.key === "D")) {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDark((d) => !d);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-gray-300 dark:bg-gray-800 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black dark:text-white ">
|
<header className="bg-[var(--color-surface)] dark:bg-[var(--color-surface)]/90 backdrop-blur flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-[var(--color-fg)] dark:text-[var(--color-fg)] shadow-sm border-b border-[var(--color-border)]">
|
||||||
<div
|
<div
|
||||||
className="absolute transform -translate-y-1/2
|
className="absolute transform -translate-y-1/2
|
||||||
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
|
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
|
||||||
@@ -102,10 +176,10 @@ function Header() {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col leading-tight whitespace-nowrap">
|
<div className="flex flex-col leading-tight whitespace-nowrap">
|
||||||
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-[var(--color-fg)]">
|
||||||
Meldestation
|
Meldestation
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
<p className="text-[var(--color-fg-muted)] text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
||||||
{deviceName}
|
{deviceName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +191,7 @@ function Header() {
|
|||||||
<button
|
<button
|
||||||
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
||||||
onClick={() => setIsDark((d) => !d)}
|
onClick={() => setIsDark((d) => !d)}
|
||||||
className="rounded-full p-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition"
|
className="rounded-full p-2 bg-[var(--color-surface-alt)]/80 hover:bg-[var(--color-surface-alt)] dark:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)] transition border border-[var(--color-border)]"
|
||||||
title={isDark ? "Light Mode" : "Dark Mode"}
|
title={isDark ? "Light Mode" : "Dark Mode"}
|
||||||
>
|
>
|
||||||
{isDark ? (
|
{isDark ? (
|
||||||
@@ -139,7 +213,8 @@ function Header() {
|
|||||||
<div className="flex items-center justify-end w-1/4 space-x-1">
|
<div className="flex items-center justify-end w-1/4 space-x-1">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded"
|
aria-label="Abmelden"
|
||||||
|
className="px-4 py-2 rounded bg-[var(--color-accent)] text-white hover:brightness-110 shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-ring)] focus:ring-offset-2 focus:ring-offset-[var(--color-background)] transition"
|
||||||
>
|
>
|
||||||
Abmelden
|
Abmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -149,7 +224,7 @@ function Header() {
|
|||||||
|
|
||||||
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
||||||
{isAdminLoggedIn && (
|
{isAdminLoggedIn && (
|
||||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-yellow-400 text-center py-2 text-black font-bold">
|
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-[var(--color-warning)] text-center py-2 text-black font-bold tracking-wide">
|
||||||
Admin-Modus aktiv
|
Admin-Modus aktiv
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg laptop:p-1 xl:p-1 ${
|
className={`text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg laptop:p-1 xl:p-1 ${
|
||||||
loading ? "cursor-wait" : ""
|
loading ? "cursor-wait opacity-70" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||||
@@ -54,24 +54,24 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
loading ? "cursor-wait" : ""
|
loading ? "cursor-wait" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b items-center">
|
<thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/30 text-[var(--color-fg)] border-b border-[var(--color-border)] items-center">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Eingang
|
Eingang
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Messwert
|
Messwert
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Einheit
|
Einheit
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Einstellungen
|
Einstellungen
|
||||||
</th>
|
</th>
|
||||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
Messkurve
|
Messkurve
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -92,12 +92,12 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
loading
|
loading
|
||||||
? "cursor-wait"
|
? "cursor-wait"
|
||||||
: analogInput.id === activeId
|
: analogInput.id === activeId
|
||||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
? "bg-[var(--color-accent-soft)] dark:bg-[var(--color-surface-alt)]/60 text-[var(--color-fg)]"
|
||||||
: "hover:bg-gray-100 dark:hover:bg-gray-800"
|
: "hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 ">
|
<div className="flex items-center gap-1 ">
|
||||||
@@ -109,7 +109,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="border p-2 text-right bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 text-right bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
{typeof analogInput.value === "number"
|
{typeof analogInput.value === "number"
|
||||||
@@ -118,19 +118,19 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
{analogInput.unit || "-"}
|
{analogInput.unit || "-"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||||
>
|
>
|
||||||
{analogInput.label || "----"}
|
{analogInput.label || "----"}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelect(analogInput.id!, analogInput);
|
handleSelect(analogInput.id!, analogInput);
|
||||||
@@ -141,7 +141,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
|||||||
<Icon icon={settingsIcon} className="text-xl" />
|
<Icon icon={settingsIcon} className="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelect(analogInput.id!, analogInput);
|
handleSelect(analogInput.id!, analogInput);
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ function AnalogInputsView() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 max-w-3xl text-gray-900 dark:text-gray-100">
|
<div className="rounded-lg p-4 max-w-3xl text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] shadow-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl font-semibold mb-4 text-[var(--color-fg)] tracking-wide">
|
||||||
Messwerteingänge
|
Messwerteingänge
|
||||||
</h2>
|
</h2>
|
||||||
<AnalogInputsTable loading={loading} />
|
<AnalogInputsTable loading={loading} />
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const Baugruppentraeger: React.FC = () => {
|
|||||||
baugruppen.push(
|
baugruppen.push(
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex bg-white shadow-md rounded-lg mb-4 xl:mb-0 lg:mb-0 border border-gray-200 w-full laptop:scale-y-75 xl:scale-y-90"
|
className="flex card mb-4 xl:mb-0 lg:mb-0 w-full laptop:scale-y-75 xl:scale-y-90"
|
||||||
>
|
>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{slots.map((version, index) => {
|
{slots.map((version, index) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const DashboardView: React.FC = () => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
//-------------------------------------
|
//-------------------------------------
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center w-full lg:w-2/3">
|
<div className="flex justify-between items-center w-full lg:w-2/3">
|
||||||
<div className="flex justify-between gap-1">
|
<div className="flex justify-between gap-1">
|
||||||
@@ -33,7 +33,7 @@ const DashboardView: React.FC = () => {
|
|||||||
icon="ri:calendar-schedule-line"
|
icon="ri:calendar-schedule-line"
|
||||||
className="text-littwin-blue text-4xl xl:text-2xl"
|
className="text-littwin-blue text-4xl xl:text-2xl"
|
||||||
/>
|
/>
|
||||||
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100 xl:text-base">
|
<h1 className="text-xl font-bold xl:text-base text-[var(--color-fg)] tracking-wide">
|
||||||
Letzten 20 Meldungen
|
Letzten 20 Meldungen
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,22 +48,22 @@ export default function Last20MessagesTable({ className }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
||||||
<div className="overflow-auto max-h-[80vh]">
|
<div className="overflow-auto max-h-[80vh]">
|
||||||
<table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<table className="min-w-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
<thead className="text-left sticky top-0 z-10 bg-[var(--color-surface-alt)]/70 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Prio
|
Prio
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Zeitstempel
|
Zeitstempel
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Quelle
|
Quelle
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Meldung
|
Meldung
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -72,24 +72,24 @@ export default function Last20MessagesTable({ className }: Props) {
|
|||||||
{filteredMessages.slice(0, 20).map((msg, index) => (
|
{filteredMessages.slice(0, 20).map((msg, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className="hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||||
>
|
>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded"
|
className="w-4 h-4 rounded"
|
||||||
style={{ backgroundColor: msg.c }}
|
style={{ backgroundColor: msg.c }}
|
||||||
></div>
|
></div>
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.t}
|
{msg.t}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.i}
|
{msg.i}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.m}
|
{msg.m}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||||
{msg.v}
|
{msg.v}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -97,7 +97,7 @@ export default function Last20MessagesTable({ className }: Props) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="mt-4 text-center text-gray-500 italic dark:text-gray-400">
|
<div className="mt-4 text-center italic text-[var(--color-fg-muted)]">
|
||||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex-direction: row flex">
|
<div className="w-full flex-direction: row flex">
|
||||||
<div className=" flex-grow flex justify-between items-center mt-1 bg-white dark:bg-gray-800 p-2 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
<div className=" flex-grow flex justify-between items-center mt-1 p-2 rounded-lg shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Image
|
<Image
|
||||||
src="/images/IP-icon.svg"
|
src="/images/IP-icon.svg"
|
||||||
@@ -49,12 +49,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
|
||||||
IP-Adresse
|
<p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{ip}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,10 +64,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
|
||||||
Subnet-Maske
|
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{subnet}
|
{subnet}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,8 +81,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
|
<p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||||
{gateway}
|
{gateway}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,8 +91,8 @@ const NetworkInfo: React.FC = () => {
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
|
<p className="text-xs text-[var(--color-fg-muted)]">Status</p>
|
||||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||||
{opcUaZustand}
|
{opcUaZustand}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { Icon } from "@iconify/react";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { RootState } from "../../../redux/store";
|
import { RootState } from "../../../redux/store";
|
||||||
|
|
||||||
type VersionInfoProps = {
|
type VersionInfoProps = { className?: string };
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||||
const appVersion =
|
const appVersion =
|
||||||
@@ -17,26 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`card w-full p-3 laptop:p-2 ${className}`}>
|
||||||
className={`bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 w-full laptop:p-2 ${className}`}
|
<h2 className="text-base font-semibold mb-2 text-[var(--color-fg)]">
|
||||||
>
|
|
||||||
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
|
||||||
Versionsinformationen
|
Versionsinformationen
|
||||||
</h2>
|
</h2>
|
||||||
|
<ul className="space-y-1">
|
||||||
<div className="flex flex-row p-2 space-x-2">
|
<li className="flex items-start gap-2">
|
||||||
<Icon icon="bx:code-block" className="text-xl text-blue-400" />
|
<Icon icon="bx:code-block" className="text-xl text-accent" />
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-fg-muted">
|
||||||
Applikationsversion: {appVersion}
|
Applikationsversion:{" "}
|
||||||
</p>
|
<span className="text-[var(--color-fg)]">{appVersion}</span>
|
||||||
</div>
|
</p>
|
||||||
|
</li>
|
||||||
<div className="flex flex-row p-2 space-x-2">
|
<li className="flex items-start gap-2">
|
||||||
<Icon icon="mdi:web" className="text-xl text-blue-400" />
|
<Icon icon="mdi:web" className="text-xl text-accent" />
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
<p className="text-sm text-fg-muted">
|
||||||
Webversion: {webVersion}
|
Webversion:{" "}
|
||||||
</p>
|
<span className="text-[var(--color-fg)]">{webVersion}</span>
|
||||||
</div>
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
|
|||||||
//console.log("DigitalInputs", inputs);
|
//console.log("DigitalInputs", inputs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
<div className="text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
||||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||||
<Icon
|
<Icon
|
||||||
icon={inputIcon}
|
icon={inputIcon}
|
||||||
@@ -38,19 +38,19 @@ export default function DigitalInputsWidget({
|
|||||||
/>
|
/>
|
||||||
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
||||||
</h2>
|
</h2>
|
||||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
|
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)]">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
<thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)] border-b border-[var(--color-border)]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Eingang
|
Eingang
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Zustand
|
Zustand
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Aktion
|
Aktion
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -59,9 +59,9 @@ export default function DigitalInputsWidget({
|
|||||||
{inputs.map((input) => (
|
{inputs.map((input) => (
|
||||||
<tr
|
<tr
|
||||||
key={input.id}
|
key={input.id}
|
||||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="border-b border-[var(--color-border)] hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||||
>
|
>
|
||||||
<td className="px-1 py-0 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-0 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<div className="flex items-center gap-1 ">
|
<div className="flex items-center gap-1 ">
|
||||||
<Icon
|
<Icon
|
||||||
icon={loginIcon}
|
icon={loginIcon}
|
||||||
@@ -70,7 +70,7 @@ export default function DigitalInputsWidget({
|
|||||||
{input.id}
|
{input.id}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
{input.eingangOffline ? (
|
{input.eingangOffline ? (
|
||||||
<div className="relative group inline-block">
|
<div className="relative group inline-block">
|
||||||
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
|
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
|
||||||
@@ -91,10 +91,10 @@ export default function DigitalInputsWidget({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
{input.label}
|
{input.label}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={settingsIcon}
|
icon={settingsIcon}
|
||||||
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function DigitalOutputsWidget({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
|
<div className="bg-[var(--color-surface)] text-[var(--color-fg)] shadow-md border border-base p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
|
||||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||||
<Icon
|
<Icon
|
||||||
icon={outputIcon}
|
icon={outputIcon}
|
||||||
@@ -74,19 +74,19 @@ export default function DigitalOutputsWidget({
|
|||||||
/>
|
/>
|
||||||
Schaltausgänge
|
Schaltausgänge
|
||||||
</h2>
|
</h2>
|
||||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900 rounded-lg">
|
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)] rounded-lg">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
<thead className="bg-[var(--color-surface)] text-[var(--color-fg)] border-b border-base">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Ausgang
|
Ausgang
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Bezeichnung
|
Bezeichnung
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Schalter
|
Schalter
|
||||||
</th>
|
</th>
|
||||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
Aktion
|
Aktion
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -95,33 +95,33 @@ export default function DigitalOutputsWidget({
|
|||||||
{digitalOutputs.map((output) => (
|
{digitalOutputs.map((output) => (
|
||||||
<tr
|
<tr
|
||||||
key={output.id}
|
key={output.id}
|
||||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
className="border-b border-base hover:bg-[var(--color-surface-alt)] transition-colors"
|
||||||
>
|
>
|
||||||
<td className="flex items-center px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="flex items-center px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={outputIcon}
|
icon={outputIcon}
|
||||||
className="text-gray-600 mr-1 text-base"
|
className="text-[var(--color-muted)] mr-1 text-base"
|
||||||
/>
|
/>
|
||||||
{output.id}
|
{output.id}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
{output.label}
|
{output.label}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={switchIcon}
|
icon={switchIcon}
|
||||||
className={`cursor-pointer text-base transition ${
|
className={`cursor-pointer text-base transition ${
|
||||||
output.status
|
output.status
|
||||||
? "text-littwin-blue"
|
? "text-accent"
|
||||||
: "text-gray-500 scale-x-[-1]"
|
: "text-[var(--color-muted)] scale-x-[-1]"
|
||||||
} dark:hover:text-littwin-blue`}
|
} hover:text-accent`}
|
||||||
onClick={() => handleToggle(output.id)}
|
onClick={() => handleToggle(output.id)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||||
<Icon
|
<Icon
|
||||||
icon={settingsIcon}
|
icon={settingsIcon}
|
||||||
className="text-gray-400 text-base cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
className="text-[var(--color-muted)] text-base cursor-pointer hover:text-[var(--color-fg)]"
|
||||||
onClick={() => openOutputModal(output)}
|
onClick={() => openOutputModal(output)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -119,19 +119,23 @@ function KabelueberwachungView() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
{[1, 2, 3, 4].map((rack) => (
|
{[1, 2, 3, 4].map((rack) => {
|
||||||
<button
|
const isActive = Number(activeRack) === Number(rack);
|
||||||
key={rack}
|
return (
|
||||||
onClick={() => changeRack(rack)}
|
<button
|
||||||
className={`mr-2 ${
|
key={rack}
|
||||||
Number(activeRack) === Number(rack)
|
onClick={() => changeRack(rack)}
|
||||||
? "bg-littwin-blue text-white p-1 rounded-sm"
|
aria-pressed={isActive}
|
||||||
: "bg-gray-300 p-1 text-sm"
|
className={`mr-2 px-2 py-1 rounded-sm text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent/50 ${
|
||||||
}`}
|
isActive
|
||||||
>
|
? "btn-primary"
|
||||||
Rack {rack}
|
: "btn-muted text-fg opacity-90 hover:opacity-100"
|
||||||
</button>
|
}`}
|
||||||
))}
|
>
|
||||||
|
Rack {rack}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
|
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
|
||||||
{(
|
{(
|
||||||
|
|||||||
@@ -389,7 +389,8 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
|||||||
<div className="mb-4 text-center space-y-1">
|
<div className="mb-4 text-center space-y-1">
|
||||||
<p className="text-lg font-semibold">RSL Messung läuft</p>
|
<p className="text-lg font-semibold">RSL Messung läuft</p>
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-700">
|
||||||
Bitte warten… (noch {TOTAL_DURATION - rslProgress}s)
|
Bitte warten…{" "}
|
||||||
|
{Math.min(100, Math.round((rslProgress / TOTAL_DURATION) * 100))}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
||||||
|
|||||||
@@ -30,6 +30,36 @@ const TDRChartActionBar: React.FC = () => {
|
|||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
|
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
|
||||||
|
|
||||||
|
// ▶ Fortschrittsanzeige für laufende TDR-Messung (max. 120s bzw. konfigurierbar)
|
||||||
|
const TDR_TOTAL_DURATION = parseInt(
|
||||||
|
process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120",
|
||||||
|
10
|
||||||
|
);
|
||||||
|
const [tdrRunning, setTdrRunning] = useState(false);
|
||||||
|
const [tdrProgress, setTdrProgress] = useState(0); // Sekunden
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tdrRunning) return;
|
||||||
|
setTdrProgress(0);
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
|
||||||
|
if (elapsed >= TDR_TOTAL_DURATION) {
|
||||||
|
setTdrProgress(TDR_TOTAL_DURATION);
|
||||||
|
setTdrRunning(false);
|
||||||
|
clearInterval(interval);
|
||||||
|
} else {
|
||||||
|
setTdrProgress(elapsed);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [tdrRunning, TDR_TOTAL_DURATION]);
|
||||||
|
|
||||||
|
const startTdrProgress = () => {
|
||||||
|
setTdrRunning(true);
|
||||||
|
setTdrProgress(0);
|
||||||
|
};
|
||||||
|
|
||||||
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
|
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
|
||||||
const handleSetReference = async () => {
|
const handleSetReference = async () => {
|
||||||
if (
|
if (
|
||||||
@@ -94,15 +124,19 @@ const TDRChartActionBar: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
|
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
|
||||||
console.log("📡 CGI URL:", cgiUrl);
|
console.log("📡 CGI URL:", cgiUrl);
|
||||||
|
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
|
||||||
const response = await fetch(cgiUrl);
|
if (isDev) {
|
||||||
|
// Dev / Simulator: sofort starten & Progress anzeigen
|
||||||
if (!response.ok) {
|
await new Promise((r) => setTimeout(r, 150));
|
||||||
throw new Error(`CGI-Fehler: ${response.status}`);
|
console.log("✅ [DEV] TDR Mock-Start ok für Slot", selectedSlot);
|
||||||
|
startTdrProgress();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await fetch(cgiUrl);
|
||||||
|
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
|
||||||
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
|
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
|
||||||
//alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
|
startTdrProgress();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Fehler beim Starten der TDR Messung:", err);
|
console.error("❌ Fehler beim Starten der TDR Messung:", err);
|
||||||
//alert("❌ Fehler beim Starten der TDR Messung.");
|
//alert("❌ Fehler beim Starten der TDR Messung.");
|
||||||
@@ -130,109 +164,141 @@ const TDRChartActionBar: React.FC = () => {
|
|||||||
}, [selectedSlot, dispatch]);
|
}, [selectedSlot, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
|
<>
|
||||||
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
|
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
|
||||||
<div className="text-sm font-semibold">
|
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
|
||||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
<div className="text-sm font-semibold">
|
||||||
</div>
|
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ✅ Referenz setzen */}
|
{/* ✅ Referenz setzen */}
|
||||||
{selectedId !== null && (
|
{selectedId !== null && (
|
||||||
|
<button
|
||||||
|
onClick={handleSetReference}
|
||||||
|
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
TDR-Kurve als Referenz speichern
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🚀 TDR starten */}
|
||||||
<button
|
<button
|
||||||
onClick={handleSetReference}
|
onClick={handleStartTDR}
|
||||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
||||||
|
disabled={selectedSlot === null || tdrRunning}
|
||||||
>
|
>
|
||||||
TDR-Kurve als Referenz speichern
|
{tdrRunning
|
||||||
|
? `TDR läuft... (${Math.min(
|
||||||
|
100,
|
||||||
|
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
|
||||||
|
)}%)`
|
||||||
|
: "TDR-Messung starten"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 🚀 TDR starten */}
|
{/* 🔽 Dropdown für Messungen */}
|
||||||
<button
|
<div className="flex items-center space-x-2">
|
||||||
onClick={handleStartTDR}
|
<Listbox
|
||||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
value={selectedId}
|
||||||
disabled={selectedSlot === null}
|
onChange={(id) => {
|
||||||
>
|
setSelectedId(id);
|
||||||
TDR-Messung starten
|
if (id !== null) {
|
||||||
</button>
|
dispatch(getTDRChartDataByIdThunk(id));
|
||||||
|
}
|
||||||
{/* 🔽 Dropdown für Messungen */}
|
}}
|
||||||
<div className="flex items-center space-x-2">
|
disabled={idsForSlot.length === 0}
|
||||||
<Listbox
|
>
|
||||||
value={selectedId}
|
<div className="relative w-96">
|
||||||
onChange={(id) => {
|
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||||
setSelectedId(id);
|
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
if (id !== null) {
|
{selectedId
|
||||||
dispatch(getTDRChartDataByIdThunk(id));
|
? (() => {
|
||||||
}
|
const selected = idsForSlot.find(
|
||||||
}}
|
(e) => e.id === selectedId
|
||||||
disabled={idsForSlot.length === 0}
|
);
|
||||||
>
|
return selected
|
||||||
<div className="relative w-96">
|
? `${new Date(selected.t).toLocaleString("de-DE", {
|
||||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
day: "2-digit",
|
||||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
month: "2-digit",
|
||||||
{selectedId
|
year: "numeric",
|
||||||
? (() => {
|
hour: "2-digit",
|
||||||
const selected = idsForSlot.find(
|
minute: "2-digit",
|
||||||
(e) => e.id === selectedId
|
second: "2-digit",
|
||||||
);
|
})} – Fehlerstelle: ${selected.d} m`
|
||||||
return selected
|
: "Wähle Messung";
|
||||||
? `${new Date(selected.t).toLocaleString("de-DE", {
|
})()
|
||||||
day: "2-digit",
|
: "Wähle Messung"}
|
||||||
month: "2-digit",
|
</span>
|
||||||
year: "numeric",
|
<svg
|
||||||
hour: "2-digit",
|
className="w-5 h-5 text-gray-400"
|
||||||
minute: "2-digit",
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
second: "2-digit",
|
viewBox="0 0 20 20"
|
||||||
})} – Fehlerstelle: ${selected.d} m`
|
fill="currentColor"
|
||||||
: "Wähle Messung";
|
aria-hidden="true"
|
||||||
})()
|
|
||||||
: "Wähle Messung"}
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5 text-gray-400"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Listbox.Button>
|
|
||||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
|
||||||
{idsForSlot.map((entry) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={entry.id}
|
|
||||||
value={entry.id}
|
|
||||||
className={({ selected, active }) =>
|
|
||||||
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
|
|
||||||
selected
|
|
||||||
? "bg-littwin-blue text-white"
|
|
||||||
: active
|
|
||||||
? "bg-gray-200"
|
|
||||||
: ""
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{new Date(entry.t).toLocaleString("de-DE", {
|
<path
|
||||||
day: "2-digit",
|
fillRule="evenodd"
|
||||||
month: "2-digit",
|
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
|
||||||
year: "numeric",
|
clipRule="evenodd"
|
||||||
hour: "2-digit",
|
/>
|
||||||
minute: "2-digit",
|
</svg>
|
||||||
second: "2-digit",
|
</Listbox.Button>
|
||||||
})}{" "}
|
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
||||||
– Fehlerstelle: {entry.d} m
|
{idsForSlot.map((entry) => (
|
||||||
</Listbox.Option>
|
<Listbox.Option
|
||||||
))}
|
key={entry.id}
|
||||||
</Listbox.Options>
|
value={entry.id}
|
||||||
</div>
|
className={({ selected, active }) =>
|
||||||
</Listbox>
|
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
|
||||||
|
selected
|
||||||
|
? "bg-littwin-blue text-white"
|
||||||
|
: active
|
||||||
|
? "bg-gray-200"
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{new Date(entry.t).toLocaleString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
})}{" "}
|
||||||
|
– Fehlerstelle: {entry.d} m
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{tdrRunning && (
|
||||||
|
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
|
||||||
|
<div className="mb-4 text-center space-y-1">
|
||||||
|
<p className="text-lg font-semibold">
|
||||||
|
TDR Messung läuft... kann bis zu zwei Minuten dauern
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
Bitte warten…{" "}
|
||||||
|
{Math.min(
|
||||||
|
100,
|
||||||
|
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
||||||
|
<div
|
||||||
|
className="h-full bg-littwin-blue transition-all ease-linear"
|
||||||
|
style={{
|
||||||
|
width: `${(tdrProgress / TDR_TOTAL_DURATION) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -174,18 +174,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
const openTdrModal = () => {
|
const openTdrModal = () => {
|
||||||
setActiveButton("TDR");
|
setActiveButton("TDR");
|
||||||
setloopTitleText("Entfernung [km]");
|
setloopTitleText("Entfernung [km]");
|
||||||
|
|
||||||
const latestTdrDistanceMeters =
|
|
||||||
Array.isArray(tdmChartData?.[slotIndex]) &&
|
|
||||||
tdmChartData[slotIndex].length > 0 &&
|
|
||||||
typeof tdmChartData[slotIndex][0].d === "number"
|
|
||||||
? tdmChartData[slotIndex][0].d
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const latestTdrDistance = Number(
|
|
||||||
(latestTdrDistanceMeters / 1000).toFixed(3)
|
|
||||||
);
|
|
||||||
setLoopDisplayValue(latestTdrDistance);
|
|
||||||
setShowTdrModal(true);
|
setShowTdrModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,30 +260,16 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
return () => window.removeEventListener("resize", measure);
|
return () => window.removeEventListener("resize", measure);
|
||||||
}, [moduleName48, scrollFeatureEnabled]);
|
}, [moduleName48, scrollFeatureEnabled]);
|
||||||
//---------------------------------
|
//---------------------------------
|
||||||
const tdmChartData = useSelector(
|
// TDR Distanz wird im Display nicht angezeigt – Daten für Modal werden separat geladen
|
||||||
(state: RootState) => state.tdmChartSlice.data
|
|
||||||
);
|
|
||||||
const latestTdrDistanceMeters =
|
|
||||||
Array.isArray(tdmChartData?.[slotIndex]) &&
|
|
||||||
tdmChartData[slotIndex].length > 0 &&
|
|
||||||
typeof tdmChartData[slotIndex][0].d === "number"
|
|
||||||
? tdmChartData[slotIndex][0].d
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const latestTdrDistance = Number((latestTdrDistanceMeters / 1000).toFixed(3));
|
|
||||||
//setLoopDisplayValue(latestTdrDistance);
|
|
||||||
|
|
||||||
//---------------------------------
|
//---------------------------------
|
||||||
|
|
||||||
const loopValue =
|
const rslValue =
|
||||||
activeButton === "TDR"
|
typeof schleifenwiderstand === "number"
|
||||||
? latestTdrDistance
|
|
||||||
: typeof schleifenwiderstand === "number"
|
|
||||||
? schleifenwiderstand
|
? schleifenwiderstand
|
||||||
: Number(schleifenwiderstand);
|
: Number(schleifenwiderstand);
|
||||||
|
|
||||||
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
|
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
|
||||||
loopValue,
|
rslValue,
|
||||||
activeButton
|
activeButton
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -409,7 +383,7 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
|||||||
.toFixed(2)
|
.toFixed(2)
|
||||||
.replace(".", ",")} MOhm`}
|
.replace(".", ",")} MOhm`}
|
||||||
</span>
|
</span>
|
||||||
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
|
{/* 3. Zeile: Schleifenwert (RSL) immer anzeigen, unabhängig von aktivem Button */}
|
||||||
<span
|
<span
|
||||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||||
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
||||||
|
|||||||
@@ -12,22 +12,48 @@ export default function SlotActivityOverlay({
|
|||||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||||
const alignmentStartedAt = useAppSelector(
|
const comparisonStartedAt = useAppSelector(
|
||||||
(s) => s.deviceEvents.alignmentStartedAt
|
(s) => s.deviceEvents.comparisonStartedAt
|
||||||
|
);
|
||||||
|
const loopStartedAtBySlot = useAppSelector(
|
||||||
|
(s) => s.deviceEvents.loopStartedAtBySlot
|
||||||
|
);
|
||||||
|
const tdrStartedAtBySlot = useAppSelector(
|
||||||
|
(s) => s.deviceEvents.tdrStartedAtBySlot
|
||||||
|
);
|
||||||
|
const comparisonStartedAtBySlot = useAppSelector(
|
||||||
|
(s) => s.deviceEvents.comparisonStartedAtBySlot
|
||||||
);
|
);
|
||||||
|
|
||||||
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
|
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
|
||||||
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
|
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
|
||||||
const alignActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
const comparisonActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
||||||
|
|
||||||
|
// Persist whenever arrays change
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
"deviceEventsTimingsV1",
|
||||||
|
JSON.stringify({
|
||||||
|
loop: loopStartedAtBySlot,
|
||||||
|
tdr: tdrStartedAtBySlot,
|
||||||
|
compare: comparisonStartedAtBySlot,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("Failed to persist timings", e);
|
||||||
|
}
|
||||||
|
}, [loopStartedAtBySlot, tdrStartedAtBySlot, comparisonStartedAtBySlot]);
|
||||||
|
|
||||||
// Progress ticker
|
// Progress ticker
|
||||||
const [now, setNow] = useState<number>(Date.now());
|
const [now, setNow] = useState<number>(Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const any = loopActive || tdrActive || alignActive;
|
const any = loopActive || tdrActive || comparisonActive;
|
||||||
if (!any) return;
|
if (!any) return;
|
||||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [loopActive, tdrActive, alignActive]);
|
}, [loopActive, tdrActive, comparisonActive]);
|
||||||
|
|
||||||
const clamp = (v: number, min = 0, max = 1) =>
|
const clamp = (v: number, min = 0, max = 1) =>
|
||||||
Math.max(min, Math.min(max, v));
|
Math.max(min, Math.min(max, v));
|
||||||
@@ -40,10 +66,10 @@ export default function SlotActivityOverlay({
|
|||||||
|
|
||||||
// Durations
|
// Durations
|
||||||
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
||||||
const TDR_MS = 30 * 1000; // ~30 s
|
const TDR_MS = 110 * 1000; // ~2 min laut die Eigaben
|
||||||
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
||||||
|
|
||||||
if (!loopActive && !tdrActive && !alignActive) return null;
|
if (!loopActive && !tdrActive && !comparisonActive) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
||||||
@@ -56,7 +82,8 @@ export default function SlotActivityOverlay({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
|
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(loopStartedAt, LOOP_MS);
|
const started = loopStartedAtBySlot[slotIndex] ?? loopStartedAt;
|
||||||
|
const { pct } = compute(started, LOOP_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
@@ -77,7 +104,8 @@ export default function SlotActivityOverlay({
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
|
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(tdrStartedAt, TDR_MS);
|
const started = tdrStartedAtBySlot[slotIndex] ?? tdrStartedAt;
|
||||||
|
const { pct } = compute(started, TDR_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
@@ -94,11 +122,13 @@ export default function SlotActivityOverlay({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{alignActive && (
|
{comparisonActive && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
|
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
const started =
|
||||||
|
comparisonStartedAtBySlot[slotIndex] ?? comparisonStartedAt;
|
||||||
|
const { pct } = compute(started, ALIGN_MS);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
|
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Keeps and updates the loop (RSL) display value only when "Schleife" active.
|
||||||
|
// For ISO or TDR views we do not overwrite the displayed RSL value.
|
||||||
const useLoopDisplay = (
|
const useLoopDisplay = (
|
||||||
schleifenwiderstand: number,
|
rslValue: number,
|
||||||
activeButton: "Schleife" | "TDR" | "ISO"
|
activeButton: "Schleife" | "TDR" | "ISO"
|
||||||
) => {
|
) => {
|
||||||
const [loopDisplayValue, setLoopDisplayValue] =
|
const [loopDisplayValue, setLoopDisplayValue] = useState<number>(rslValue);
|
||||||
useState<number>(schleifenwiderstand);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeButton === "Schleife") {
|
if (activeButton === "Schleife") {
|
||||||
setLoopDisplayValue(schleifenwiderstand);
|
setLoopDisplayValue(rslValue);
|
||||||
}
|
}
|
||||||
// For ISO and TDR, the value is set manually via setLoopDisplayValue
|
}, [rslValue, activeButton]);
|
||||||
}, [schleifenwiderstand, activeButton]);
|
|
||||||
|
|
||||||
return { loopDisplayValue, setLoopDisplayValue };
|
return { loopDisplayValue, setLoopDisplayValue };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,14 +69,20 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-2 flex justify-between items-center rounded-t-md">
|
<div className="p-2 flex justify-between items-center rounded-t-md bg-surface-alt border-b border-base">
|
||||||
<h2 className="text-base font-bold">Einstellungen KÜ {slot + 1}</h2>
|
<h2 className="text-base font-bold text-fg">
|
||||||
<button onClick={onClose} className="text-2xl hover:text-gray-200">
|
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>
|
<i className="bi bi-x-circle-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-start bg-gray-100 space-x-2 p-2">
|
<div className="flex justify-start bg-surface-alt space-x-2 p-2 border-b border-base">
|
||||||
{[
|
{[
|
||||||
{ label: "Allgemein", key: "kue" as const },
|
{ label: "Allgemein", key: "kue" as const },
|
||||||
{ label: "TDR ", key: "tdr" as const },
|
{ label: "TDR ", key: "tdr" as const },
|
||||||
@@ -86,18 +92,17 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
|||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() => setActiveTab(key)}
|
onClick={() => setActiveTab(key)}
|
||||||
className={`px-4 py-1 rounded-t font-bold text-sm ${
|
className={`px-4 py-1 rounded-t font-semibold text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||||
activeTab === key
|
activeTab === key
|
||||||
? "bg-white text-littwin-blue"
|
? "bg-surface text-accent shadow-sm"
|
||||||
: "text-gray-500 hover:text-black"
|
: "text-fg-muted hover:text-fg"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-4 bg-surface rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto text-fg">
|
||||||
<div className="p-4 bg-white rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto">
|
|
||||||
{activeTab === "kue" && (
|
{activeTab === "kue" && (
|
||||||
<KueEinstellung
|
<KueEinstellung
|
||||||
slot={slot}
|
slot={slot}
|
||||||
|
|||||||
@@ -14,22 +14,22 @@ export default function MeldungenTabelle({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto max-h-[80vh]">
|
<div className="overflow-auto max-h-[80vh]">
|
||||||
<table className="min-w-full border">
|
<table className="min-w-full border border-base table-surface text-fg">
|
||||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
<thead className="text-left sticky top-0 z-10 bg-surface-alt/90 backdrop-blur supports-[backdrop-filter]:bg-surface-alt/70">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Prio
|
Prio
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Zeitstempel
|
Zeitstempel
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Quelle
|
Quelle
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Meldung
|
Meldung
|
||||||
</th>
|
</th>
|
||||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -38,15 +38,15 @@ export default function MeldungenTabelle({
|
|||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className="hover:bg-gray-100 dark:hover:bg-gray-700"
|
className="transition-colors hover:bg-surface-alt/60"
|
||||||
>
|
>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
<div
|
<div
|
||||||
className="w-4 h-4 rounded"
|
className="w-4 h-4 rounded"
|
||||||
style={{ backgroundColor: msg.c }}
|
style={{ backgroundColor: msg.c }}
|
||||||
></div>
|
></div>
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{new Date(msg.t).toLocaleString("de-DE", {
|
{new Date(msg.t).toLocaleString("de-DE", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
@@ -56,13 +56,13 @@ export default function MeldungenTabelle({
|
|||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{msg.i}
|
{msg.i}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{msg.m}
|
{msg.m}
|
||||||
</td>
|
</td>
|
||||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
<td className="border border-base p-2 bg-surface text-fg">
|
||||||
{msg.v}
|
{msg.v}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -70,7 +70,7 @@ export default function MeldungenTabelle({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="mt-4 text-center text-gray-500 italic">
|
<div className="mt-4 text-center text-fg-muted italic">
|
||||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ export default function MeldungenView() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
|
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded h-fit"
|
className="btn-primary px-4 py-2 h-fit"
|
||||||
>
|
>
|
||||||
Anzeigen
|
Anzeigen
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
||||||
<div className="relative ml-6 w-64">
|
<div className="relative ml-6 w-64">
|
||||||
<Listbox.Button className="bg-white text-gray-900 w-full border px-4 py-2 rounded text-left flex justify-between items-center dark:bg-gray-900 dark:text-gray-100">
|
<Listbox.Button className="bg-[var(--color-surface)] text-[var(--color-fg)] w-full border border-base px-4 py-2 rounded text-left flex justify-between items-center">
|
||||||
<span>{sourceFilter}</span>
|
<span>{sourceFilter}</span>
|
||||||
<svg
|
<svg
|
||||||
className="w-5 h-5 text-gray-400"
|
className="w-5 h-5 text-gray-400"
|
||||||
@@ -83,19 +83,19 @@ export default function MeldungenView() {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
<Listbox.Options className="bg-white absolute z-50 mt-1 w-full border rounded dark:bg-gray-900">
|
<Listbox.Options className="bg-[var(--color-surface)] absolute z-50 mt-1 w-full border border-base rounded shadow-sm">
|
||||||
{sources.map((src) => (
|
{sources.map((src) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={src}
|
key={src}
|
||||||
value={src}
|
value={src}
|
||||||
className={({ selected, active, disabled }) =>
|
className={({ selected, active, disabled }) =>
|
||||||
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
|
`px-4 py-2 cursor-pointer text-[var(--color-fg)] transition-colors ${
|
||||||
selected
|
disabled
|
||||||
? "bg-littwin-blue text-white"
|
? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
|
||||||
|
: selected
|
||||||
|
? "bg-accent text-white"
|
||||||
: active
|
: active
|
||||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
? "bg-[var(--color-surface-alt)]"
|
||||||
: disabled
|
|
||||||
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
|
||||||
: ""
|
: ""
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
|
|||||||
const DatabaseSettings: React.FC = () => {
|
const DatabaseSettings: React.FC = () => {
|
||||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||||
return (
|
return (
|
||||||
<div className="p-6 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto rounded shadow text-gray-900 dark:text-gray-100">
|
<div className="p-6 bg-[var(--color-surface-alt)] max-w-5xl mr-auto rounded shadow text-[var(--color-fg)]">
|
||||||
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
|
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearMessages}
|
onClick={handleClearMessages}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Meldungen löschen
|
Meldungen löschen
|
||||||
</button>
|
</button>
|
||||||
@@ -25,7 +25,7 @@ const DatabaseSettings: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearLogger}
|
onClick={handleClearLogger}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Messwerte Logger löschen
|
Messwerte Logger löschen
|
||||||
</button>
|
</button>
|
||||||
@@ -41,7 +41,7 @@ const DatabaseSettings: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearDatabase}
|
onClick={handleClearDatabase}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Datenbank vollständig leeren
|
Datenbank vollständig leeren
|
||||||
</button>
|
</button>
|
||||||
@@ -49,7 +49,7 @@ const DatabaseSettings: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearConfig}
|
onClick={handleClearConfig}
|
||||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
className="btn-accent px-4 py-2 rounded shadow"
|
||||||
>
|
>
|
||||||
Konfiguration löschen
|
Konfiguration löschen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -63,8 +63,11 @@ const GeneralSettings: React.FC = () => {
|
|||||||
setMac1(systemSettings.mac1 || "");
|
setMac1(systemSettings.mac1 || "");
|
||||||
}, [systemSettings]);
|
}, [systemSettings]);
|
||||||
|
|
||||||
|
const inputCls =
|
||||||
|
"border border-base focus:border-accent rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] dark:text-gray-100 ">
|
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] text-[var(--color-fg)]">
|
||||||
<h2 className="text-sm md:text-md font-bold mb-2">
|
<h2 className="text-sm md:text-md font-bold mb-2">
|
||||||
Allgemeine Einstellungen
|
Allgemeine Einstellungen
|
||||||
</h2>
|
</h2>
|
||||||
@@ -74,25 +77,18 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className={inputCls}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MAC Adresse */}
|
{/* MAC Adresse */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs md:text-sm font-medium">
|
<label className="block text-xs md:text-sm font-medium">
|
||||||
MAC Adresse 1:
|
MAC Adresse 1:
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input type="text" className={inputCls} value={mac1} disabled />
|
||||||
type="text"
|
|
||||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
|
||||||
value={mac1}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Systemzeit */}
|
{/* Systemzeit */}
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-xs md:text-sm font-medium mb-1">
|
<label className="block text-xs md:text-sm font-medium mb-1">
|
||||||
@@ -101,13 +97,13 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className={inputCls}
|
||||||
value={systemUhr.replace(/\s*Uhr$/, "")}
|
value={systemUhr.replace(/\s*Uhr$/, "")}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={() => handleSetDateTime()}
|
onClick={() => handleSetDateTime()}
|
||||||
>
|
>
|
||||||
Systemzeit übernehmen
|
Systemzeit übernehmen
|
||||||
@@ -120,7 +116,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className={inputCls}
|
||||||
value={ip}
|
value={ip}
|
||||||
onChange={(e) => setIp(e.target.value)}
|
onChange={(e) => setIp(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -131,7 +127,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className={inputCls}
|
||||||
value={subnet}
|
value={subnet}
|
||||||
onChange={(e) => setSubnet(e.target.value)}
|
onChange={(e) => setSubnet(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -142,7 +138,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className={inputCls}
|
||||||
value={gateway}
|
value={gateway}
|
||||||
onChange={(e) => setGateway(e.target.value)}
|
onChange={(e) => setGateway(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -152,7 +148,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
|
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={() => handleReboot()}
|
onClick={() => handleReboot()}
|
||||||
>
|
>
|
||||||
Neustart CPL
|
Neustart CPL
|
||||||
@@ -160,7 +156,7 @@ const GeneralSettings: React.FC = () => {
|
|||||||
{isAdminLoggedIn && (
|
{isAdminLoggedIn && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const confirmed = window.confirm(
|
const confirmed = window.confirm(
|
||||||
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
|
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||||
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
|
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-3">
|
<div className="grid md:grid-cols-2 gap-3">
|
||||||
@@ -35,7 +35,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
<label className="block text-xs font-medium">NTP Server 1</label>
|
<label className="block text-xs font-medium">NTP Server 1</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntp1}
|
value={ntp1}
|
||||||
onChange={(e) => setNtp1(e.target.value)}
|
onChange={(e) => setNtp1(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -45,7 +45,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
<label className="block text-xs font-medium">NTP Server 2</label>
|
<label className="block text-xs font-medium">NTP Server 2</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntp2}
|
value={ntp2}
|
||||||
onChange={(e) => setNtp2(e.target.value)}
|
onChange={(e) => setNtp2(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -55,7 +55,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
<label className="block text-xs font-medium">NTP Server 3</label>
|
<label className="block text-xs font-medium">NTP Server 3</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntp3}
|
value={ntp3}
|
||||||
onChange={(e) => setNtp3(e.target.value)}
|
onChange={(e) => setNtp3(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -65,7 +65,7 @@ const NTPSettings: React.FC = () => {
|
|||||||
<label className="block text-xs font-medium">Zeitzone</label>
|
<label className="block text-xs font-medium">Zeitzone</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={ntpTimezone}
|
value={ntpTimezone}
|
||||||
onChange={(e) => setNtpTimezone(e.target.value)}
|
onChange={(e) => setNtpTimezone(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100 ">
|
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||||
<div className="flex justify-between items-center mb-3">
|
<div className="flex justify-between items-center mb-3">
|
||||||
<Image
|
<Image
|
||||||
src="/images/OPCUA.jpg"
|
src="/images/OPCUA.jpg"
|
||||||
@@ -44,9 +44,11 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<label className="mr-3 font-medium text-sm">Server Status:</label>
|
<label className="mr-3 font-medium text-sm">Server Status:</label>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(toggleOpcUaServer())}
|
onClick={() => dispatch(toggleOpcUaServer())}
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
className={`px-3 py-1 rounded text-sm font-medium transition-colors text-white ${
|
||||||
opcuaSettings.isEnabled ? "bg-littwin-blue" : "bg-gray-300"
|
opcuaSettings.isEnabled
|
||||||
} text-white`}
|
? "bg-accent hover:brightness-110"
|
||||||
|
: "bg-[var(--color-muted)] hover:bg-[var(--color-fg)]/20"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
|
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
|
||||||
</button>
|
</button>
|
||||||
@@ -62,7 +64,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<select
|
<select
|
||||||
value={opcuaSettings.encryption}
|
value={opcuaSettings.encryption}
|
||||||
onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
|
onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
|
||||||
className="w-full p-1 border border-gray-300 rounded-md text-sm"
|
className="w-full p-1 border border-base rounded-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)]"
|
||||||
>
|
>
|
||||||
<option value="None">Keine</option>
|
<option value="None">Keine</option>
|
||||||
<option value="Basic256">Basic256</option>
|
<option value="Basic256">Basic256</option>
|
||||||
@@ -74,7 +76,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
{/* ✅ OPCUA Zustand */}
|
{/* ✅ OPCUA Zustand */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
|
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
|
||||||
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
|
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
|
||||||
{opcuaSettings.opcUaZustand}
|
{opcuaSettings.opcUaZustand}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +87,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="flex-grow p-1 border border-gray-300 dark:border-gray-700 rounded-l-md text-sm !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="flex-grow p-1 border border-base rounded-l-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={nodesetName}
|
value={nodesetName}
|
||||||
onChange={(e) => setNodesetName(e.target.value)}
|
onChange={(e) => setNodesetName(e.target.value)}
|
||||||
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
|
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
|
||||||
@@ -106,7 +108,7 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
<label className="block font-medium text-sm mb-1">
|
<label className="block font-medium text-sm mb-1">
|
||||||
Aktuelle OPC-Clients
|
Aktuelle OPC-Clients
|
||||||
</label>
|
</label>
|
||||||
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
|
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
|
||||||
{opcUaActiveClientCount}
|
{opcUaActiveClientCount}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,12 +122,12 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
{opcuaSettings.users.map((user) => (
|
{opcuaSettings.users.map((user) => (
|
||||||
<li
|
<li
|
||||||
key={user.id}
|
key={user.id}
|
||||||
className="p-1 bg-white shadow-sm rounded-md flex justify-between items-center text-sm"
|
className="p-1 bg-[var(--color-surface)] border border-base rounded-md flex justify-between items-center text-sm text-[var(--color-fg)]"
|
||||||
>
|
>
|
||||||
<span className="font-medium">{user.username}</span>
|
<span className="font-medium">{user.username}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch(removeOpcUaUser(user.id))}
|
onClick={() => dispatch(removeOpcUaUser(user.id))}
|
||||||
className="text-red-500"
|
className="text-danger hover:underline"
|
||||||
>
|
>
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
@@ -140,18 +142,18 @@ export default function OPCUAInterfaceSettings() {
|
|||||||
placeholder="Benutzername"
|
placeholder="Benutzername"
|
||||||
value={newUsername}
|
value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
onChange={(e) => setNewUsername(e.target.value)}
|
||||||
className="p-1 border rounded flex-grow text-sm"
|
className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
className="p-1 border rounded flex-grow text-sm"
|
className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddUser}
|
onClick={handleAddUser}
|
||||||
className="bg-littwin-blue text-white p-1 rounded text-sm"
|
className="btn-primary p-1 rounded text-sm"
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
() => {
|
() => {
|
||||||
setLoginSuccess(true);
|
setLoginSuccess(true);
|
||||||
setError("");
|
setError("");
|
||||||
|
// Speichere die System-Uhrzeit (Login-Zeitpunkt) im localStorage
|
||||||
|
try {
|
||||||
|
localStorage.setItem("adminLoginTime", new Date().toISOString());
|
||||||
|
} catch {
|
||||||
|
// Ignoriere Speicherfehler (z. B. in Private Mode)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(errorMsg) => {
|
(errorMsg) => {
|
||||||
setLoginSuccess(false);
|
setLoginSuccess(false);
|
||||||
@@ -38,7 +44,7 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
|
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||||
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
|
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
|
||||||
|
|
||||||
{/* Admin Login/Logout */}
|
{/* Admin Login/Logout */}
|
||||||
@@ -46,8 +52,15 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
{isAdminLoggedIn ? (
|
{isAdminLoggedIn ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={logoutAdmin}
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem("adminLoginTime");
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
logoutAdmin();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Admin abmelden
|
Admin abmelden
|
||||||
</button>
|
</button>
|
||||||
@@ -57,7 +70,7 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Benutzername"
|
placeholder="Benutzername"
|
||||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -65,14 +78,14 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
>
|
>
|
||||||
Admin anmelden
|
Admin anmelden
|
||||||
@@ -83,9 +96,9 @@ const UserManagementSettings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loginSuccess && (
|
{loginSuccess && (
|
||||||
<p className="text-green-600 text-xs mt-2">Login erfolgreich!</p>
|
<p className="text-success text-xs mt-2">Login erfolgreich!</p>
|
||||||
)}
|
)}
|
||||||
{error && <p className="text-red-500 text-xs mt-2">{error}</p>}
|
{error && <p className="text-danger text-xs mt-2">{error}</p>}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
// Benutzerverwaltungstabelle (kommt später)
|
// Benutzerverwaltungstabelle (kommt später)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
|
||||||
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-6 rounded shadow-md text-center w-80">
|
<div className="p-6 rounded shadow-sm text-center w-80 bg-[var(--color-surface)] dark:bg-[var(--color-surface)] text-[var(--color-fg)] border border-[var(--color-border)]">
|
||||||
{/*
|
{/*
|
||||||
<h2 className="text-lg font-bold mb-4">
|
<h2 className="text-lg font-bold mb-4">
|
||||||
Firmwareupdate
|
Firmwareupdate
|
||||||
@@ -26,9 +26,9 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
|||||||
</h2>
|
</h2>
|
||||||
Bitte Fenster nicht schließen
|
Bitte Fenster nicht schließen
|
||||||
<h2></h2>
|
<h2></h2>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-4">
|
<div className="w-full h-4 rounded-full bg-[var(--color-surface-alt)]/80 dark:bg-[var(--color-surface-alt)]/30 border border-[var(--color-border)] overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-littwin-blue h-4 rounded-full transition-all duration-100"
|
className="h-full rounded-full transition-all duration-200 bg-[var(--color-accent)]"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ export const DetailModal = ({
|
|||||||
<div className="absolute top-0 right-0 flex gap-3">
|
<div className="absolute top-0 right-0 flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={toggleFullScreen}
|
onClick={toggleFullScreen}
|
||||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] transition"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className={
|
className={
|
||||||
@@ -467,7 +467,7 @@ export const DetailModal = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-danger)] transition"
|
||||||
>
|
>
|
||||||
<i className="bi bi-x-circle-fill"></i>
|
<i className="bi bi-x-circle-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -481,7 +481,7 @@ export const DetailModal = ({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
|
<div className="h-[85%] rounded shadow border p-2 bg-[var(--color-surface)] border-[var(--color-border)]">
|
||||||
<Line ref={chartRef} data={chartData} options={chartOptions} />
|
<Line ref={chartRef} data={chartData} options={chartOptions} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
className="p-4 border rounded shadow bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"
|
className="p-4 border rounded shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-[var(--color-border)] text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||||
>
|
>
|
||||||
<h2 className="font-semibold">{key}</h2>
|
<h2 className="font-semibold">{key}</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ const SystemPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-white dark:bg-gray-900">
|
<div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||||
<h1 className="text-xl font-bold mb-4">
|
<h1 className="text-xl font-bold mb-4 tracking-wide">
|
||||||
System Spannungen & Temperaturen
|
System Spannungen & Temperaturen
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ const SystemPage = () => {
|
|||||||
<div className="flex justify-center items-center h-[400px]">
|
<div className="flex justify-center items-center h-[400px]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<ClipLoader size={50} color="#3B82F6" />
|
<ClipLoader size={50} color="#3B82F6" />
|
||||||
<p className="mt-4 text-gray-500">
|
<p className="mt-4 text-[var(--color-fg-muted)]">
|
||||||
Lade Systemdaten … bitte warten
|
Lade Systemdaten … bitte warten
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="bg-white dark:bg-gray-900 h-full">
|
<aside className="h-full bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-r border-[var(--color-border)]">
|
||||||
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
|
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<div key={item.name}>
|
<div key={item.name}>
|
||||||
{item.disabled ? (
|
{item.disabled ? (
|
||||||
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 dark:text-gray-600 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-[var(--color-fg-muted)] opacity-60 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
||||||
{item.name}
|
{item.name}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -49,11 +49,13 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
|||||||
href={formatPath(item.path)}
|
href={formatPath(item.path)}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
onClick={() => setActiveLink(item.path)}
|
onClick={() => setActiveLink(item.path)}
|
||||||
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${
|
className={`block px-4 py-2 mb-4 font-semibold whitespace-nowrap transition duration-200 rounded-r-full pr-6 relative text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg
|
||||||
|
${
|
||||||
activeLink.startsWith(item.path)
|
activeLink.startsWith(item.path)
|
||||||
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white"
|
? "bg-[var(--color-accent)] text-white shadow-sm xl:mr-4 xl:w-full"
|
||||||
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
|
: "text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/80 dark:hover:bg-[var(--color-surface-alt)]/40"
|
||||||
}`}
|
}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"win_da_state": [
|
"win_da_state": [
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
0,
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
"win_da_bezeichnung": [
|
"win_da_bezeichnung": [
|
||||||
|
|||||||
@@ -269,7 +269,12 @@ var tdrMeasurementEvent = [
|
|||||||
0, 0, 0, 0, 0, 0,
|
0, 0, 0, 0, 0, 0,
|
||||||
];
|
];
|
||||||
//Event Abgleich
|
//Event Abgleich
|
||||||
var alignmentEvent = [
|
var comparisonEvent = [
|
||||||
|
// renamed from alignmentEvent
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0, 0, 0, 0, 0, 0,
|
0, 0, 0, 0, 0, 0,
|
||||||
];
|
];
|
||||||
|
// expose for browser simulation
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.comparisonEvent = comparisonEvent;
|
||||||
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "cpl-v4",
|
"name": "cpl-v4",
|
||||||
"version": "1.6.855",
|
"version": "1.6.879",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cpl-v4",
|
"name": "cpl-v4",
|
||||||
"version": "1.6.855",
|
"version": "1.6.879",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.1.0",
|
"@fontsource/roboto": "^5.1.0",
|
||||||
"@headlessui/react": "^2.2.4",
|
"@headlessui/react": "^2.2.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cpl-v4",
|
"name": "cpl-v4",
|
||||||
"version": "1.6.855",
|
"version": "1.6.879",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"dev": "next dev -p 3000",
|
||||||
|
|||||||
@@ -148,11 +148,11 @@ function AppContent({
|
|||||||
}, [pathname, dispatch]);
|
}, [pathname, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen overflow-hidden bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
<div className="flex flex-col h-screen overflow-hidden bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="flex flex-grow w-full">
|
<div className="flex flex-grow w-full">
|
||||||
<Navigation className="w-56" />
|
<Navigation className="w-56" />
|
||||||
<main className="w-full flex-grow bg-white dark:bg-gray-900">
|
<main className="w-full flex-grow bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-l border-[var(--color-border)]">
|
||||||
{sessionExpired && (
|
{sessionExpired && (
|
||||||
<div className="bg-red-500 text-white p-4 text-center">
|
<div className="bg-red-500 text-white p-4 text-center">
|
||||||
❌ Ihre Sitzung ist abgelaufen oder die Verbindung ist
|
❌ Ihre Sitzung ist abgelaufen oder die Verbindung ist
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ export default function Document() {
|
|||||||
<link rel="icon" href="/favicon.png" type="image/png" />
|
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
|
{/* Theme init (executed before React hydration) */}
|
||||||
|
<script
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `(() => { try { const ls = localStorage.getItem('theme'); const mql = window.matchMedia('(prefers-color-scheme: dark)'); const wantDark = ls === 'dark' || (!ls && mql.matches); if (wantDark) document.documentElement.classList.add('dark'); } catch(e) {} })();`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Main /> {/* Hier wird der Seiteninhalt eingebettet */}
|
<Main /> {/* Hier wird der Seiteninhalt eingebettet */}
|
||||||
<NextScript /> {/* Fügt Next.js-Skripte für die Seite hinzu */}
|
<NextScript /> {/* Fügt Next.js-Skripte für die Seite hinzu */}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ var win_tdrLocation=[<%=KTF80%>,<%=KTF81%>,<%=KTF82%>,<%=KTF83%>];//Entfernung B
|
|||||||
var loopMeasurementEvent=[<%=KSX80%>, <%=KSX81%>, <%=KSX82%>, <%=KSX83%>];
|
var loopMeasurementEvent=[<%=KSX80%>, <%=KSX81%>, <%=KSX82%>, <%=KSX83%>];
|
||||||
//Event TDR-Messung
|
//Event TDR-Messung
|
||||||
var tdrMeasurementEvent=[<%=KSY80%>, <%=KSY81%>, <%=KSY82%>, <%=KSY83%>];
|
var tdrMeasurementEvent=[<%=KSY80%>, <%=KSY81%>, <%=KSY82%>, <%=KSY83%>];
|
||||||
//Event Abgleich
|
//Event Comparison (ehem. Abgleich)
|
||||||
var alignmentEvent=[<%=KSZ80%>, <%=KSZ81%>, <%=KSZ82%>, <%=KSZ83%>];
|
var comparisonEvent=[<%=KSZ80%>, <%=KSZ81%>, <%=KSZ82%>, <%=KSZ83%>];
|
||||||
|
|
||||||
//---------------------------------------------------
|
//---------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||||||
export interface DeviceEventsState {
|
export interface DeviceEventsState {
|
||||||
ksx: number[]; // 32 Slots: Schleifenmessung aktiv
|
ksx: number[]; // 32 Slots: Schleifenmessung aktiv
|
||||||
ksy: number[]; // 32 Slots: TDR-Messung aktiv
|
ksy: number[]; // 32 Slots: TDR-Messung aktiv
|
||||||
ksz: number[]; // 32 Slots: Abgleich aktiv
|
ksz: number[]; // 32 Slots: Comparison (ehem. Abgleich) aktiv
|
||||||
anyLoopActive: boolean;
|
anyLoopActive: boolean;
|
||||||
anyTdrActive: boolean;
|
anyTdrActive: boolean;
|
||||||
anyAlignmentActive: boolean;
|
anyComparisonActive: boolean; // renamed from anyAlignmentActive
|
||||||
loopStartedAt: number | null; // unix ms timestamp when KSX became active
|
loopStartedAt: number | null; // unix ms timestamp when KSX became active
|
||||||
tdrStartedAt: number | null; // unix ms timestamp when KSY became active
|
tdrStartedAt: number | null; // unix ms timestamp when KSY became active
|
||||||
alignmentStartedAt: number | null; // unix ms timestamp when KSZ became active
|
comparisonStartedAt: number | null; // renamed from alignmentStartedAt
|
||||||
|
// per-slot start timestamps (persistable)
|
||||||
|
loopStartedAtBySlot: (number | null)[];
|
||||||
|
tdrStartedAtBySlot: (number | null)[];
|
||||||
|
comparisonStartedAtBySlot: (number | null)[]; // renamed from alignmentStartedAtBySlot
|
||||||
}
|
}
|
||||||
|
|
||||||
const ZERO32 = Array.from({ length: 32 }, () => 0);
|
const ZERO32 = Array.from({ length: 32 }, () => 0);
|
||||||
@@ -20,10 +24,13 @@ const initialState: DeviceEventsState = {
|
|||||||
ksz: ZERO32.slice(),
|
ksz: ZERO32.slice(),
|
||||||
anyLoopActive: false,
|
anyLoopActive: false,
|
||||||
anyTdrActive: false,
|
anyTdrActive: false,
|
||||||
anyAlignmentActive: false,
|
anyComparisonActive: false,
|
||||||
loopStartedAt: null,
|
loopStartedAt: null,
|
||||||
tdrStartedAt: null,
|
tdrStartedAt: null,
|
||||||
alignmentStartedAt: null,
|
comparisonStartedAt: null,
|
||||||
|
loopStartedAtBySlot: Array.from({ length: 32 }, () => null),
|
||||||
|
tdrStartedAtBySlot: Array.from({ length: 32 }, () => null),
|
||||||
|
comparisonStartedAtBySlot: Array.from({ length: 32 }, () => null),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deviceEventsSlice = createSlice({
|
export const deviceEventsSlice = createSlice({
|
||||||
@@ -36,7 +43,10 @@ export const deviceEventsSlice = createSlice({
|
|||||||
) {
|
) {
|
||||||
const prevLoop = state.anyLoopActive;
|
const prevLoop = state.anyLoopActive;
|
||||||
const prevTdr = state.anyTdrActive;
|
const prevTdr = state.anyTdrActive;
|
||||||
const prevAlign = state.anyAlignmentActive;
|
const prevCompare = state.anyComparisonActive;
|
||||||
|
const prevKsx = state.ksx.slice();
|
||||||
|
const prevKsy = state.ksy.slice();
|
||||||
|
const prevKsz = state.ksz.slice();
|
||||||
const to32 = (arr?: number[]) => {
|
const to32 = (arr?: number[]) => {
|
||||||
if (!Array.isArray(arr)) return ZERO32.slice();
|
if (!Array.isArray(arr)) return ZERO32.slice();
|
||||||
const a = arr
|
const a = arr
|
||||||
@@ -50,17 +60,43 @@ export const deviceEventsSlice = createSlice({
|
|||||||
state.ksz = to32(action.payload.ksz);
|
state.ksz = to32(action.payload.ksz);
|
||||||
state.anyLoopActive = state.ksx.some((v) => v === 1);
|
state.anyLoopActive = state.ksx.some((v) => v === 1);
|
||||||
state.anyTdrActive = state.ksy.some((v) => v === 1);
|
state.anyTdrActive = state.ksy.some((v) => v === 1);
|
||||||
state.anyAlignmentActive = state.ksz.some((v) => v === 1);
|
state.anyComparisonActive = state.ksz.some((v) => v === 1);
|
||||||
|
|
||||||
// Transition detection to set/reset startedAt timestamps
|
// Global transition detection
|
||||||
if (!prevLoop && state.anyLoopActive) state.loopStartedAt = Date.now();
|
if (!prevLoop && state.anyLoopActive) state.loopStartedAt = Date.now();
|
||||||
if (prevLoop && !state.anyLoopActive) state.loopStartedAt = null;
|
if (prevLoop && !state.anyLoopActive) state.loopStartedAt = null;
|
||||||
if (!prevTdr && state.anyTdrActive) state.tdrStartedAt = Date.now();
|
if (!prevTdr && state.anyTdrActive) state.tdrStartedAt = Date.now();
|
||||||
if (prevTdr && !state.anyTdrActive) state.tdrStartedAt = null;
|
if (prevTdr && !state.anyTdrActive) state.tdrStartedAt = null;
|
||||||
if (!prevAlign && state.anyAlignmentActive)
|
if (!prevCompare && state.anyComparisonActive)
|
||||||
state.alignmentStartedAt = Date.now();
|
state.comparisonStartedAt = Date.now();
|
||||||
if (prevAlign && !state.anyAlignmentActive)
|
if (prevCompare && !state.anyComparisonActive)
|
||||||
state.alignmentStartedAt = null;
|
state.comparisonStartedAt = null;
|
||||||
|
|
||||||
|
// Per-slot transition detection
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
if (prevKsx[i] === 0 && state.ksx[i] === 1) {
|
||||||
|
// Only set if no existing (hydrated) timestamp
|
||||||
|
if (!state.loopStartedAtBySlot[i]) {
|
||||||
|
state.loopStartedAtBySlot[i] = Date.now();
|
||||||
|
}
|
||||||
|
} else if (prevKsx[i] === 1 && state.ksx[i] === 0) {
|
||||||
|
state.loopStartedAtBySlot[i] = null;
|
||||||
|
}
|
||||||
|
if (prevKsy[i] === 0 && state.ksy[i] === 1) {
|
||||||
|
if (!state.tdrStartedAtBySlot[i]) {
|
||||||
|
state.tdrStartedAtBySlot[i] = Date.now();
|
||||||
|
}
|
||||||
|
} else if (prevKsy[i] === 1 && state.ksy[i] === 0) {
|
||||||
|
state.tdrStartedAtBySlot[i] = null;
|
||||||
|
}
|
||||||
|
if (prevKsz[i] === 0 && state.ksz[i] === 1) {
|
||||||
|
if (!state.comparisonStartedAtBySlot[i]) {
|
||||||
|
state.comparisonStartedAtBySlot[i] = Date.now();
|
||||||
|
}
|
||||||
|
} else if (prevKsz[i] === 1 && state.ksz[i] === 0) {
|
||||||
|
state.comparisonStartedAtBySlot[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetEvents(state) {
|
resetEvents(state) {
|
||||||
state.ksx = ZERO32.slice();
|
state.ksx = ZERO32.slice();
|
||||||
@@ -68,13 +104,39 @@ export const deviceEventsSlice = createSlice({
|
|||||||
state.ksz = ZERO32.slice();
|
state.ksz = ZERO32.slice();
|
||||||
state.anyLoopActive = false;
|
state.anyLoopActive = false;
|
||||||
state.anyTdrActive = false;
|
state.anyTdrActive = false;
|
||||||
state.anyAlignmentActive = false;
|
state.anyComparisonActive = false;
|
||||||
state.loopStartedAt = null;
|
state.loopStartedAt = null;
|
||||||
state.tdrStartedAt = null;
|
state.tdrStartedAt = null;
|
||||||
state.alignmentStartedAt = null;
|
state.comparisonStartedAt = null;
|
||||||
|
state.loopStartedAtBySlot = Array.from({ length: 32 }, () => null);
|
||||||
|
state.tdrStartedAtBySlot = Array.from({ length: 32 }, () => null);
|
||||||
|
state.comparisonStartedAtBySlot = Array.from({ length: 32 }, () => null);
|
||||||
|
},
|
||||||
|
initPersistedTimings(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
loop?: (number | null)[];
|
||||||
|
tdr?: (number | null)[];
|
||||||
|
compare?: (number | null)[]; // renamed key
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const normalize = (arr?: (number | null)[]) => {
|
||||||
|
const out: (number | null)[] = Array.isArray(arr)
|
||||||
|
? arr.slice(0, 32).map((v) => (typeof v === "number" ? v : null))
|
||||||
|
: [];
|
||||||
|
while (out.length < 32) out.push(null);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
if (action.payload.loop)
|
||||||
|
state.loopStartedAtBySlot = normalize(action.payload.loop);
|
||||||
|
if (action.payload.tdr)
|
||||||
|
state.tdrStartedAtBySlot = normalize(action.payload.tdr);
|
||||||
|
if (action.payload.compare)
|
||||||
|
state.comparisonStartedAtBySlot = normalize(action.payload.compare);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setEvents, resetEvents } = deviceEventsSlice.actions;
|
export const { setEvents, resetEvents, initPersistedTimings } =
|
||||||
|
deviceEventsSlice.actions;
|
||||||
export default deviceEventsSlice.reducer;
|
export default deviceEventsSlice.reducer;
|
||||||
|
|||||||
@@ -3,45 +3,255 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Light theme tokens */
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #f1f5f9; /* slate-100 */
|
||||||
--foreground: #171717;
|
--foreground: #0f172a; /* slate-900 */
|
||||||
--littwin-blue: #00aeef;
|
--littwin-blue: #00aeef;
|
||||||
|
--color-background: #f1f5f9;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-alt: #f8fafc; /* slate-50 */
|
||||||
|
--color-border: #e2e8f0; /* slate-200 */
|
||||||
|
--color-fg: #0f172a; /* slate-900 */
|
||||||
|
--color-fg-muted: #475569; /* slate-600 */
|
||||||
|
/* Alias for convenience (used in components) */
|
||||||
|
--color-muted: var(--color-fg-muted);
|
||||||
|
--color-accent: #00aeef;
|
||||||
|
--color-accent-soft: #e0f7ff;
|
||||||
|
--color-danger: #dc2626; /* red-600 */
|
||||||
|
--color-warning: #f59e0b; /* amber-500 */
|
||||||
|
--color-success: #16a34a; /* green-600 */
|
||||||
|
--color-info: #0ea5e9; /* sky-500 */
|
||||||
|
--color-ring: #38bdf8; /* sky-400 */
|
||||||
|
--color-input-bg: #ffffff;
|
||||||
|
--color-input-border: #cbd5e1; /* slate-300 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* Dark theme overrides (activated via .dark class on <html>) */
|
||||||
:root {
|
.dark {
|
||||||
--background: #0a0a0a;
|
--background: #0f1115;
|
||||||
--foreground: #ededed;
|
--foreground: #e2e8f0;
|
||||||
}
|
--color-background: #0f1115; /* near slate-950 */
|
||||||
|
--color-surface: #1c232d; /* custom dark surface */
|
||||||
|
--color-surface-alt: #243140; /* slightly elevated */
|
||||||
|
--color-border: #334155; /* slate-600 */
|
||||||
|
--color-fg: #e2e8f0; /* slate-200 */
|
||||||
|
--color-fg-muted: #94a3b8; /* slate-400 */
|
||||||
|
--color-muted: var(--color-fg-muted);
|
||||||
|
--color-accent: #00aeef;
|
||||||
|
--color-accent-soft: #06394d; /* soft accent background */
|
||||||
|
--color-danger: #ef4444; /* red-500 */
|
||||||
|
--color-warning: #fbbf24; /* amber-400 */
|
||||||
|
--color-success: #22c55e; /* green-500 */
|
||||||
|
--color-info: #38bdf8; /* sky-400 */
|
||||||
|
--color-ring: #0ea5e9;
|
||||||
|
--color-input-bg: #1c232d;
|
||||||
|
--color-input-border: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
color: var(--color-fg);
|
||||||
background: var(--background);
|
background: var(--color-background);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
accent-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth theme transitions (respect reduced motion) */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
transition: background-color 0.25s ease, color 0.25s ease;
|
||||||
|
}
|
||||||
|
.theme-transition {
|
||||||
|
transition: background-color 0.25s ease, color 0.25s ease,
|
||||||
|
border-color 0.25s ease, box-shadow 0.25s ease;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.text-balance {
|
.text-balance {
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
}
|
/* Semantic shortcut utilities */
|
||||||
|
.bg-background {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
.bg-surface {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
.bg-surface-alt {
|
||||||
|
background-color: var(--color-surface-alt);
|
||||||
|
}
|
||||||
|
.text-fg {
|
||||||
|
color: var(--color-fg);
|
||||||
|
}
|
||||||
|
.text-fg-muted {
|
||||||
|
color: var(--color-fg-muted);
|
||||||
|
}
|
||||||
|
.text-muted {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
.border-base {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
.ring-accent {
|
||||||
|
--tw-ring-color: var(--color-ring);
|
||||||
|
}
|
||||||
|
.bg-accent {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
.bg-accent-soft {
|
||||||
|
background-color: var(--color-accent-soft);
|
||||||
|
}
|
||||||
|
.text-success {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
.text-danger {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* Component abstractions (no @apply to avoid processing issues in global layer) */
|
||||||
input {
|
.card {
|
||||||
background-color: #333 !important; /* Dunkler Hintergrund im Darkmode */
|
background: var(--color-surface);
|
||||||
color: #fff !important; /* Weißer Text im Darkmode */
|
color: var(--color-fg);
|
||||||
border: 1px solid #555; /* Optional: etwas hellerer Rahmen */
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.card-elevated {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-fg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 4px 10px -2px rgba(0, 0, 0, 0.25),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.table-surface {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-fg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-head {
|
||||||
|
background: var(--color-surface-alt);
|
||||||
|
color: var(--color-fg);
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
.table-row-hover:hover {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-surface-alt) 85%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.btn-accent {
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-accent:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
.btn-accent:focus {
|
||||||
|
outline: 2px solid var(--color-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
/* Button Variants */
|
||||||
|
.btn-primary {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
transition: background-color 0.2s ease, filter 0.2s ease;
|
||||||
|
}
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
.btn-primary:focus {
|
||||||
|
outline: 2px solid var(--color-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.btn-muted {
|
||||||
|
background: var(--color-muted);
|
||||||
|
color: var(--color-fg);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.btn-muted:hover {
|
||||||
|
background: var(--color-fg);
|
||||||
|
color: var(--color-background);
|
||||||
|
}
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-fg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: var(--color-surface-alt);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--color-danger);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
.input-base {
|
||||||
|
background: var(--color-input-bg);
|
||||||
|
color: var(--color-fg);
|
||||||
|
border: 1px solid var(--color-input-border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.input-base:focus {
|
||||||
|
border-color: var(--color-ring);
|
||||||
|
outline: 1px solid var(--color-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.input-base:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
/* Form elements use tokens */
|
||||||
input {
|
input,
|
||||||
background-color: white !important; /* Heller Hintergrund im Lightmode */
|
select,
|
||||||
color: black !important; /* Schwarzer Text im Lightmode */
|
textarea {
|
||||||
border: 1px solid #ccc; /* Optional: heller Rahmen */
|
background-color: var(--color-input-bg) !important;
|
||||||
}
|
color: var(--color-fg) !important;
|
||||||
|
border: 1px solid var(--color-input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--color-muted);
|
||||||
|
opacity: 1; /* ensure consistent visibility */
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: 2px solid var(--color-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-color: var(--color-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day--selected,
|
.react-datepicker__day--selected,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: "class",
|
// Ermöglicht zusätzlich zu .dark auch [data-theme="dark"] als Schalter
|
||||||
|
darkMode: ["class", '[data-theme="dark"]'],
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
|||||||
Reference in New Issue
Block a user