Compare commits
93 Commits
9db92a2728
...
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 | ||
|
|
c03802e97f | ||
|
|
1485c0c92c | ||
|
|
44ecbfa417 | ||
|
|
927a807c4d | ||
|
|
29a79ce0a9 | ||
|
|
2166744c63 | ||
|
|
81239f41ae | ||
|
|
584593ba71 | ||
|
|
4b83ff01cf | ||
|
|
8c88aa843c | ||
|
|
8a9cd72718 | ||
|
|
2484d057fb | ||
|
|
941b914fa9 | ||
|
|
bd683d021a | ||
|
|
124b1c1e59 | ||
|
|
6820fa9eed | ||
|
|
3daa6b1dbb | ||
|
|
9c7ad37233 | ||
|
|
0286670b81 | ||
|
|
02a0ce5891 | ||
|
|
47e0efeb80 | ||
|
|
b62c477d50 | ||
|
|
653a31ce63 | ||
|
|
57ffdecb10 | ||
|
|
11bd68200b | ||
|
|
2c92ca0866 | ||
|
|
a9ccdfc9ab | ||
|
|
f4f4c28cb7 | ||
|
|
d6fcf95795 | ||
|
|
6c2707ff47 | ||
|
|
420f1da114 | ||
|
|
5aa7618832 | ||
|
|
35171891a3 | ||
|
|
2df1ee1022 | ||
|
|
7fe842aa93 | ||
|
|
cdf4869548 | ||
|
|
bb115a9a4f | ||
|
|
da87ebc5c8 | ||
|
|
5b4eb7ff51 | ||
|
|
3254563458 | ||
|
|
5252ec5998 | ||
|
|
2d9cd74375 | ||
|
|
f4e0620b49 | ||
|
|
35e34b96d1 | ||
|
|
fb79817136 | ||
|
|
89dc26b0d2 | ||
|
|
c8616f7bbe | ||
|
|
b0b9952a2d | ||
|
|
8da1457e4d | ||
|
|
7dc64ca972 | ||
|
|
1b038ac844 | ||
|
|
cbc476b09a | ||
|
|
306f469634 | ||
|
|
772baea4ed | ||
|
|
f3f6e25e9c | ||
|
|
43fe9e2065 | ||
|
|
30f156934c | ||
|
|
b108d63106 | ||
|
|
b53762cf5c | ||
|
|
629548bfdd | ||
|
|
174d67cfd8 | ||
|
|
57baca292a | ||
|
|
0c02e6f1c9 | ||
|
|
dcf22d08fb | ||
|
|
867031d3c3 | ||
|
|
0c3eb4cc5a | ||
|
|
d4d0c91400 | ||
|
|
9147cec40f | ||
|
|
e732971cdc |
@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
|
||||
NEXT_PUBLIC_EXPORT_STATIC=false
|
||||
NEXT_PUBLIC_USE_CGI=false
|
||||
# App-Versionsnummer
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.785
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.879
|
||||
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
|
||||
NEXT_PUBLIC_EXPORT_STATIC=true
|
||||
NEXT_PUBLIC_USE_CGI=true
|
||||
# App-Versionsnummer
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.785
|
||||
NEXT_PUBLIC_APP_VERSION=1.6.879
|
||||
NEXT_PUBLIC_CPL_MODE=production
|
||||
@@ -1,34 +1,58 @@
|
||||
# .woodpecker.yml — Option B (Browser im Workspace, stabil für CI)
|
||||
when:
|
||||
- event: push
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: install-dependencies-and-browsers
|
||||
image: node:22
|
||||
environment:
|
||||
PLAYWRIGHT_BROWSERS_PATH: "0"
|
||||
CI: "true"
|
||||
E2E_BASE_URL: "http://localhost:3000"
|
||||
LANG: "C.UTF-8"
|
||||
TZ: "Europe/Berlin"
|
||||
PW_HEADLESS: "1" # erzwingt headless über die Config
|
||||
- name: clone
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
depth: 0
|
||||
lfs: true
|
||||
submodules: true
|
||||
|
||||
- name: verify-mocks
|
||||
image: mcr.microsoft.com/playwright:v1.54.2-jammy
|
||||
commands:
|
||||
- echo "📦 Installing deps..."
|
||||
- pwd
|
||||
- node -v && npm -v
|
||||
# Skip lifecycle scripts in CI to avoid running husky's prepare step
|
||||
- npm ci
|
||||
- echo "🧩 Installing Playwright (Chromium) into workspace..."
|
||||
- npx playwright install chromium
|
||||
# Zeig mir, ob die Datei wirklich im Checkout liegt:
|
||||
- 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)
|
||||
|
||||
- name: run-tests
|
||||
image: node:22
|
||||
- name: e2e-dev
|
||||
image: mcr.microsoft.com/playwright:v1.54.2-jammy
|
||||
environment:
|
||||
PLAYWRIGHT_BROWSERS_PATH: "0"
|
||||
CI: "true"
|
||||
E2E_BASE_URL: "http://localhost:3000"
|
||||
LANG: "C.UTF-8"
|
||||
TZ: "Europe/Berlin"
|
||||
PW_HEADLESS: "1"
|
||||
NODE_ENV: "production"
|
||||
NEXT_TELEMETRY_DISABLED: "1"
|
||||
PORT: "3000"
|
||||
commands:
|
||||
- echo "🔧 Installing system dependencies for Playwright..."
|
||||
- npx playwright install-deps
|
||||
- echo "🌱 Starting dev server (npm run dev)..."
|
||||
- npm run dev &
|
||||
|
||||
- echo "🧪 Running Playwright tests (Chromium only)..."
|
||||
- node -v && npm -v
|
||||
# Skip lifecycle scripts in CI to avoid running husky's prepare step (husky is a devDep)
|
||||
- env npm_config_production=false npm ci
|
||||
- npm run build
|
||||
# Start local static simulator in background
|
||||
- npm run server:sim &
|
||||
# Wait until simulator responds on port 3000 (no curl dependency)
|
||||
- node -e "const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();"
|
||||
- npx playwright test --project=chromium
|
||||
|
||||
- name: notify-success
|
||||
image: alpine/curl:latest
|
||||
when:
|
||||
status: success
|
||||
commands:
|
||||
- curl -d "Tests erfolgreich in woodpecker" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||
|
||||
- name: notify-failure
|
||||
image: alpine/curl:latest
|
||||
when:
|
||||
status: failure
|
||||
commands:
|
||||
- curl -d "Tests fehlgeschlagen in woodpecker" https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||
|
||||
494
CHANGELOG.md
494
CHANGELOG.md
@@ -1,3 +1,497 @@
|
||||
## [1.6.879] – 2025-09-08
|
||||
|
||||
- WIP: dark mode Modale
|
||||
|
||||
---
|
||||
## [1.6.878] – 2025-09-08
|
||||
|
||||
- WIP: dark mode Berichte
|
||||
|
||||
---
|
||||
## [1.6.877] – 2025-09-08
|
||||
|
||||
- WIP: dark mode Modale
|
||||
|
||||
---
|
||||
## [1.6.876] – 2025-09-08
|
||||
|
||||
- WIP: dark mode Baugrüppenträger sttus
|
||||
|
||||
---
|
||||
## [1.6.875] – 2025-09-08
|
||||
|
||||
- WIP: dark mode
|
||||
|
||||
---
|
||||
## [1.6.874] – 2025-09-08
|
||||
|
||||
- fix: TDR 2 Minuten eingestellt laut eingabe
|
||||
|
||||
---
|
||||
## [1.6.873] – 2025-09-08
|
||||
|
||||
- fix: TDR 2 Minuten eingestellt laut eingaben
|
||||
|
||||
---
|
||||
## [1.6.872] – 2025-09-08
|
||||
|
||||
- fix: TDR 2 Minuten eingestellt laut eingaben
|
||||
|
||||
---
|
||||
## [1.6.871] – 2025-09-08
|
||||
|
||||
- WIP: dark mode
|
||||
|
||||
---
|
||||
## [1.6.870] – 2025-09-08
|
||||
|
||||
- fix: Beim Aufruf der TDR-Detailseite erscheint im Hintergrund auf der KÜ ein Schleifenwiderstand von 0 KOhm. In der Daten Javascriptdatei steht jedoch der richtige Wert.
|
||||
|
||||
---
|
||||
## [1.6.869] – 2025-09-08
|
||||
|
||||
- fix: Beim Ausführen einer TDR-Messung (Klick auf blauen Button in der TDR-Detailseite) erscheint keine Rückmeldung. Dort müsste ein Hinweis erscheinen “TDR-Messung wird ausgeführt und kann bis zu zwei Minuten dauern”
|
||||
|
||||
---
|
||||
## [1.6.868] – 2025-09-08
|
||||
|
||||
- fix: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
|
||||
|
||||
---
|
||||
## [1.6.867] – 2025-09-08
|
||||
|
||||
- WIP: Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
|
||||
|
||||
---
|
||||
## [1.6.866] – 2025-09-08
|
||||
|
||||
- Test: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.865] – 2025-09-08
|
||||
|
||||
- test: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.864] – 2025-09-08
|
||||
|
||||
- fix: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.863] – 2025-09-08
|
||||
|
||||
- fix: Vereinfacht: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.862] – 2025-09-08
|
||||
|
||||
- fix. Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.861] – 2025-09-08
|
||||
|
||||
- test: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.860] – 2025-09-08
|
||||
|
||||
- fix: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.859] – 2025-09-08
|
||||
|
||||
- Jenkinsfile auf Woodpecker-Parität umgestellt:
|
||||
|
||||
---
|
||||
## [1.6.858] – 2025-09-08
|
||||
|
||||
- fix: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.857] – 2025-09-08
|
||||
|
||||
- Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage) , automatisch abmelden
|
||||
|
||||
---
|
||||
## [1.6.856] – 2025-09-08
|
||||
|
||||
- chore: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.855] – 2025-09-05
|
||||
|
||||
- fix: allow scripts in woodpecker
|
||||
|
||||
---
|
||||
## [1.6.854] – 2025-09-05
|
||||
|
||||
- fix: woodpecker allow scripts
|
||||
|
||||
---
|
||||
## [1.6.853] – 2025-09-05
|
||||
|
||||
- fix: woodpecker npm run server:sim
|
||||
|
||||
---
|
||||
## [1.6.852] – 2025-09-05
|
||||
|
||||
- fix: woodpecker
|
||||
|
||||
---
|
||||
## [1.6.851] – 2025-09-05
|
||||
|
||||
- fix: woodpecker rimraf not found
|
||||
|
||||
---
|
||||
## [1.6.850] – 2025-09-05
|
||||
|
||||
- fix: woodpecker compiler error
|
||||
|
||||
---
|
||||
## [1.6.849] – 2025-09-05
|
||||
|
||||
- fix: .woodpecker.yml
|
||||
|
||||
---
|
||||
## [1.6.848] – 2025-09-05
|
||||
|
||||
- test: playwright mit npm run dev erfolgreich
|
||||
|
||||
---
|
||||
## [1.6.847] – 2025-09-05
|
||||
|
||||
- chore: move playwright components folder to tests
|
||||
|
||||
---
|
||||
## [1.6.846] – 2025-09-05
|
||||
|
||||
- feat(kue705FO): scrolling für lange Modulnamen (48 Zeichen) + Version-Gate/Env-Override
|
||||
|
||||
- Unterstützt bis zu 48 Zeichen im Modulnamen; bei Überlänge automatische Laufschrift
|
||||
- Marquee via react-fast-marquee (SSR-sicher per next/dynamic)
|
||||
- Overflow-Erkennung + Tooltip mit vollem Namen
|
||||
- Version-Gate: aktiviert ab V4.30
|
||||
|
||||
---
|
||||
## [1.6.845] – 2025-09-05
|
||||
|
||||
- feat: prepare KÜ 8 for scrolling text
|
||||
|
||||
---
|
||||
## [1.6.844] – 2025-09-05
|
||||
|
||||
- test: woodpecker
|
||||
|
||||
---
|
||||
## [1.6.843] – 2025-09-05
|
||||
|
||||
- feat: local-cpl-sim.mjs port 3000
|
||||
|
||||
---
|
||||
## [1.6.842] – 2025-09-05
|
||||
|
||||
- refactor: retime all messages and charts data
|
||||
|
||||
---
|
||||
## [1.6.841] – 2025-09-05
|
||||
|
||||
- feat: local-cpl-sim.mjs Detailansicht Modal in System
|
||||
|
||||
---
|
||||
## [1.6.840] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim.mjs analogInputs /Messwerteingäge / analoge Eingänge
|
||||
|
||||
---
|
||||
## [1.6.839] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim.mjs digitalInputs /Messwerteingänge
|
||||
|
||||
---
|
||||
## [1.6.838] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim system
|
||||
|
||||
---
|
||||
## [1.6.837] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim meldungen/Berichte
|
||||
|
||||
---
|
||||
## [1.6.836] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim.mjs kabelueberwachung
|
||||
|
||||
---
|
||||
## [1.6.835] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim.mjs Einstellungen done
|
||||
|
||||
---
|
||||
## [1.6.834] – 2025-09-04
|
||||
|
||||
- feat: local-cpl-sim.mjs
|
||||
|
||||
---
|
||||
## [1.6.833] – 2025-09-04
|
||||
|
||||
- test: npx playwright test erfolgreich
|
||||
|
||||
---
|
||||
## [1.6.832] – 2025-09-03
|
||||
|
||||
- refactoring: test files
|
||||
|
||||
---
|
||||
## [1.6.831] – 2025-09-03
|
||||
|
||||
- fix: DigitalOutputsVies.tsx
|
||||
|
||||
---
|
||||
## [1.6.830] – 2025-09-03
|
||||
|
||||
- feat: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl
|
||||
|
||||
---
|
||||
## [1.6.829] – 2025-09-03
|
||||
|
||||
- feat(mocks): mesages_all.json mock script
|
||||
|
||||
---
|
||||
## [1.6.828] – 2025-09-03
|
||||
|
||||
- feat(mocks): retime chart mocks to today; add global/all-slot scripts
|
||||
|
||||
Add retimeAnalogInputs.mjs (all slots, single slot, or path)
|
||||
Add retimeAllCharts.mjs (recursive under chartsData)
|
||||
Update package.json with npm scripts:
|
||||
mocks:retime:ai (all analog slots)
|
||||
mocks:retime:ai:slot (single slot via %SLOT%)
|
||||
mocks:retime:all (entire chartsData tree)
|
||||
Preserve relative deltas; set first entry to today (same time); DIA2 daily at 00:00
|
||||
Skip files/arrays without parsable "t" timestamps
|
||||
|
||||
---
|
||||
## [1.6.827] – 2025-09-03
|
||||
|
||||
- feat: update analogInputs data in mocks
|
||||
|
||||
---
|
||||
## [1.6.826] – 2025-09-03
|
||||
|
||||
- refactor(api): Pfad für Digitalausgänge vereinheitlicht; Duplikat entfernt
|
||||
|
||||
updateDigitalOutputsHandler: JSON-Schreibpfad auf digitalOutputsMockData.json umgestellt
|
||||
digitalOutputsMockData.json gelöscht (nicht mehr benötigt)
|
||||
GET-Handler liest bereits aus dem kanonischen Pfad; Verhalten unverändert
|
||||
|
||||
---
|
||||
## [1.6.825] – 2025-09-03
|
||||
|
||||
- refactor(api): Legacy-Fallback entfernt; nur noch chartsData/cable-monitoring-data
|
||||
|
||||
---
|
||||
## [1.6.824] – 2025-09-03
|
||||
|
||||
- fix: AnalogInputsView.tsx style
|
||||
|
||||
---
|
||||
## [1.6.823] – 2025-09-03
|
||||
|
||||
- del: remove old mock files
|
||||
|
||||
---
|
||||
## [1.6.822] – 2025-09-03
|
||||
|
||||
- del: remove 4000value folder from mock
|
||||
|
||||
---
|
||||
## [1.6.821] – 2025-09-03
|
||||
|
||||
- doc: TODOs
|
||||
|
||||
---
|
||||
## [1.6.820] – 2025-09-03
|
||||
|
||||
- fix: curl
|
||||
|
||||
---
|
||||
## [1.6.819] – 2025-09-03
|
||||
|
||||
- fix: Die .woodpecker.yml wurde korrigiert:
|
||||
|
||||
trigger entfernt, stattdessen wieder when auf Top-Level.
|
||||
Für die Notification-Steps wurde das Image alpine/curl:latest hinzugefügt.
|
||||
Damit sollten die Linter-Fehler verschwinden und die Push-Benachrichtigungen funktionieren.
|
||||
|
||||
---
|
||||
## [1.6.818] – 2025-09-03
|
||||
|
||||
- fix: Die .woodpecker.yml wurde korrigiert:
|
||||
|
||||
Der clone-Step ist jetzt Teil der Steps.
|
||||
curl.exe wurde zu curl geändert (Linux-kompatibel).
|
||||
when wurde zu trigger geändert.
|
||||
Damit sollte Woodpecker den Build korrekt starten und die Push-Benachrichtigungen funktionieren.
|
||||
|
||||
---
|
||||
## [1.6.817] – 2025-09-03
|
||||
|
||||
- playwright headless true
|
||||
|
||||
---
|
||||
## [1.6.816] – 2025-09-03
|
||||
|
||||
- feat(ci): Push-Benachrichtigung bei Test-Erfolg oder -Fehlschlag für Jenkins und Woodpecker integriert
|
||||
|
||||
---
|
||||
## [1.6.815] – 2025-09-03
|
||||
|
||||
- refactor: move headerTest to header folder
|
||||
|
||||
---
|
||||
## [1.6.814] – 2025-09-02
|
||||
|
||||
- fix: playwright Test bugs beheben
|
||||
|
||||
---
|
||||
## [1.6.813] – 2025-09-02
|
||||
|
||||
- WIP: Test fehlgeschlagen
|
||||
|
||||
---
|
||||
## [1.6.812] – 2025-09-02
|
||||
|
||||
- test: extracted navigation tests to separate file
|
||||
|
||||
---
|
||||
## [1.6.811] – 2025-09-02
|
||||
|
||||
- Test: nav ausgelagert
|
||||
|
||||
---
|
||||
## [1.6.810] – 2025-09-02
|
||||
|
||||
- Test: done
|
||||
|
||||
---
|
||||
## [1.6.809] – 2025-09-02
|
||||
|
||||
- chore: remove jsconfig.json, project uses only tsconfig.json for path aliases
|
||||
|
||||
---
|
||||
## [1.6.808] – 2025-09-02
|
||||
|
||||
- Test: playwright done
|
||||
|
||||
---
|
||||
## [1.6.807] – 2025-09-02
|
||||
|
||||
- test: refactoring playwright test structure
|
||||
|
||||
---
|
||||
## [1.6.806] – 2025-09-02
|
||||
|
||||
- fix: Detailansicht Modal sichtbar beim klicken
|
||||
|
||||
---
|
||||
## [1.6.805] – 2025-09-02
|
||||
|
||||
- fix: ohne E-Mail
|
||||
|
||||
---
|
||||
## [1.6.804] – 2025-09-02
|
||||
|
||||
- fix: woodpecker
|
||||
|
||||
---
|
||||
## [1.6.803] – 2025-09-02
|
||||
|
||||
- feat: woodpecker E-Mail
|
||||
|
||||
---
|
||||
## [1.6.802] – 2025-09-02
|
||||
|
||||
- fix(ci): match case for systemMockData.js (Linux case-sensitive)
|
||||
|
||||
---
|
||||
## [1.6.801] – 2025-09-01
|
||||
|
||||
- test: find mock
|
||||
|
||||
---
|
||||
## [1.6.800] – 2025-09-01
|
||||
|
||||
- fix: TDR Messungstarten statt TDR aktivieren
|
||||
|
||||
---
|
||||
## [1.6.799] – 2025-09-01
|
||||
|
||||
- test: woodpecker dev mode
|
||||
|
||||
---
|
||||
## [1.6.798] – 2025-09-01
|
||||
|
||||
- test: .woodpecker
|
||||
|
||||
---
|
||||
## [1.6.797] – 2025-09-01
|
||||
|
||||
- Test: .woodpecker.yml
|
||||
|
||||
---
|
||||
## [1.6.796] – 2025-09-01
|
||||
|
||||
- Test: In KÜ RSL: Zahl mit 3 Nachkommastellen
|
||||
|
||||
---
|
||||
## [1.6.795] – 2025-09-01
|
||||
|
||||
- fix: KÜ ISO 2 Nachkommastellen und RSL 3 Nachkommastellen
|
||||
|
||||
---
|
||||
## [1.6.794] – 2025-09-01
|
||||
|
||||
- fix: System Footer responsive
|
||||
|
||||
---
|
||||
## [1.6.793] – 2025-09-01
|
||||
|
||||
- fix: playwright headless true
|
||||
|
||||
---
|
||||
## [1.6.792] – 2025-09-01
|
||||
|
||||
- fix: playwright August Text
|
||||
|
||||
---
|
||||
## [1.6.791] – 2025-09-01
|
||||
|
||||
- fix: playwright
|
||||
|
||||
---
|
||||
## [1.6.790] – 2025-08-31
|
||||
|
||||
- fix: Jenkins
|
||||
|
||||
---
|
||||
## [1.6.789] – 2025-08-31
|
||||
|
||||
- fix: Jenkins node20
|
||||
|
||||
---
|
||||
## [1.6.788] – 2025-08-31
|
||||
|
||||
- Test: Jenkinsfile
|
||||
|
||||
---
|
||||
## [1.6.787] – 2025-08-31
|
||||
|
||||
- fix: Die Konfiguration ist jetzt angepasst:
|
||||
Playwright verwendet immer einen bereits laufenden Dev-Server (reuseExistingServer: true).
|
||||
Damit gibt es keinen Port-Konflikt mehr,
|
||||
|
||||
---
|
||||
## [1.6.785] – 2025-08-29
|
||||
|
||||
- fix: playwright ->npm run dev -p 3000
|
||||
|
||||
106
Jenkinsfile
vendored
106
Jenkinsfile
vendored
@@ -1,21 +1,95 @@
|
||||
pipeline {
|
||||
agent any
|
||||
agent any
|
||||
|
||||
environment {
|
||||
NODE_PATH = '/var/jenkins_home/.npm/node_modules'
|
||||
NPM_CONFIG_CACHE = '/var/jenkins_home/.npm'
|
||||
environment {
|
||||
CI = "true"
|
||||
NODE_ENV = "production"
|
||||
NEXT_TELEMETRY_DISABLED = "1"
|
||||
PORT = "3000"
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
sh '''
|
||||
set -eux
|
||||
git status --short || true
|
||||
# Submodule & LFS falls vorhanden
|
||||
git submodule update --init --recursive || true
|
||||
git lfs install || true
|
||||
git lfs fetch || true
|
||||
git lfs checkout || true
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Install dependencies') {
|
||||
steps {
|
||||
sh 'npm install --prefer-offline --no-audit'
|
||||
}
|
||||
}
|
||||
stage('Run Playwright tests') {
|
||||
steps {
|
||||
sh 'npx playwright test'
|
||||
}
|
||||
}
|
||||
stage('verify-mocks') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eux
|
||||
docker run --rm -v "$PWD":/ws -w /ws \
|
||||
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||
pwd
|
||||
node -v && npm -v
|
||||
npm ci --ignore-scripts
|
||||
echo '=== git ls-files ==='
|
||||
git ls-files | grep -i '^mocks/device-cgi-simulator/SERVICE/systemMockData.js' || true
|
||||
echo '=== ls -la ==='
|
||||
ls -la mocks/device-cgi-simulator/SERVICE || true
|
||||
echo '=== file exists? ==='
|
||||
test -f mocks/device-cgi-simulator/SERVICE/systemMockData.js && echo 'FOUND' || (echo 'MISSING' && exit 1)
|
||||
"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('e2e-dev') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eux
|
||||
docker run --rm -v "$PWD":/ws -w /ws -p 3000:3000 \
|
||||
mcr.microsoft.com/playwright:v1.54.2-jammy bash -lc "
|
||||
node -v && npm -v
|
||||
env npm_config_production=false npm ci
|
||||
npm run build
|
||||
npm run server:sim &
|
||||
# Auf Port 3000 warten
|
||||
node -e \\"const http=require('http');let n=120;function ping(){http.get('http://localhost:3000',res=>{console.log('Server is up');process.exit(0)}).on('error',()=>{if(n--<=0){console.error('Server did not start');process.exit(1)}setTimeout(ping,1000)});}ping();\\"
|
||||
npx playwright test --project=chromium
|
||||
"
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
docker run --rm curlimages/curl:8.9.1 \
|
||||
-d "Tests erfolgreich in Jenkins" \
|
||||
https://ntfy.sh/OEOr8DNB0aT2mXWg231PeEEKwvuzt86qgM8ezQmgfcX9ZIlZ35
|
||||
'''
|
||||
}
|
||||
failure {
|
||||
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.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 117 KiB |
@@ -1,13 +1,16 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useAppDispatch } from "@/redux/store";
|
||||
import { setEvents } from "@/redux/slices/deviceEventsSlice";
|
||||
import {
|
||||
setEvents,
|
||||
initPersistedTimings,
|
||||
} from "@/redux/slices/deviceEventsSlice";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
loopMeasurementEvent?: number[];
|
||||
tdrMeasurementEvent?: number[];
|
||||
alignmentEvent?: number[];
|
||||
comparisonEvent?: number[]; // renamed from alignmentEvent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +21,25 @@ export default function DeviceEventsBridge() {
|
||||
|
||||
React.useEffect(() => {
|
||||
let lastSig = "";
|
||||
// Hydrate persisted timings once
|
||||
try {
|
||||
const raw =
|
||||
typeof window !== "undefined" &&
|
||||
localStorage.getItem("deviceEventsTimingsV1");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
dispatch(
|
||||
initPersistedTimings({
|
||||
loop: parsed.loop,
|
||||
tdr: parsed.tdr,
|
||||
compare: parsed.compare || parsed.align,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("DeviceEventsBridge hydration failed", e);
|
||||
}
|
||||
const readAndDispatch = () => {
|
||||
const ksx = Array.isArray(window.loopMeasurementEvent)
|
||||
? window.loopMeasurementEvent
|
||||
@@ -25,8 +47,8 @@ export default function DeviceEventsBridge() {
|
||||
const ksy = Array.isArray(window.tdrMeasurementEvent)
|
||||
? window.tdrMeasurementEvent
|
||||
: undefined;
|
||||
const ksz = Array.isArray(window.alignmentEvent)
|
||||
? window.alignmentEvent
|
||||
const ksz = Array.isArray(window.comparisonEvent)
|
||||
? window.comparisonEvent
|
||||
: undefined;
|
||||
// Build a stable signature of first 32 values per array
|
||||
const to32 = (a?: number[]) => {
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useAppSelector } from "@/redux/store";
|
||||
export default function GlobalActivityOverlay() {
|
||||
const anyLoop = useAppSelector((s) => s.deviceEvents.anyLoopActive);
|
||||
const anyTdr = useAppSelector((s) => s.deviceEvents.anyTdrActive);
|
||||
const anyAlign = useAppSelector((s) => s.deviceEvents.anyAlignmentActive);
|
||||
const anyCompare = useAppSelector((s) => s.deviceEvents.anyComparisonActive);
|
||||
const ksx = useAppSelector((s) => s.deviceEvents.ksx);
|
||||
const ksy = useAppSelector((s) => s.deviceEvents.ksy);
|
||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||
const alignmentStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.alignmentStartedAt
|
||||
const comparisonStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.comparisonStartedAt
|
||||
);
|
||||
|
||||
const fmt = (arr: number[]) =>
|
||||
@@ -24,13 +24,13 @@ export default function GlobalActivityOverlay() {
|
||||
// Simple 1s ticker so progress bars advance while overlay is shown
|
||||
const [now, setNow] = useState<number>(Date.now());
|
||||
useEffect(() => {
|
||||
const active = anyLoop || anyTdr || anyAlign;
|
||||
const active = anyLoop || anyTdr || anyCompare;
|
||||
if (!active) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [anyLoop, anyTdr, anyAlign]);
|
||||
}, [anyLoop, anyTdr, anyCompare]);
|
||||
|
||||
const active = anyLoop || anyTdr || anyAlign;
|
||||
const active = anyLoop || anyTdr || anyCompare;
|
||||
if (!active) return null;
|
||||
|
||||
const clamp = (v: number, min = 0, max = 1) =>
|
||||
@@ -102,13 +102,13 @@ export default function GlobalActivityOverlay() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{anyAlign && (
|
||||
{anyCompare && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-800 mb-1">
|
||||
Abgleich läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||
Comparison läuft… (KÜ: {fmt(ksz)}) kann bis zu 10 Minuten dauern
|
||||
</div>
|
||||
{(() => {
|
||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
||||
const { pct } = compute(comparisonStartedAt, ALIGN_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
|
||||
@@ -55,45 +55,57 @@ function Footer() {
|
||||
}, [showSlider]);
|
||||
|
||||
return (
|
||||
<footer className="relative bg-gray-300 p-4 xl:p-2 2xl:p-4 overflow-hidden text-black laptop:h-[5vh] ">
|
||||
<footer className="relative bg-[var(--color-surface-alt)] border-t border-base p-4 xl:p-2 2xl:p-4 overflow-hidden text-[var(--color-fg)] laptop:h-[5vh] theme-transition">
|
||||
<div className="container mx-auto flex justify-between">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Icon
|
||||
icon="material-symbols:factory-outline"
|
||||
className="text-xl text-blue-400"
|
||||
className="text-xl text-accent"
|
||||
/>
|
||||
<p className="text-sm">Littwin Systemtechnik GmbH & Co. KG</p>
|
||||
<p className="text-sm text-fg-muted">
|
||||
Littwin Systemtechnik GmbH & Co. KG
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Icon icon="charm:phone" className="text-xl text-blue-400" />
|
||||
<p className="text-sm">Telefon: 04402 972577-0</p>
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Icon icon="charm:phone" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">Telefon: 04402 972577-0</p>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Icon icon="mdi:email-outline" className="text-xl text-blue-400" />
|
||||
<p className="text-sm">kontakt@littwin-systemtechnik.de</p>
|
||||
<div className="flex flex-row space-x-2 items-center">
|
||||
<Icon icon="mdi:email-outline" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">
|
||||
kontakt@littwin-systemtechnik.de
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-row space-x-2 cursor-pointer"
|
||||
className="flex flex-row space-x-2 cursor-pointer items-center group"
|
||||
onClick={() => setShowSlider(true)}
|
||||
>
|
||||
<Icon icon="bi:book" className="text-xl text-blue-400" />
|
||||
<p className="text-sm">Handbücher</p>
|
||||
<Icon
|
||||
icon="bi:book"
|
||||
className="text-xl text-accent group-hover:brightness-110 transition"
|
||||
/>
|
||||
<p className="text-sm text-fg-muted group-hover:text-[var(--color-fg)] transition">
|
||||
Handbücher
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={sliderRef}
|
||||
className={`fixed top-0 right-0 w-64 h-full bg-white shadow-lg transform transition-transform duration-300 ${
|
||||
className={`fixed top-0 right-0 w-64 h-full bg-[var(--color-surface)] border-l border-base shadow-lg transform transition-transform duration-300 ${
|
||||
showSlider ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4 flex justify-between items-center border-b">
|
||||
<h3 className="text-lg font-semibold">PDF Handbücher</h3>
|
||||
<div className="p-4 flex justify-between items-center border-b border-base">
|
||||
<h3 className="text-base font-semibold text-[var(--color-fg)]">
|
||||
PDF Handbücher
|
||||
</h3>
|
||||
<button
|
||||
className="text-gray-500 hover:text-gray-800"
|
||||
className="text-[var(--color-muted)] hover:text-[var(--color-fg)] transition"
|
||||
onClick={() => setShowSlider(false)}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<Icon icon="carbon:close" className="text-2xl" />
|
||||
<Icon icon="carbon:close" className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +114,7 @@ function Footer() {
|
||||
{pdfFiles.map((fileName) => (
|
||||
<li
|
||||
key={fileName}
|
||||
className="cursor-pointer text-blue-500 hover:underline mb-2"
|
||||
className="cursor-pointer text-accent hover:underline mb-2 text-sm"
|
||||
onClick={() => loadPDF(fileName)}
|
||||
>
|
||||
{fileName}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client"; // components/Header.jsx
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -15,16 +15,18 @@ function Header() {
|
||||
const router = useRouter();
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||
const [isAdminLoggedIn, setIsAdminLoggedIn] = useState(false);
|
||||
const autoLogoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Removed duplicate declaration of deviceName
|
||||
|
||||
const handleCloseSettingsModal = () => setShowSettingsModal(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
const handleLogout = useCallback(() => {
|
||||
sessionStorage.removeItem("token"); // Token entfernen
|
||||
localStorage.setItem("isAdminLoggedIn", "false"); // Admin-Status entfernen
|
||||
localStorage.removeItem("adminLoginTime"); // Login-Zeitpunkt entfernen
|
||||
setIsAdminLoggedIn(false); // Zustand sofort aktualisieren
|
||||
router.push("/offline.html"); // Weiterleitung
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialer Check beim Laden der Komponente
|
||||
@@ -43,6 +45,56 @@ function Header() {
|
||||
clearInterval(interval); // Intervall stoppen, wenn die Komponente entladen wird
|
||||
};
|
||||
}, [isAdminLoggedIn]);
|
||||
|
||||
// Auto-Logout nach 1 Minute (Test): nutzt adminLoginTime aus localStorage
|
||||
useEffect(() => {
|
||||
// Timer bereinigen, wenn sich der Status ändert
|
||||
if (autoLogoutTimerRef.current) {
|
||||
clearTimeout(autoLogoutTimerRef.current);
|
||||
autoLogoutTimerRef.current = null;
|
||||
}
|
||||
|
||||
if (!isAdminLoggedIn) return;
|
||||
|
||||
const iso = localStorage.getItem("adminLoginTime");
|
||||
const loginTime = iso ? new Date(iso).getTime() : Date.now();
|
||||
if (!iso) {
|
||||
// Falls älterer Login ohne Zeitstempel, setze jetzt
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"adminLoginTime",
|
||||
new Date(loginTime).toISOString()
|
||||
);
|
||||
} catch {
|
||||
void 0; // ignore write errors (e.g., storage disabled)
|
||||
}
|
||||
}
|
||||
|
||||
// 1 Minute ab Login (60_000 ms), eine Stunde (3_600_000 ms) im Produktivbetrieb
|
||||
const target = loginTime + 3_600_000;
|
||||
const delay = Math.max(0, target - Date.now());
|
||||
|
||||
// Fallback: wenn Datum in Vergangenheit (z.B. Uhrzeit geändert), sofort abmelden
|
||||
autoLogoutTimerRef.current = setTimeout(() => {
|
||||
// Versuche den Button zu klicken, falls vorhanden
|
||||
const btn = document.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Abmelden"]'
|
||||
);
|
||||
if (btn) {
|
||||
btn.click();
|
||||
} else {
|
||||
// Fallback direkt
|
||||
handleLogout();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
if (autoLogoutTimerRef.current) {
|
||||
clearTimeout(autoLogoutTimerRef.current);
|
||||
autoLogoutTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isAdminLoggedIn, handleLogout]);
|
||||
//----------------------------------------------------------------
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
@@ -57,21 +109,43 @@ function Header() {
|
||||
}, [deviceName, dispatch]);
|
||||
//----------------------------------------------------------------
|
||||
|
||||
// Dark/Light Mode Toggle
|
||||
// Dark/Light Mode Toggle (persisted)
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
// Initial state from html class / localStorage (set by _document script before hydration)
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const html = document.documentElement;
|
||||
if (isDark) {
|
||||
html.classList.add("dark");
|
||||
} else {
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
if (typeof window === "undefined") return;
|
||||
const html = document.documentElement;
|
||||
const stored = localStorage.getItem("theme");
|
||||
const active = stored ? stored === "dark" : html.classList.contains("dark");
|
||||
setIsDark(active);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const html = document.documentElement;
|
||||
if (isDark) {
|
||||
html.classList.add("dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
html.classList.remove("dark");
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
}, [isDark]);
|
||||
|
||||
// Keyboard shortcut Alt + D to toggle theme
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.altKey && (e.key === "d" || e.key === "D")) {
|
||||
e.preventDefault();
|
||||
setIsDark((d) => !d);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="bg-gray-300 dark:bg-gray-800 flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-black dark:text-white ">
|
||||
<header className="bg-[var(--color-surface)] dark:bg-[var(--color-surface)]/90 backdrop-blur flex justify-between items-center w-full h-[13vh] laptop:h-[10vh] relative text-[var(--color-fg)] dark:text-[var(--color-fg)] shadow-sm border-b border-[var(--color-border)]">
|
||||
<div
|
||||
className="absolute transform -translate-y-1/2
|
||||
left-[8%] sm:left-[8%] md:left-[8%] lg:left-[8%] xl:left-[6%] 2xl:left-[2%] laptop:left-[4%] laptop:
|
||||
@@ -102,10 +176,10 @@ function Header() {
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col leading-tight whitespace-nowrap">
|
||||
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-xl laptop:text-base xl:text-lg font-bold text-[var(--color-fg)]">
|
||||
Meldestation
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
||||
<p className="text-[var(--color-fg-muted)] text-lg laptop:text-sm xl:text-base truncate max-w-[20vw]">
|
||||
{deviceName}
|
||||
</p>
|
||||
</div>
|
||||
@@ -117,7 +191,7 @@ function Header() {
|
||||
<button
|
||||
aria-label={isDark ? "Light Mode" : "Dark Mode"}
|
||||
onClick={() => setIsDark((d) => !d)}
|
||||
className="rounded-full p-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition"
|
||||
className="rounded-full p-2 bg-[var(--color-surface-alt)]/80 hover:bg-[var(--color-surface-alt)] dark:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)] transition border border-[var(--color-border)]"
|
||||
title={isDark ? "Light Mode" : "Dark Mode"}
|
||||
>
|
||||
{isDark ? (
|
||||
@@ -139,7 +213,8 @@ function Header() {
|
||||
<div className="flex items-center justify-end w-1/4 space-x-1">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded"
|
||||
aria-label="Abmelden"
|
||||
className="px-4 py-2 rounded bg-[var(--color-accent)] text-white hover:brightness-110 shadow-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-ring)] focus:ring-offset-2 focus:ring-offset-[var(--color-background)] transition"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
@@ -149,7 +224,7 @@ function Header() {
|
||||
|
||||
{/* Warnhinweis, wenn der Admin angemeldet ist */}
|
||||
{isAdminLoggedIn && (
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-yellow-400 text-center py-2 text-black font-bold">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-full xl:w-1/4 2xl:w-1/4 h-8 bg-[var(--color-warning)] text-center py-2 text-black font-bold tracking-wide">
|
||||
Admin-Modus aktiv
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -37,8 +37,8 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg laptop:p-1 xl:p-1 ${
|
||||
loading ? "cursor-wait" : ""
|
||||
className={`text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg laptop:p-1 xl:p-1 ${
|
||||
loading ? "cursor-wait opacity-70" : ""
|
||||
}`}
|
||||
>
|
||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||
@@ -54,24 +54,24 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
loading ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b items-center">
|
||||
<thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/30 text-[var(--color-fg)] border-b border-[var(--color-border)] items-center">
|
||||
<tr>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
Eingang
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
Messwert
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
Einheit
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
Einstellungen
|
||||
</th>
|
||||
<th className="border p-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="border p-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
Messkurve
|
||||
</th>
|
||||
</tr>
|
||||
@@ -92,12 +92,12 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
loading
|
||||
? "cursor-wait"
|
||||
: analogInput.id === activeId
|
||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
||||
: "hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
? "bg-[var(--color-accent-soft)] dark:bg-[var(--color-surface-alt)]/60 text-[var(--color-fg)]"
|
||||
: "hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30"
|
||||
}`}
|
||||
>
|
||||
<td
|
||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
<div className="flex items-center gap-1 ">
|
||||
@@ -109,7 +109,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className="border p-2 text-right bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="border p-2 text-right bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
{typeof analogInput.value === "number"
|
||||
@@ -118,19 +118,19 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
</td>
|
||||
|
||||
<td
|
||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
{analogInput.unit || "-"}
|
||||
</td>
|
||||
<td
|
||||
className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]"
|
||||
onClick={() => handleSelect(analogInput.id!, analogInput)}
|
||||
>
|
||||
{analogInput.label || "----"}
|
||||
</td>
|
||||
|
||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSelect(analogInput.id!, analogInput);
|
||||
@@ -141,7 +141,7 @@ export default function AnalogInputsTable({ loading }: { loading: boolean }) {
|
||||
<Icon icon={settingsIcon} className="text-xl" />
|
||||
</button>
|
||||
</td>
|
||||
<td className="border p-2 text-center bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 text-center bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSelect(analogInput.id!, analogInput);
|
||||
|
||||
@@ -35,14 +35,12 @@ function AnalogInputsView() {
|
||||
loading ? "cursor-wait" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="container mx-auto">
|
||||
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg p-4 max-w-3xl text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
||||
Messwerteingänge
|
||||
</h2>
|
||||
<AnalogInputsTable loading={loading} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 justify-items-start">
|
||||
<div className="rounded-lg p-4 max-w-3xl text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] shadow-sm">
|
||||
<h2 className="text-xl font-semibold mb-4 text-[var(--color-fg)] tracking-wide">
|
||||
Messwerteingänge
|
||||
</h2>
|
||||
<AnalogInputsTable loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ const Baugruppentraeger: React.FC = () => {
|
||||
baugruppen.push(
|
||||
<div
|
||||
key={i}
|
||||
className="flex bg-white shadow-md rounded-lg mb-4 xl:mb-0 lg:mb-0 border border-gray-200 w-full laptop:scale-y-75 xl:scale-y-90"
|
||||
className="flex card mb-4 xl:mb-0 lg:mb-0 w-full laptop:scale-y-75 xl:scale-y-90"
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
{slots.map((version, index) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ const DashboardView: React.FC = () => {
|
||||
}, [dispatch]);
|
||||
//-------------------------------------
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<div className="flex flex-col gap-3 p-4 h-[calc(100vh-13vh-8vh)] laptop:h-[calc(100vh-10vh-5vh)] xl:h-[calc(100vh-10vh-6vh)] laptop:gap-0 bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center w-full lg:w-2/3">
|
||||
<div className="flex justify-between gap-1">
|
||||
@@ -33,7 +33,7 @@ const DashboardView: React.FC = () => {
|
||||
icon="ri:calendar-schedule-line"
|
||||
className="text-littwin-blue text-4xl xl:text-2xl"
|
||||
/>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100 xl:text-base">
|
||||
<h1 className="text-xl font-bold xl:text-base text-[var(--color-fg)] tracking-wide">
|
||||
Letzten 20 Meldungen
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -48,22 +48,22 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-3 p-4 ${className}`}>
|
||||
<div className="overflow-auto max-h-[80vh]">
|
||||
<table className="min-w-full border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
||||
<table className="min-w-full border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<thead className="text-left sticky top-0 z-10 bg-[var(--color-surface-alt)]/70 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)]">
|
||||
<tr>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Prio
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Zeitstempel
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Quelle
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Meldung
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
@@ -72,24 +72,24 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
{filteredMessages.slice(0, 20).map((msg, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
className="hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||
>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
<div
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
{msg.t}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
{msg.i}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
{msg.m}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border p-2 bg-[var(--color-surface)] text-[var(--color-fg)] border-[var(--color-border)]">
|
||||
{msg.v}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -97,7 +97,7 @@ export default function Last20MessagesTable({ className }: Props) {
|
||||
</tbody>
|
||||
</table>
|
||||
{messages.length === 0 && (
|
||||
<div className="mt-4 text-center text-gray-500 italic dark:text-gray-400">
|
||||
<div className="mt-4 text-center italic text-[var(--color-fg-muted)]">
|
||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ const NetworkInfo: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full flex-direction: row flex">
|
||||
<div className=" flex-grow flex justify-between items-center mt-1 bg-white dark:bg-gray-800 p-2 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
||||
<div className=" flex-grow flex justify-between items-center mt-1 p-2 rounded-lg shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border border-[var(--color-border)] laptop:m-0 laptop:scale-y-75 2xl:scale-y-75">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image
|
||||
src="/images/IP-icon.svg"
|
||||
@@ -49,12 +49,8 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
IP-Adresse
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{ip}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">IP-Adresse</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">{ip}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,10 +64,8 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Subnet-Maske
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">Subnet-Maske</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||
{subnet}
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,8 +81,8 @@ const NetworkInfo: React.FC = () => {
|
||||
priority
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Gateway</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">Gateway</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||
{gateway}
|
||||
</p>
|
||||
</div>
|
||||
@@ -97,8 +91,8 @@ const NetworkInfo: React.FC = () => {
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-xs font-bold text-littwin-blue">OPC-UA</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Status</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
<p className="text-xs text-[var(--color-fg-muted)]">Status</p>
|
||||
<p className="text-sm font-medium text-[var(--color-fg)]">
|
||||
{opcUaZustand}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,7 @@ import { Icon } from "@iconify/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store";
|
||||
|
||||
type VersionInfoProps = {
|
||||
className?: string;
|
||||
};
|
||||
type VersionInfoProps = { className?: string };
|
||||
|
||||
const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||
const appVersion =
|
||||
@@ -17,26 +15,26 @@ const VersionInfo: React.FC<VersionInfoProps> = ({ className = "" }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 w-full laptop:p-2 ${className}`}
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
||||
<div className={`card w-full p-3 laptop:p-2 ${className}`}>
|
||||
<h2 className="text-base font-semibold mb-2 text-[var(--color-fg)]">
|
||||
Versionsinformationen
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-row p-2 space-x-2">
|
||||
<Icon icon="bx:code-block" className="text-xl text-blue-400" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Applikationsversion: {appVersion}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row p-2 space-x-2">
|
||||
<Icon icon="mdi:web" className="text-xl text-blue-400" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Webversion: {webVersion}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="bx:code-block" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">
|
||||
Applikationsversion:{" "}
|
||||
<span className="text-[var(--color-fg)]">{appVersion}</span>
|
||||
</p>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="mdi:web" className="text-xl text-accent" />
|
||||
<p className="text-sm text-fg-muted">
|
||||
Webversion:{" "}
|
||||
<span className="text-[var(--color-fg)]">{webVersion}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function DigitalInputsWidget({
|
||||
//console.log("DigitalInputs", inputs);
|
||||
|
||||
return (
|
||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
||||
<div className="text-[var(--color-fg)] bg-[var(--color-surface)] dark:bg-[var(--color-surface)] shadow-sm border border-[var(--color-border)] p-3 rounded-lg w-full laptop:p-1 xl:p-1">
|
||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||
<Icon
|
||||
icon={inputIcon}
|
||||
@@ -38,19 +38,19 @@ export default function DigitalInputsWidget({
|
||||
/>
|
||||
Meldungseingänge {inputRange.start + 1} – {inputRange.end}
|
||||
</h2>
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)]">
|
||||
<thead className="bg-[var(--color-surface-alt)]/60 dark:bg-[var(--color-surface-alt)]/25 text-[var(--color-fg)] border-b border-[var(--color-border)]">
|
||||
<tr>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Eingang
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Zustand
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Aktion
|
||||
</th>
|
||||
</tr>
|
||||
@@ -59,9 +59,9 @@ export default function DigitalInputsWidget({
|
||||
{inputs.map((input) => (
|
||||
<tr
|
||||
key={input.id}
|
||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
className="border-b border-[var(--color-border)] hover:bg-[var(--color-surface-alt)]/70 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||
>
|
||||
<td className="px-1 py-0 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-0 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<div className="flex items-center gap-1 ">
|
||||
<Icon
|
||||
icon={loginIcon}
|
||||
@@ -70,7 +70,7 @@ export default function DigitalInputsWidget({
|
||||
{input.id}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
{input.eingangOffline ? (
|
||||
<div className="relative group inline-block">
|
||||
<span className="text-red-500 sm:text-sm md:text-base lg:text-lg xl:text-xl 2xl:text-2xl laptop:text-sm ">
|
||||
@@ -91,10 +91,10 @@ export default function DigitalInputsWidget({
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
{input.label}
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<Icon
|
||||
icon={settingsIcon}
|
||||
className="text-gray-400 text-base laptop:text-sm xl:text-sm 2xl:text-lg cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
||||
|
||||
@@ -18,6 +18,10 @@ const DigitalOutputsView: React.FC = () => {
|
||||
const [isOutputModalOpen, setIsOutputModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch immediately on mount to ensure data is present without waiting for the first interval
|
||||
dispatch(getDigitalOutputsThunk());
|
||||
|
||||
// Then continue polling periodically
|
||||
const interval = setInterval(() => {
|
||||
dispatch(getDigitalOutputsThunk());
|
||||
}, 3000);
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function DigitalOutputsWidget({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 shadow-md border border-gray-200 dark:border-gray-700 p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
|
||||
<div className="bg-[var(--color-surface)] text-[var(--color-fg)] shadow-md border border-base p-3 rounded-lg w-full h-fit max-h-[400px] overflow-auto">
|
||||
<h2 className="laptop:text-sm md:text-base 2xl:text-lg font-bold mb-3 flex items-center">
|
||||
<Icon
|
||||
icon={outputIcon}
|
||||
@@ -74,19 +74,19 @@ export default function DigitalOutputsWidget({
|
||||
/>
|
||||
Schaltausgänge
|
||||
</h2>
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-white dark:bg-gray-900 rounded-lg">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 border-b">
|
||||
<table className="w-full text-xs laptop:text-[10px] xl:text-xs 2xl:text-sm border-collapse bg-[var(--color-surface)] rounded-lg">
|
||||
<thead className="bg-[var(--color-surface)] text-[var(--color-fg)] border-b border-base">
|
||||
<tr>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Ausgang
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Bezeichnung
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Schalter
|
||||
</th>
|
||||
<th className="px-1 py-1 text-left bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="px-1 py-1 text-left bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
Aktion
|
||||
</th>
|
||||
</tr>
|
||||
@@ -95,33 +95,33 @@ export default function DigitalOutputsWidget({
|
||||
{digitalOutputs.map((output) => (
|
||||
<tr
|
||||
key={output.id}
|
||||
className="border-b hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
className="border-b border-base hover:bg-[var(--color-surface-alt)] transition-colors"
|
||||
>
|
||||
<td className="flex items-center px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="flex items-center px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<Icon
|
||||
icon={outputIcon}
|
||||
className="text-gray-600 mr-1 text-base"
|
||||
className="text-[var(--color-muted)] mr-1 text-base"
|
||||
/>
|
||||
{output.id}
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
{output.label}
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<Icon
|
||||
icon={switchIcon}
|
||||
className={`cursor-pointer text-base transition ${
|
||||
output.status
|
||||
? "text-littwin-blue"
|
||||
: "text-gray-500 scale-x-[-1]"
|
||||
} dark:hover:text-littwin-blue`}
|
||||
? "text-accent"
|
||||
: "text-[var(--color-muted)] scale-x-[-1]"
|
||||
} hover:text-accent`}
|
||||
onClick={() => handleToggle(output.id)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-1 py-1 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="px-1 py-1 bg-[var(--color-surface)] text-[var(--color-fg)]">
|
||||
<Icon
|
||||
icon={settingsIcon}
|
||||
className="text-gray-400 text-base cursor-pointer dark:text-gray-300 dark:hover:text-white"
|
||||
className="text-[var(--color-muted)] text-base cursor-pointer hover:text-[var(--color-fg)]"
|
||||
onClick={() => openOutputModal(output)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -119,19 +119,23 @@ function KabelueberwachungView() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
{[1, 2, 3, 4].map((rack) => (
|
||||
<button
|
||||
key={rack}
|
||||
onClick={() => changeRack(rack)}
|
||||
className={`mr-2 ${
|
||||
Number(activeRack) === Number(rack)
|
||||
? "bg-littwin-blue text-white p-1 rounded-sm"
|
||||
: "bg-gray-300 p-1 text-sm"
|
||||
}`}
|
||||
>
|
||||
Rack {rack}
|
||||
</button>
|
||||
))}
|
||||
{[1, 2, 3, 4].map((rack) => {
|
||||
const isActive = Number(activeRack) === Number(rack);
|
||||
return (
|
||||
<button
|
||||
key={rack}
|
||||
onClick={() => changeRack(rack)}
|
||||
aria-pressed={isActive}
|
||||
className={`mr-2 px-2 py-1 rounded-sm text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-accent/50 ${
|
||||
isActive
|
||||
? "btn-primary"
|
||||
: "btn-muted text-fg opacity-90 hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
Rack {rack}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-row space-x-8 xl:space-x-0 2xl:space-x-8 qhd:space-x-16 ml-[5%] mt-[5%]">
|
||||
{(
|
||||
|
||||
@@ -373,7 +373,7 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap"
|
||||
disabled={isLoading || rslRunning}
|
||||
>
|
||||
{rslRunning ? "RSL läuft..." : "RSL starten"}
|
||||
{rslRunning ? "RSL läuft..." : "RSL Messung starten"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleFetchData}
|
||||
@@ -389,7 +389,8 @@ const LoopChartActionBar = forwardRef((_props, ref) => {
|
||||
<div className="mb-4 text-center space-y-1">
|
||||
<p className="text-lg font-semibold">RSL Messung läuft</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Bitte warten… (noch {TOTAL_DURATION - rslProgress}s)
|
||||
Bitte warten…{" "}
|
||||
{Math.min(100, Math.round((rslProgress / TOTAL_DURATION) * 100))}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
||||
|
||||
@@ -30,6 +30,36 @@ const TDRChartActionBar: React.FC = () => {
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const currentChartData = selectedId !== null ? tdrDataById[selectedId] : [];
|
||||
|
||||
// ▶ Fortschrittsanzeige für laufende TDR-Messung (max. 120s bzw. konfigurierbar)
|
||||
const TDR_TOTAL_DURATION = parseInt(
|
||||
process.env.NEXT_PUBLIC_TDR_DURATION_SECONDS || "120",
|
||||
10
|
||||
);
|
||||
const [tdrRunning, setTdrRunning] = useState(false);
|
||||
const [tdrProgress, setTdrProgress] = useState(0); // Sekunden
|
||||
|
||||
useEffect(() => {
|
||||
if (!tdrRunning) return;
|
||||
setTdrProgress(0);
|
||||
const startedAt = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
|
||||
if (elapsed >= TDR_TOTAL_DURATION) {
|
||||
setTdrProgress(TDR_TOTAL_DURATION);
|
||||
setTdrRunning(false);
|
||||
clearInterval(interval);
|
||||
} else {
|
||||
setTdrProgress(elapsed);
|
||||
}
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [tdrRunning, TDR_TOTAL_DURATION]);
|
||||
|
||||
const startTdrProgress = () => {
|
||||
setTdrRunning(true);
|
||||
setTdrProgress(0);
|
||||
};
|
||||
|
||||
// 📌 Referenz setzen (nutzt Slotnummer + 1 für die API)
|
||||
const handleSetReference = async () => {
|
||||
if (
|
||||
@@ -94,18 +124,22 @@ const TDRChartActionBar: React.FC = () => {
|
||||
try {
|
||||
console.log("🚀 Starte TDR Messung für Slot:", selectedSlot);
|
||||
console.log("📡 CGI URL:", cgiUrl);
|
||||
|
||||
const response = await fetch(cgiUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`CGI-Fehler: ${response.status}`);
|
||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV === "development";
|
||||
if (isDev) {
|
||||
// Dev / Simulator: sofort starten & Progress anzeigen
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
console.log("✅ [DEV] TDR Mock-Start ok für Slot", selectedSlot);
|
||||
startTdrProgress();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(cgiUrl);
|
||||
if (!response.ok) throw new Error(`CGI-Fehler: ${response.status}`);
|
||||
console.log("✅ TDR Messung gestartet für Slot", selectedSlot);
|
||||
alert(`✅ TDR Messung für Slot ${selectedSlot + 1} gestartet`);
|
||||
startTdrProgress();
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Starten der TDR Messung:", err);
|
||||
alert("❌ Fehler beim Starten der TDR Messung.");
|
||||
//alert("❌ Fehler beim Starten der TDR Messung.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,109 +164,141 @@ const TDRChartActionBar: React.FC = () => {
|
||||
}, [selectedSlot, dispatch]);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
|
||||
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
|
||||
<div className="text-sm font-semibold">
|
||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
||||
</div>
|
||||
<>
|
||||
<div className="flex justify-between items-center p-2 bg-gray-100 rounded-lg space-x-4">
|
||||
{/* 🧩 Slot-Anzeige (1-basiert für Benutzer) */}
|
||||
<div className="text-sm font-semibold">
|
||||
{selectedSlot !== null ? `KÜ ${selectedSlot + 1}` : "Kein KÜ gewählt"}
|
||||
</div>
|
||||
|
||||
{/* ✅ Referenz setzen */}
|
||||
{selectedId !== null && (
|
||||
{/* ✅ Referenz setzen */}
|
||||
{selectedId !== null && (
|
||||
<button
|
||||
onClick={handleSetReference}
|
||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
||||
>
|
||||
TDR-Kurve als Referenz speichern
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 🚀 TDR starten */}
|
||||
<button
|
||||
onClick={handleSetReference}
|
||||
className="border border-littwin-blue text-littwin-blue bg-white rounded px-3 py-1 text-sm hover:bg-gray-200"
|
||||
onClick={handleStartTDR}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
||||
disabled={selectedSlot === null || tdrRunning}
|
||||
>
|
||||
TDR-Kurve als Referenz speichern
|
||||
{tdrRunning
|
||||
? `TDR läuft... (${Math.min(
|
||||
100,
|
||||
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
|
||||
)}%)`
|
||||
: "TDR-Messung starten"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 🚀 TDR starten */}
|
||||
<button
|
||||
onClick={handleStartTDR}
|
||||
className="px-4 py-1 bg-littwin-blue text-white rounded text-sm whitespace-nowrap "
|
||||
disabled={selectedSlot === null}
|
||||
>
|
||||
Messung aktivieren
|
||||
</button>
|
||||
|
||||
{/* 🔽 Dropdown für Messungen */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Listbox
|
||||
value={selectedId}
|
||||
onChange={(id) => {
|
||||
setSelectedId(id);
|
||||
if (id !== null) {
|
||||
dispatch(getTDRChartDataByIdThunk(id));
|
||||
}
|
||||
}}
|
||||
disabled={idsForSlot.length === 0}
|
||||
>
|
||||
<div className="relative w-96">
|
||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedId
|
||||
? (() => {
|
||||
const selected = idsForSlot.find(
|
||||
(e) => e.id === selectedId
|
||||
);
|
||||
return selected
|
||||
? `${new Date(selected.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})} – Fehlerstelle: ${selected.d} m`
|
||||
: "Wähle Messung";
|
||||
})()
|
||||
: "Wähle Messung"}
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
||||
{idsForSlot.map((entry) => (
|
||||
<Listbox.Option
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
className={({ selected, active }) =>
|
||||
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
{/* 🔽 Dropdown für Messungen */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Listbox
|
||||
value={selectedId}
|
||||
onChange={(id) => {
|
||||
setSelectedId(id);
|
||||
if (id !== null) {
|
||||
dispatch(getTDRChartDataByIdThunk(id));
|
||||
}
|
||||
}}
|
||||
disabled={idsForSlot.length === 0}
|
||||
>
|
||||
<div className="relative w-96">
|
||||
<Listbox.Button className="w-full border px-2 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{selectedId
|
||||
? (() => {
|
||||
const selected = idsForSlot.find(
|
||||
(e) => e.id === selectedId
|
||||
);
|
||||
return selected
|
||||
? `${new Date(selected.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})} – Fehlerstelle: ${selected.d} m`
|
||||
: "Wähle Messung";
|
||||
})()
|
||||
: "Wähle Messung"}
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{new Date(entry.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}{" "}
|
||||
– Fehlerstelle: {entry.d} m
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 10.585l3.71-3.355a.75.75 0 111.02 1.1l-4.25 3.85a.75.75 0 01-1.02 0l-4.25-3.85a.75.75 0 01.02-1.06z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
|
||||
{idsForSlot.map((entry) => (
|
||||
<Listbox.Option
|
||||
key={entry.id}
|
||||
value={entry.id}
|
||||
className={({ selected, active }) =>
|
||||
`px-4 py-1 cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{new Date(entry.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}{" "}
|
||||
– Fehlerstelle: {entry.d} m
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tdrRunning && (
|
||||
<div className="fixed inset-0 z-[1000] flex flex-col items-center justify-center bg-white/80 backdrop-blur-sm">
|
||||
<div className="mb-4 text-center space-y-1">
|
||||
<p className="text-lg font-semibold">
|
||||
TDR Messung läuft... kann bis zu zwei Minuten dauern
|
||||
</p>
|
||||
<p className="text-sm text-gray-700">
|
||||
Bitte warten…{" "}
|
||||
{Math.min(
|
||||
100,
|
||||
Math.round((tdrProgress / TDR_TOTAL_DURATION) * 100)
|
||||
)}
|
||||
%
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-2/3 max-w-xl h-4 bg-gray-200 rounded overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="h-full bg-littwin-blue transition-all ease-linear"
|
||||
style={{
|
||||
width: `${(tdrProgress / TDR_TOTAL_DURATION) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"use client"; // components/modules/kue705FO/Kue705FO.tsx
|
||||
import React, { useState, useMemo } from "react";
|
||||
import React, { useState, useMemo, useEffect, useRef } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Marquee: any = dynamic(() => import("react-fast-marquee"), {
|
||||
ssr: false,
|
||||
});
|
||||
import { useSelector } from "react-redux";
|
||||
import KueModal from "./modals/SettingsModalWrapper";
|
||||
// import FallSensors from "../../fall-detection-sensors/FallSensors";
|
||||
@@ -54,6 +59,19 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
// Admin authentication hook for security - using showModal as true for continuous auth check
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
|
||||
// Modulname (max 48 Zeichen) vorbereiten
|
||||
const moduleNameRaw = useMemo(
|
||||
() => kueName?.[slotIndex] || `Modul ${slotIndex + 1}`,
|
||||
[kueName, slotIndex]
|
||||
);
|
||||
const moduleName48 = useMemo(
|
||||
() =>
|
||||
typeof moduleNameRaw === "string"
|
||||
? moduleNameRaw.slice(0, 48)
|
||||
: String(moduleNameRaw),
|
||||
[moduleNameRaw]
|
||||
);
|
||||
|
||||
const [activeButton, setActiveButton] = useState<"Schleife" | "TDR" | "ISO">(
|
||||
"Schleife"
|
||||
);
|
||||
@@ -156,18 +174,6 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
const openTdrModal = () => {
|
||||
setActiveButton("TDR");
|
||||
setloopTitleText("Entfernung [km]");
|
||||
|
||||
const latestTdrDistanceMeters =
|
||||
Array.isArray(tdmChartData?.[slotIndex]) &&
|
||||
tdmChartData[slotIndex].length > 0 &&
|
||||
typeof tdmChartData[slotIndex][0].d === "number"
|
||||
? tdmChartData[slotIndex][0].d
|
||||
: 0;
|
||||
|
||||
const latestTdrDistance = Number(
|
||||
(latestTdrDistanceMeters / 1000).toFixed(3)
|
||||
);
|
||||
setLoopDisplayValue(latestTdrDistance);
|
||||
setShowTdrModal(true);
|
||||
};
|
||||
|
||||
@@ -206,31 +212,64 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
);
|
||||
const { setCurrentModulName } = useModulName(slotIndex, modulName);
|
||||
//---------------------------------
|
||||
//---------------------------------
|
||||
const tdmChartData = useSelector(
|
||||
(state: RootState) => state.tdmChartSlice.data
|
||||
// Version-gate für Laufschrift: erst ab V4.30 aktiv
|
||||
const parseVersion = (v?: string): [number, number, number] => {
|
||||
if (!v) return [0, 0, 0];
|
||||
const m = String(v).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
||||
if (!m) return [0, 0, 0];
|
||||
const major = parseInt(m[1] || "0", 10) || 0;
|
||||
const minor = parseInt(m[2] || "0", 10) || 0;
|
||||
const patch = parseInt(m[3] || "0", 10) || 0;
|
||||
return [major, minor, patch];
|
||||
};
|
||||
const gte = (a: [number, number, number], b: [number, number, number]) => {
|
||||
if (a[0] !== b[0]) return a[0] > b[0];
|
||||
if (a[1] !== b[1]) return a[1] > b[1];
|
||||
return a[2] >= b[2];
|
||||
};
|
||||
const marqueeOverride =
|
||||
process.env.NEXT_PUBLIC_ENABLE_KUE_MARQUEE === "1" ||
|
||||
process.env.NEXT_PUBLIC_ENABLE_KUE_MARQUEE === "true";
|
||||
const scrollFeatureEnabled = useMemo(
|
||||
() => marqueeOverride || gte(parseVersion(kueVersion), [4, 30, 0]),
|
||||
[kueVersion, marqueeOverride]
|
||||
);
|
||||
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);
|
||||
// Überlängen-Erkennung für Laufschrift
|
||||
const nameContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const measureTextRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [shouldScroll, setShouldScroll] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const measure = () => {
|
||||
if (!scrollFeatureEnabled) {
|
||||
setShouldScroll(false);
|
||||
return;
|
||||
}
|
||||
const container = nameContainerRef.current;
|
||||
const text = measureTextRef.current;
|
||||
if (!container || !text) {
|
||||
setShouldScroll(false);
|
||||
return;
|
||||
}
|
||||
const needs = text.scrollWidth > container.clientWidth + 2;
|
||||
setShouldScroll(needs);
|
||||
};
|
||||
measure();
|
||||
window.addEventListener("resize", measure);
|
||||
return () => window.removeEventListener("resize", measure);
|
||||
}, [moduleName48, scrollFeatureEnabled]);
|
||||
//---------------------------------
|
||||
// TDR Distanz wird im Display nicht angezeigt – Daten für Modal werden separat geladen
|
||||
|
||||
//---------------------------------
|
||||
|
||||
const loopValue =
|
||||
activeButton === "TDR"
|
||||
? latestTdrDistance
|
||||
: typeof schleifenwiderstand === "number"
|
||||
const rslValue =
|
||||
typeof schleifenwiderstand === "number"
|
||||
? schleifenwiderstand
|
||||
: Number(schleifenwiderstand);
|
||||
|
||||
const { loopDisplayValue, setLoopDisplayValue } = useLoopDisplay(
|
||||
loopValue,
|
||||
rslValue,
|
||||
activeButton
|
||||
);
|
||||
|
||||
@@ -340,15 +379,19 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
>
|
||||
{isoDisplayValue === "Abgleich"
|
||||
? "ISO: Abgleich"
|
||||
: `ISO: ${Number(isolationswert)} MOhm`}
|
||||
: `ISO: ${Number(isolationswert)
|
||||
.toFixed(2)
|
||||
.replace(".", ",")} MOhm`}
|
||||
</span>
|
||||
{/* 3. Zeile: Schleifenwert, in Rot bei Schleifenfehler, sonst normal */}
|
||||
{/* 3. Zeile: Schleifenwert (RSL) immer anzeigen, unabhängig von aktivem Button */}
|
||||
<span
|
||||
className={`whitespace-nowrap block text-[0.65rem] font-semibold ${
|
||||
Number(kueAlarm2?.[slotIndex]) === 1 ? "text-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{`RSL: ${loopDisplayValue} kOhm`}
|
||||
{`RSL: ${Number(loopDisplayValue)
|
||||
.toFixed(3)
|
||||
.replace(".", ",")} kOhm`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,8 +399,38 @@ const Kue705FO: React.FC<Kue705FOProps> = ({
|
||||
<div className="absolute top-0 left-[4.688rem] w-[0.188rem] h-full bg-white z-0"></div>
|
||||
<div className="absolute top-[2.5rem] left-[4.688rem] w-[2.5rem] h-[0.188rem] bg-white z-0"></div>
|
||||
|
||||
<div className="absolute bottom-[1.25rem] left-0 right-0 text-black text-[0.625rem] bg-gray-300 p-[0.063rem] text-center">
|
||||
{kueName?.[slotIndex] || `Modul ${slotIndex + 1}`}
|
||||
{/* Hidden measuring span for overflow detection (kept measurable) */}
|
||||
<span
|
||||
ref={measureTextRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: -9999,
|
||||
top: -9999,
|
||||
visibility: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{moduleName48}
|
||||
</span>
|
||||
|
||||
<div
|
||||
ref={nameContainerRef}
|
||||
className="absolute bottom-[1.25rem] left-0 right-0 text-black text-[0.625rem] bg-gray-300 p-[0.063rem] overflow-hidden"
|
||||
>
|
||||
{shouldScroll && scrollFeatureEnabled ? (
|
||||
<Marquee pauseOnHover gradient={false} speed={40}>
|
||||
<span className="pr-8 whitespace-nowrap" title={moduleName48}>
|
||||
{moduleName48}
|
||||
</span>
|
||||
</Marquee>
|
||||
) : (
|
||||
<span
|
||||
className="block text-center whitespace-nowrap"
|
||||
title={moduleName48}
|
||||
>
|
||||
{moduleName48}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-[0.063rem] right-[0.063rem] text-black text-[0.5rem]">
|
||||
|
||||
@@ -12,22 +12,48 @@ export default function SlotActivityOverlay({
|
||||
const ksz = useAppSelector((s) => s.deviceEvents.ksz);
|
||||
const loopStartedAt = useAppSelector((s) => s.deviceEvents.loopStartedAt);
|
||||
const tdrStartedAt = useAppSelector((s) => s.deviceEvents.tdrStartedAt);
|
||||
const alignmentStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.alignmentStartedAt
|
||||
const comparisonStartedAt = useAppSelector(
|
||||
(s) => s.deviceEvents.comparisonStartedAt
|
||||
);
|
||||
const loopStartedAtBySlot = useAppSelector(
|
||||
(s) => s.deviceEvents.loopStartedAtBySlot
|
||||
);
|
||||
const tdrStartedAtBySlot = useAppSelector(
|
||||
(s) => s.deviceEvents.tdrStartedAtBySlot
|
||||
);
|
||||
const comparisonStartedAtBySlot = useAppSelector(
|
||||
(s) => s.deviceEvents.comparisonStartedAtBySlot
|
||||
);
|
||||
|
||||
const loopActive = Array.isArray(ksx) && ksx[slotIndex] === 1;
|
||||
const tdrActive = Array.isArray(ksy) && ksy[slotIndex] === 1;
|
||||
const alignActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
||||
const comparisonActive = Array.isArray(ksz) && ksz[slotIndex] === 1;
|
||||
|
||||
// Persist whenever arrays change
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"deviceEventsTimingsV1",
|
||||
JSON.stringify({
|
||||
loop: loopStartedAtBySlot,
|
||||
tdr: tdrStartedAtBySlot,
|
||||
compare: comparisonStartedAtBySlot,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to persist timings", e);
|
||||
}
|
||||
}, [loopStartedAtBySlot, tdrStartedAtBySlot, comparisonStartedAtBySlot]);
|
||||
|
||||
// Progress ticker
|
||||
const [now, setNow] = useState<number>(Date.now());
|
||||
useEffect(() => {
|
||||
const any = loopActive || tdrActive || alignActive;
|
||||
const any = loopActive || tdrActive || comparisonActive;
|
||||
if (!any) return;
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [loopActive, tdrActive, alignActive]);
|
||||
}, [loopActive, tdrActive, comparisonActive]);
|
||||
|
||||
const clamp = (v: number, min = 0, max = 1) =>
|
||||
Math.max(min, Math.min(max, v));
|
||||
@@ -40,10 +66,10 @@ export default function SlotActivityOverlay({
|
||||
|
||||
// Durations
|
||||
const LOOP_MS = 2 * 60 * 1000; // ~2 min
|
||||
const TDR_MS = 30 * 1000; // ~30 s
|
||||
const TDR_MS = 110 * 1000; // ~2 min laut die Eigaben
|
||||
const ALIGN_MS = 10 * 60 * 1000; // ~10 min
|
||||
|
||||
if (!loopActive && !tdrActive && !alignActive) return null;
|
||||
if (!loopActive && !tdrActive && !comparisonActive) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/70 backdrop-blur-sm">
|
||||
@@ -56,7 +82,8 @@ export default function SlotActivityOverlay({
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-gray-800 mb-1">Schleife</div>
|
||||
{(() => {
|
||||
const { pct } = compute(loopStartedAt, LOOP_MS);
|
||||
const started = loopStartedAtBySlot[slotIndex] ?? loopStartedAt;
|
||||
const { pct } = compute(started, LOOP_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
@@ -77,7 +104,8 @@ export default function SlotActivityOverlay({
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-gray-800 mb-1">TDR</div>
|
||||
{(() => {
|
||||
const { pct } = compute(tdrStartedAt, TDR_MS);
|
||||
const started = tdrStartedAtBySlot[slotIndex] ?? tdrStartedAt;
|
||||
const { pct } = compute(started, TDR_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
@@ -94,11 +122,13 @@ export default function SlotActivityOverlay({
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{alignActive && (
|
||||
{comparisonActive && (
|
||||
<div>
|
||||
<div className="text-[0.7rem] text-gray-800 mb-1">Abgleich</div>
|
||||
{(() => {
|
||||
const { pct } = compute(alignmentStartedAt, ALIGN_MS);
|
||||
const started =
|
||||
comparisonStartedAtBySlot[slotIndex] ?? comparisonStartedAt;
|
||||
const { pct } = compute(started, ALIGN_MS);
|
||||
return (
|
||||
<div>
|
||||
<div className="h-2 w-full bg-gray-200 rounded overflow-hidden">
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// components/main/kabelueberwachung/kue705FO/hooks/useLoopDisplay.ts
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Keeps and updates the loop (RSL) display value only when "Schleife" active.
|
||||
// For ISO or TDR views we do not overwrite the displayed RSL value.
|
||||
const useLoopDisplay = (
|
||||
schleifenwiderstand: number,
|
||||
rslValue: number,
|
||||
activeButton: "Schleife" | "TDR" | "ISO"
|
||||
) => {
|
||||
const [loopDisplayValue, setLoopDisplayValue] =
|
||||
useState<number>(schleifenwiderstand);
|
||||
const [loopDisplayValue, setLoopDisplayValue] = useState<number>(rslValue);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeButton === "Schleife") {
|
||||
setLoopDisplayValue(schleifenwiderstand);
|
||||
setLoopDisplayValue(rslValue);
|
||||
}
|
||||
// For ISO and TDR, the value is set manually via setLoopDisplayValue
|
||||
}, [schleifenwiderstand, activeButton]);
|
||||
}, [rslValue, activeButton]);
|
||||
|
||||
return { loopDisplayValue, setLoopDisplayValue };
|
||||
};
|
||||
|
||||
@@ -69,14 +69,20 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="p-2 flex justify-between items-center rounded-t-md">
|
||||
<h2 className="text-base font-bold">Einstellungen KÜ {slot + 1}</h2>
|
||||
<button onClick={onClose} className="text-2xl hover:text-gray-200">
|
||||
<div className="p-2 flex justify-between items-center rounded-t-md bg-surface-alt border-b border-base">
|
||||
<h2 className="text-base font-bold text-fg">
|
||||
Einstellungen KÜ {slot + 1}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-2xl text-fg-muted hover:text-fg transition-colors focus:outline-none focus:ring-2 focus:ring-accent/50 rounded"
|
||||
aria-label="Modal schließen"
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start bg-gray-100 space-x-2 p-2">
|
||||
<div className="flex justify-start bg-surface-alt space-x-2 p-2 border-b border-base">
|
||||
{[
|
||||
{ label: "Allgemein", key: "kue" as const },
|
||||
{ label: "TDR ", key: "tdr" as const },
|
||||
@@ -86,18 +92,17 @@ export default function KueModal({ showModal, onClose, slot }: KueModalProps) {
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveTab(key)}
|
||||
className={`px-4 py-1 rounded-t font-bold text-sm ${
|
||||
className={`px-4 py-1 rounded-t font-semibold text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
activeTab === key
|
||||
? "bg-white text-littwin-blue"
|
||||
: "text-gray-500 hover:text-black"
|
||||
? "bg-surface text-accent shadow-sm"
|
||||
: "text-fg-muted hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto">
|
||||
<div className="p-4 bg-surface rounded-b-md h-[20rem] laptop:h-[24rem] 2xl:h-[30rem] overflow-y-auto text-fg">
|
||||
{activeTab === "kue" && (
|
||||
<KueEinstellung
|
||||
slot={slot}
|
||||
|
||||
@@ -14,22 +14,22 @@ export default function MeldungenTabelle({
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-auto max-h-[80vh]">
|
||||
<table className="min-w-full border">
|
||||
<thead className="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100 text-left sticky top-0 z-10">
|
||||
<table className="min-w-full border border-base table-surface text-fg">
|
||||
<thead className="text-left sticky top-0 z-10 bg-surface-alt/90 backdrop-blur supports-[backdrop-filter]:bg-surface-alt/70">
|
||||
<tr>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||
Prio
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||
Zeitstempel
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||
Quelle
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||
Meldung
|
||||
</th>
|
||||
<th className="p-2 border bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<th className="p-2 border border-base bg-surface-alt text-fg font-medium">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
@@ -38,15 +38,15 @@ export default function MeldungenTabelle({
|
||||
{messages.map((msg, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
className="transition-colors hover:bg-surface-alt/60"
|
||||
>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border border-base p-2 bg-surface text-fg">
|
||||
<div
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: msg.c }}
|
||||
></div>
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border border-base p-2 bg-surface text-fg">
|
||||
{new Date(msg.t).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
@@ -56,13 +56,13 @@ export default function MeldungenTabelle({
|
||||
second: "2-digit",
|
||||
})}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border border-base p-2 bg-surface text-fg">
|
||||
{msg.i}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border border-base p-2 bg-surface text-fg">
|
||||
{msg.m}
|
||||
</td>
|
||||
<td className="border p-2 bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
|
||||
<td className="border border-base p-2 bg-surface text-fg">
|
||||
{msg.v}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -70,7 +70,7 @@ export default function MeldungenTabelle({
|
||||
</tbody>
|
||||
</table>
|
||||
{messages.length === 0 && (
|
||||
<div className="mt-4 text-center text-gray-500 italic">
|
||||
<div className="mt-4 text-center text-fg-muted italic">
|
||||
Keine Meldungen im gewählten Zeitraum vorhanden.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -60,14 +60,14 @@ export default function MeldungenView() {
|
||||
/>
|
||||
<button
|
||||
onClick={() => dispatch(getMessagesThunk({ fromDate, toDate }))}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded h-fit"
|
||||
className="btn-primary px-4 py-2 h-fit"
|
||||
>
|
||||
Anzeigen
|
||||
</button>
|
||||
|
||||
<Listbox value={sourceFilter} onChange={setSourceFilter}>
|
||||
<div className="relative ml-6 w-64">
|
||||
<Listbox.Button className="bg-white text-gray-900 w-full border px-4 py-2 rounded text-left flex justify-between items-center dark:bg-gray-900 dark:text-gray-100">
|
||||
<Listbox.Button className="bg-[var(--color-surface)] text-[var(--color-fg)] w-full border border-base px-4 py-2 rounded text-left flex justify-between items-center">
|
||||
<span>{sourceFilter}</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
@@ -83,19 +83,19 @@ export default function MeldungenView() {
|
||||
/>
|
||||
</svg>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options className="bg-white absolute z-50 mt-1 w-full border rounded dark:bg-gray-900">
|
||||
<Listbox.Options className="bg-[var(--color-surface)] absolute z-50 mt-1 w-full border border-base rounded shadow-sm">
|
||||
{sources.map((src) => (
|
||||
<Listbox.Option
|
||||
key={src}
|
||||
value={src}
|
||||
className={({ selected, active, disabled }) =>
|
||||
`px-4 py-2 cursor-pointer text-gray-900 dark:text-gray-100 ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
`px-4 py-2 cursor-pointer text-[var(--color-fg)] transition-colors ${
|
||||
disabled
|
||||
? "opacity-50 text-[var(--color-muted)] cursor-not-allowed"
|
||||
: selected
|
||||
? "bg-accent text-white"
|
||||
: active
|
||||
? "bg-blue-100 dark:bg-gray-700 dark:text-white"
|
||||
: disabled
|
||||
? "opacity-50 text-gray-400 dark:text-gray-500 cursor-not-allowed"
|
||||
? "bg-[var(--color-surface-alt)]"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import { useAdminAuth } from "./hooks/useAdminAuth";
|
||||
const DatabaseSettings: React.FC = () => {
|
||||
const { isAdminLoggedIn } = useAdminAuth(true);
|
||||
return (
|
||||
<div className="p-6 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto rounded shadow text-gray-900 dark:text-gray-100">
|
||||
<div className="p-6 bg-[var(--color-surface-alt)] max-w-5xl mr-auto rounded shadow text-[var(--color-fg)]">
|
||||
<h2 className="text-lg font-bold mb-6">Datenbank Einstellungen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearMessages}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
||||
className="btn-accent px-4 py-2 rounded shadow"
|
||||
>
|
||||
Meldungen löschen
|
||||
</button>
|
||||
@@ -25,7 +25,7 @@ const DatabaseSettings: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearLogger}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
||||
className="btn-accent px-4 py-2 rounded shadow"
|
||||
>
|
||||
Messwerte Logger löschen
|
||||
</button>
|
||||
@@ -41,7 +41,7 @@ const DatabaseSettings: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearDatabase}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
||||
className="btn-accent px-4 py-2 rounded shadow"
|
||||
>
|
||||
Datenbank vollständig leeren
|
||||
</button>
|
||||
@@ -49,7 +49,7 @@ const DatabaseSettings: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearConfig}
|
||||
className="bg-littwin-blue text-white px-4 py-2 rounded shadow "
|
||||
className="btn-accent px-4 py-2 rounded shadow"
|
||||
>
|
||||
Konfiguration löschen
|
||||
</button>
|
||||
|
||||
@@ -63,8 +63,11 @@ const GeneralSettings: React.FC = () => {
|
||||
setMac1(systemSettings.mac1 || "");
|
||||
}, [systemSettings]);
|
||||
|
||||
const inputCls =
|
||||
"border border-base focus:border-accent rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none";
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] dark:text-gray-100 ">
|
||||
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto overflow-y-auto max-h-[calc(100vh-200px)] text-[var(--color-fg)]">
|
||||
<h2 className="text-sm md:text-md font-bold mb-2">
|
||||
Allgemeine Einstellungen
|
||||
</h2>
|
||||
@@ -74,25 +77,18 @@ const GeneralSettings: React.FC = () => {
|
||||
<label className="block text-xs md:text-sm font-medium">Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className={inputCls}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MAC Adresse */}
|
||||
<div>
|
||||
<label className="block text-xs md:text-sm font-medium">
|
||||
MAC Adresse 1:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
value={mac1}
|
||||
disabled
|
||||
/>
|
||||
<input type="text" className={inputCls} value={mac1} disabled />
|
||||
</div>
|
||||
|
||||
{/* Systemzeit */}
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs md:text-sm font-medium mb-1">
|
||||
@@ -101,13 +97,13 @@ const GeneralSettings: React.FC = () => {
|
||||
<div className="flex flex-row gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className={inputCls}
|
||||
value={systemUhr.replace(/\s*Uhr$/, "")}
|
||||
disabled
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={() => handleSetDateTime()}
|
||||
>
|
||||
Systemzeit übernehmen
|
||||
@@ -120,7 +116,7 @@ const GeneralSettings: React.FC = () => {
|
||||
<label className="block text-xs md:text-sm font-medium">IP:</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className={inputCls}
|
||||
value={ip}
|
||||
onChange={(e) => setIp(e.target.value)}
|
||||
/>
|
||||
@@ -131,7 +127,7 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className={inputCls}
|
||||
value={subnet}
|
||||
onChange={(e) => setSubnet(e.target.value)}
|
||||
/>
|
||||
@@ -142,7 +138,7 @@ const GeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-600 focus:border-littwin-blue dark:focus:border-littwin-blue rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className={inputCls}
|
||||
value={gateway}
|
||||
onChange={(e) => setGateway(e.target.value)}
|
||||
/>
|
||||
@@ -152,7 +148,7 @@ const GeneralSettings: React.FC = () => {
|
||||
<div className="col-span-2 flex flex-wrap md:justify-between gap-1 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={() => handleReboot()}
|
||||
>
|
||||
Neustart CPL
|
||||
@@ -160,7 +156,7 @@ const GeneralSettings: React.FC = () => {
|
||||
{isAdminLoggedIn && (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={() => {
|
||||
const confirmed = window.confirm(
|
||||
"⚠️ Wollen Sie wirklich ein Firmwareupdate für alle KÜ-Module starten?"
|
||||
|
||||
@@ -27,7 +27,7 @@ const NTPSettings: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
|
||||
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||
<h2 className="text-sm md:text-md font-bold mb-4">NTP Einstellungen</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
@@ -35,7 +35,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">NTP Server 1</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={ntp1}
|
||||
onChange={(e) => setNtp1(e.target.value)}
|
||||
/>
|
||||
@@ -45,7 +45,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">NTP Server 2</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={ntp2}
|
||||
onChange={(e) => setNtp2(e.target.value)}
|
||||
/>
|
||||
@@ -55,7 +55,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">NTP Server 3</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={ntp3}
|
||||
onChange={(e) => setNtp3(e.target.value)}
|
||||
/>
|
||||
@@ -65,7 +65,7 @@ const NTPSettings: React.FC = () => {
|
||||
<label className="block text-xs font-medium">Zeitzone</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={ntpTimezone}
|
||||
onChange={(e) => setNtpTimezone(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100 ">
|
||||
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<Image
|
||||
src="/images/OPCUA.jpg"
|
||||
@@ -44,9 +44,11 @@ export default function OPCUAInterfaceSettings() {
|
||||
<label className="mr-3 font-medium text-sm">Server Status:</label>
|
||||
<button
|
||||
onClick={() => dispatch(toggleOpcUaServer())}
|
||||
className={`px-3 py-1 rounded text-sm ${
|
||||
opcuaSettings.isEnabled ? "bg-littwin-blue" : "bg-gray-300"
|
||||
} text-white`}
|
||||
className={`px-3 py-1 rounded text-sm font-medium transition-colors text-white ${
|
||||
opcuaSettings.isEnabled
|
||||
? "bg-accent hover:brightness-110"
|
||||
: "bg-[var(--color-muted)] hover:bg-[var(--color-fg)]/20"
|
||||
}`}
|
||||
>
|
||||
{opcuaSettings.isEnabled ? "Aktiviert" : "Deaktiviert"}
|
||||
</button>
|
||||
@@ -62,7 +64,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
<select
|
||||
value={opcuaSettings.encryption}
|
||||
onChange={(e) => dispatch(setOpcUaEncryption(e.target.value))}
|
||||
className="w-full p-1 border border-gray-300 rounded-md text-sm"
|
||||
className="w-full p-1 border border-base rounded-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)]"
|
||||
>
|
||||
<option value="None">Keine</option>
|
||||
<option value="Basic256">Basic256</option>
|
||||
@@ -74,7 +76,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
{/* ✅ OPCUA Zustand */}
|
||||
<div className="mb-3">
|
||||
<label className="block font-medium text-sm mb-1">OPCUA Zustand</label>
|
||||
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
|
||||
{opcuaSettings.opcUaZustand}
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-grow p-1 border border-gray-300 dark:border-gray-700 rounded-l-md text-sm !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="flex-grow p-1 border border-base rounded-l-md text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={nodesetName}
|
||||
onChange={(e) => setNodesetName(e.target.value)}
|
||||
disabled={opcuaSettings.isEnabled} // Disable input when server is enabled
|
||||
@@ -106,7 +108,7 @@ export default function OPCUAInterfaceSettings() {
|
||||
<label className="block font-medium text-sm mb-1">
|
||||
Aktuelle OPC-Clients
|
||||
</label>
|
||||
<div className="p-1 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-sm text-gray-900 dark:text-gray-100">
|
||||
<div className="p-1 border border-base rounded-md bg-[var(--color-surface)] text-sm text-[var(--color-fg)]">
|
||||
{opcUaActiveClientCount}
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,12 +122,12 @@ export default function OPCUAInterfaceSettings() {
|
||||
{opcuaSettings.users.map((user) => (
|
||||
<li
|
||||
key={user.id}
|
||||
className="p-1 bg-white shadow-sm rounded-md flex justify-between items-center text-sm"
|
||||
className="p-1 bg-[var(--color-surface)] border border-base rounded-md flex justify-between items-center text-sm text-[var(--color-fg)]"
|
||||
>
|
||||
<span className="font-medium">{user.username}</span>
|
||||
<button
|
||||
onClick={() => dispatch(removeOpcUaUser(user.id))}
|
||||
className="text-red-500"
|
||||
className="text-danger hover:underline"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
@@ -140,18 +142,18 @@ export default function OPCUAInterfaceSettings() {
|
||||
placeholder="Benutzername"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
className="p-1 border rounded flex-grow text-sm"
|
||||
className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Passwort"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="p-1 border rounded flex-grow text-sm"
|
||||
className="p-1 border border-base rounded flex-grow text-sm bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddUser}
|
||||
className="bg-littwin-blue text-white p-1 rounded text-sm"
|
||||
className="btn-primary p-1 rounded text-sm"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
|
||||
@@ -22,6 +22,12 @@ const UserManagementSettings: React.FC = () => {
|
||||
() => {
|
||||
setLoginSuccess(true);
|
||||
setError("");
|
||||
// Speichere die System-Uhrzeit (Login-Zeitpunkt) im localStorage
|
||||
try {
|
||||
localStorage.setItem("adminLoginTime", new Date().toISOString());
|
||||
} catch {
|
||||
// Ignoriere Speicherfehler (z. B. in Private Mode)
|
||||
}
|
||||
},
|
||||
(errorMsg) => {
|
||||
setLoginSuccess(false);
|
||||
@@ -38,7 +44,7 @@ const UserManagementSettings: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-3 bg-gray-100 dark:bg-gray-800 max-w-5xl mr-auto text-gray-900 dark:text-gray-100">
|
||||
<div className="p-6 md:p-3 bg-[var(--color-surface-alt)] max-w-5xl mr-auto text-[var(--color-fg)]">
|
||||
<h2 className="text-sm md:text-md font-bold mb-4">Login Admin-Bereich</h2>
|
||||
|
||||
{/* Admin Login/Logout */}
|
||||
@@ -46,8 +52,15 @@ const UserManagementSettings: React.FC = () => {
|
||||
{isAdminLoggedIn ? (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={logoutAdmin}
|
||||
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={() => {
|
||||
try {
|
||||
localStorage.removeItem("adminLoginTime");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
logoutAdmin();
|
||||
}}
|
||||
>
|
||||
Admin abmelden
|
||||
</button>
|
||||
@@ -57,7 +70,7 @@ const UserManagementSettings: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Benutzername"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -65,14 +78,14 @@ const UserManagementSettings: React.FC = () => {
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Passwort"
|
||||
className="border border-gray-300 dark:border-gray-700 rounded h-8 p-1 w-full text-xs !bg-white !text-black placeholder-gray-400 transition-colors duration-150 focus:outline-none dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="border border-base rounded h-8 p-1 w-full text-xs bg-[var(--color-surface)] text-[var(--color-fg)] placeholder-[var(--color-muted)] transition-colors duration-150 focus:outline-none"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-littwin-blue text-white px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
className="btn-accent px-4 py-2 h-8 text-xs rounded whitespace-nowrap"
|
||||
onClick={handleLogin}
|
||||
>
|
||||
Admin anmelden
|
||||
@@ -83,9 +96,9 @@ const UserManagementSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{loginSuccess && (
|
||||
<p className="text-green-600 text-xs mt-2">Login erfolgreich!</p>
|
||||
<p className="text-success text-xs mt-2">Login erfolgreich!</p>
|
||||
)}
|
||||
{error && <p className="text-red-500 text-xs mt-2">{error}</p>}
|
||||
{error && <p className="text-danger text-xs mt-2">{error}</p>}
|
||||
|
||||
{/*
|
||||
// Benutzerverwaltungstabelle (kommt später)
|
||||
|
||||
@@ -13,7 +13,7 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 ">
|
||||
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-6 rounded shadow-md text-center w-80">
|
||||
<div className="p-6 rounded shadow-sm text-center w-80 bg-[var(--color-surface)] dark:bg-[var(--color-surface)] text-[var(--color-fg)] border border-[var(--color-border)]">
|
||||
{/*
|
||||
<h2 className="text-lg font-bold mb-4">
|
||||
Firmwareupdate
|
||||
@@ -26,9 +26,9 @@ const ProgressModal: React.FC<Props> = ({ visible, progress, slot }) => {
|
||||
</h2>
|
||||
Bitte Fenster nicht schließen
|
||||
<h2></h2>
|
||||
<div className="w-full bg-gray-200 rounded-full h-4">
|
||||
<div className="w-full h-4 rounded-full bg-[var(--color-surface-alt)]/80 dark:bg-[var(--color-surface-alt)]/30 border border-[var(--color-border)] overflow-hidden">
|
||||
<div
|
||||
className="bg-littwin-blue h-4 rounded-full transition-all duration-100"
|
||||
className="h-full rounded-full transition-all duration-200 bg-[var(--color-accent)]"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,11 @@ import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState, useAppDispatch } from "@/redux/store";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
import { setFullScreen } from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import DateRangePicker from "@/components/common/DateRangePicker";
|
||||
import {
|
||||
setVonDatum,
|
||||
setBisDatum,
|
||||
} from "@/redux/slices/kabelueberwachungChartSlice";
|
||||
import { resetDateRange } from "@/redux/slices/dateRangePickerSlice";
|
||||
|
||||
// Import Thunks
|
||||
import SystemChartActionBar from "@/components/main/system/SystemChartActionBar";
|
||||
import { getSystemspannung5VplusThunk } from "@/redux/thunks/getSystemspannung5VplusThunk";
|
||||
import { getSystemspannung15VplusThunk } from "@/redux/thunks/getSystemspannung15VplusThunk";
|
||||
import { getSystemspannung15VminusThunk } from "@/redux/thunks/getSystemspannung15VminusThunk";
|
||||
@@ -214,8 +210,8 @@ export const DetailModal = ({
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setZeitraum("DIA0");
|
||||
dispatch(setVonDatum(""));
|
||||
dispatch(setBisDatum(""));
|
||||
// Reset DateRangePicker to its defaults (it sets 30 days → today on mount)
|
||||
dispatch(resetDateRange());
|
||||
|
||||
// Chart-Daten zurücksetzen beim Öffnen
|
||||
setChartData({ datasets: [] });
|
||||
@@ -260,8 +256,7 @@ export const DetailModal = ({
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(setFullScreen(false));
|
||||
dispatch(setVonDatum(""));
|
||||
dispatch(setBisDatum(""));
|
||||
dispatch(resetDateRange());
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -307,25 +302,55 @@ export const DetailModal = ({
|
||||
}
|
||||
}, [chartData, isLoading]);
|
||||
|
||||
// DateRange from global DateRangePicker slice
|
||||
const pickerVonDatum = useSelector(
|
||||
(state: RootState) => state.dateRangePicker.vonDatum
|
||||
);
|
||||
const pickerBisDatum = useSelector(
|
||||
(state: RootState) => state.dateRangePicker.bisDatum
|
||||
);
|
||||
|
||||
// Update chart data when Redux data changes (only after button click)
|
||||
useEffect(() => {
|
||||
if (shouldUpdateChart && reduxData && reduxData.length > 0) {
|
||||
console.log("Redux data for chart:", reduxData);
|
||||
// Filter data by selected date range (inclusive end date)
|
||||
let filtered = reduxData;
|
||||
try {
|
||||
if (pickerVonDatum && pickerBisDatum) {
|
||||
const start = new Date(`${pickerVonDatum}T00:00:00`);
|
||||
const end = new Date(`${pickerBisDatum}T23:59:59`);
|
||||
const s = start.getTime();
|
||||
const e = end.getTime();
|
||||
filtered = reduxData.filter((entry) => {
|
||||
const t = new Date(entry.t).getTime();
|
||||
return t >= s && t <= e;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Zeitfilter konnte nicht angewendet werden:", err);
|
||||
}
|
||||
|
||||
console.log("Redux data for chart (filtered):", filtered.length);
|
||||
if (!filtered.length) {
|
||||
setChartData({ datasets: [] });
|
||||
setShouldUpdateChart(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create datasets array for multiple lines
|
||||
const datasets = [];
|
||||
|
||||
// Check which data fields are available and create datasets accordingly
|
||||
const hasMinimum = reduxData.some(
|
||||
const hasMinimum = filtered.some(
|
||||
(entry) => entry.i !== undefined && entry.i !== null && entry.i !== 0
|
||||
);
|
||||
const hasMaximum = reduxData.some(
|
||||
const hasMaximum = filtered.some(
|
||||
(entry) => entry.a !== undefined && entry.a !== null
|
||||
);
|
||||
const hasAverage = reduxData.some(
|
||||
const hasAverage = filtered.some(
|
||||
(entry) => entry.g !== undefined && entry.g !== null
|
||||
);
|
||||
const hasCurrent = reduxData.some(
|
||||
const hasCurrent = filtered.some(
|
||||
(entry) => entry.m !== undefined && entry.m !== null
|
||||
);
|
||||
|
||||
@@ -333,7 +358,7 @@ export const DetailModal = ({
|
||||
if (hasMinimum) {
|
||||
datasets.push({
|
||||
label: "Messwert Minimum",
|
||||
data: reduxData.map((entry) => ({
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.i || 0,
|
||||
})),
|
||||
@@ -348,7 +373,7 @@ export const DetailModal = ({
|
||||
if (hasMaximum) {
|
||||
datasets.push({
|
||||
label: "Messwert Maximum",
|
||||
data: reduxData.map((entry) => ({
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.a || 0,
|
||||
})),
|
||||
@@ -364,7 +389,7 @@ export const DetailModal = ({
|
||||
if (hasAverage) {
|
||||
datasets.push({
|
||||
label: "Durchschnitt",
|
||||
data: reduxData.map((entry) => ({
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.g || 0,
|
||||
})),
|
||||
@@ -379,7 +404,7 @@ export const DetailModal = ({
|
||||
if (hasCurrent) {
|
||||
datasets.push({
|
||||
label: "Messwert",
|
||||
data: reduxData.map((entry) => ({
|
||||
data: filtered.map((entry) => ({
|
||||
x: new Date(entry.t).getTime(),
|
||||
y: entry.m || 0,
|
||||
})),
|
||||
@@ -429,7 +454,7 @@ export const DetailModal = ({
|
||||
<div className="absolute top-0 right-0 flex gap-3">
|
||||
<button
|
||||
onClick={toggleFullScreen}
|
||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
||||
className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] transition"
|
||||
>
|
||||
<i
|
||||
className={
|
||||
@@ -442,79 +467,21 @@ export const DetailModal = ({
|
||||
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-2xl text-gray-600 hover:text-gray-800"
|
||||
className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-danger)] transition"
|
||||
>
|
||||
<i className="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start gap-4 mb-4 flex-wrap">
|
||||
<DateRangePicker />
|
||||
<label className="font-medium">Zeitraum:</label>
|
||||
<Listbox value={zeitraum} onChange={setZeitraum}>
|
||||
<div className="relative w-48">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span>
|
||||
{
|
||||
{
|
||||
DIA0: "Alle Messwerte",
|
||||
DIA1: "Stündlich",
|
||||
DIA2: "Täglich",
|
||||
}[zeitraum]
|
||||
}
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<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 dark:bg-gray-800 shadow max-h-60 overflow-auto text-sm border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100">
|
||||
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className={({ selected, active }) =>
|
||||
`px-4 py-1 cursor-pointer ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200 dark:bg-gray-700"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{
|
||||
{
|
||||
DIA0: "Alle Messwerte",
|
||||
DIA1: "Stündlich",
|
||||
DIA2: "Täglich",
|
||||
}[option]
|
||||
}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
<button
|
||||
onClick={handleFetchData}
|
||||
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
|
||||
isLoading ? "cursor-wait" : ""
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Laden..." : "Daten laden"}
|
||||
</button>
|
||||
</div>
|
||||
<SystemChartActionBar
|
||||
zeitraum={zeitraum}
|
||||
setZeitraum={setZeitraum}
|
||||
onFetchData={handleFetchData}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<div className="h-[85%] bg-white dark:bg-gray-800 rounded shadow border border-gray-200 dark:border-gray-700 p-2">
|
||||
<div className="h-[85%] rounded shadow border p-2 bg-[var(--color-surface)] border-[var(--color-border)]">
|
||||
<Line ref={chartRef} data={chartData} options={chartOptions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
96
components/main/system/SystemChartActionBar.tsx
Normal file
96
components/main/system/SystemChartActionBar.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
// components/main/system/SystemChartActionBar.tsx
|
||||
import React from "react";
|
||||
import DateRangePicker from "@/components/common/DateRangePicker";
|
||||
import { Listbox } from "@headlessui/react";
|
||||
|
||||
type Props = {
|
||||
zeitraum: "DIA0" | "DIA1" | "DIA2";
|
||||
setZeitraum: (typ: "DIA0" | "DIA1" | "DIA2") => void;
|
||||
onFetchData: () => void;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const SystemChartActionBar: React.FC<Props> = ({
|
||||
zeitraum,
|
||||
setZeitraum,
|
||||
onFetchData,
|
||||
isLoading = false,
|
||||
className = "",
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-start gap-3 mb-4 flex-wrap ${className}`}
|
||||
>
|
||||
{/* DateRangePicker – nutzt globalen Redux-Slice */}
|
||||
<DateRangePicker compact />
|
||||
|
||||
{/* Zeitraum (DIA0/DIA1/DIA2) */}
|
||||
<label className="font-medium text-sm">Zeitraum:</label>
|
||||
<Listbox value={zeitraum} onChange={setZeitraum}>
|
||||
<div className="relative w-48">
|
||||
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
|
||||
<span>
|
||||
{
|
||||
{ DIA0: "Alle Messwerte", DIA1: "Stündlich", DIA2: "Täglich" }[
|
||||
zeitraum
|
||||
]
|
||||
}
|
||||
</span>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<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">
|
||||
{["DIA0", "DIA1", "DIA2"].map((option) => (
|
||||
<Listbox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
className={({ selected, active }) =>
|
||||
`px-4 py-1 cursor-pointer ${
|
||||
selected
|
||||
? "bg-littwin-blue text-white"
|
||||
: active
|
||||
? "bg-gray-200"
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{
|
||||
{
|
||||
DIA0: "Alle Messwerte",
|
||||
DIA1: "Stündlich",
|
||||
DIA2: "Täglich",
|
||||
}[option as "DIA0" | "DIA1" | "DIA2"]
|
||||
}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</div>
|
||||
</Listbox>
|
||||
|
||||
{/* Daten laden */}
|
||||
<button
|
||||
onClick={onFetchData}
|
||||
className={`px-4 py-1 bg-littwin-blue text-white rounded text-sm ${
|
||||
isLoading ? "cursor-wait opacity-70" : ""
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
aria-busy={isLoading}
|
||||
>
|
||||
{isLoading ? "Laden..." : "Daten laden"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemChartActionBar;
|
||||
@@ -10,13 +10,13 @@ export const SystemOverviewGrid = ({ voltages, onOpenDetail }: Props) => {
|
||||
const formatValue = (value: number) => value.toFixed(2);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
<div className="grid grid-cols-2 gap-4 mb-2">
|
||||
{Object.entries(voltages).map(([key, value]) => {
|
||||
const unit = key.includes("Temp") ? "\u00b0C" : "V";
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="p-4 border rounded shadow bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"
|
||||
className="p-4 border rounded shadow-sm bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-[var(--color-border)] text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/60 dark:hover:bg-[var(--color-surface-alt)]/30 transition"
|
||||
>
|
||||
<h2 className="font-semibold">{key}</h2>
|
||||
<p>
|
||||
|
||||
@@ -42,40 +42,37 @@ const SystemPage = () => {
|
||||
|
||||
const handleOpenDetail = (key: string) => {
|
||||
setSelectedKey(key);
|
||||
const handleOpenDetail = (key: string) => {
|
||||
setSelectedKey(key);
|
||||
setIsModalOpen(true);
|
||||
switch (key) {
|
||||
case "+5V":
|
||||
dispatch(getSystemspannung5VplusThunk(zeitraum));
|
||||
break;
|
||||
case "+15V":
|
||||
dispatch(getSystemspannung15VplusThunk(zeitraum));
|
||||
break;
|
||||
case "-15V":
|
||||
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
||||
break;
|
||||
case "-98V":
|
||||
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
||||
break;
|
||||
case "ADC Temp":
|
||||
dispatch(getTemperaturAdWandlerThunk(zeitraum));
|
||||
break;
|
||||
case "CPU Temp":
|
||||
dispatch(getTemperaturProzessorThunk(zeitraum));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
setIsModalOpen(true);
|
||||
switch (key) {
|
||||
case "+5V":
|
||||
dispatch(getSystemspannung5VplusThunk(zeitraum));
|
||||
break;
|
||||
case "+15V":
|
||||
dispatch(getSystemspannung15VplusThunk(zeitraum));
|
||||
break;
|
||||
case "-15V":
|
||||
dispatch(getSystemspannung15VminusThunk(zeitraum));
|
||||
break;
|
||||
case "-98V":
|
||||
dispatch(getSystemspannung98VminusThunk(zeitraum));
|
||||
break;
|
||||
case "ADC Temp":
|
||||
dispatch(getTemperaturAdWandlerThunk(zeitraum));
|
||||
break;
|
||||
case "CPU Temp":
|
||||
dispatch(getTemperaturProzessorThunk(zeitraum));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
const handleCloseDetail = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white dark:bg-gray-900">
|
||||
<h1 className="text-xl font-bold mb-4">
|
||||
<div className="p-4 bg-[var(--color-background)] text-[var(--color-fg)]">
|
||||
<h1 className="text-xl font-bold mb-4 tracking-wide">
|
||||
System Spannungen & Temperaturen
|
||||
</h1>
|
||||
|
||||
@@ -83,7 +80,7 @@ const SystemPage = () => {
|
||||
<div className="flex justify-center items-center h-[400px]">
|
||||
<div className="text-center">
|
||||
<ClipLoader size={50} color="#3B82F6" />
|
||||
<p className="mt-4 text-gray-500">
|
||||
<p className="mt-4 text-[var(--color-fg-muted)]">
|
||||
Lade Systemdaten … bitte warten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,12 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="bg-white dark:bg-gray-900 h-full">
|
||||
<aside className="h-full bg-[var(--color-surface)] dark:bg-[var(--color-surface)] border-r border-[var(--color-border)]">
|
||||
<nav className={`h-full flex-shrink-0 mt-16 ${className || "w-48"}`}>
|
||||
{menuItems.map((item) => (
|
||||
<div key={item.name}>
|
||||
{item.disabled ? (
|
||||
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-gray-400 dark:text-gray-600 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
||||
<div className="block px-4 py-2 mb-4 font-bold whitespace-nowrap text-[var(--color-fg-muted)] opacity-60 cursor-not-allowed text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg">
|
||||
{item.name}
|
||||
</div>
|
||||
) : (
|
||||
@@ -49,11 +49,13 @@ const Navigation: React.FC<NavigationProps> = ({ className }) => {
|
||||
href={formatPath(item.path)}
|
||||
prefetch={false}
|
||||
onClick={() => setActiveLink(item.path)}
|
||||
className={`block px-4 py-2 mb-4 font-bold whitespace-nowrap transition duration-300 text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg ${
|
||||
className={`block px-4 py-2 mb-4 font-semibold whitespace-nowrap transition duration-200 rounded-r-full pr-6 relative text-[1rem] sm:text-[1rem] md:text-[1rem] lg:text-[1rem] xl:text-sm 2xl:text-lg
|
||||
${
|
||||
activeLink.startsWith(item.path)
|
||||
? "bg-sky-500 text-white rounded-r-full xl:mr-4 xl:w-full dark:bg-sky-600 dark:text-white"
|
||||
: "text-black hover:bg-gray-200 rounded-r-full dark:text-gray-200 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
? "bg-[var(--color-accent)] text-white shadow-sm xl:mr-4 xl:w-full"
|
||||
: "text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-surface-alt)]/80 dark:hover:bg-[var(--color-surface-alt)]/40"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
|
||||
BIN
docs/Lastenheft/CPL V4 Lastenheft 03.09.2025.pdf
Normal file
BIN
docs/Lastenheft/CPL V4 Lastenheft 03.09.2025.pdf
Normal file
Binary file not shown.
26
docs/TODO.md
26
docs/TODO.md
@@ -79,3 +79,29 @@ in Rot, wenn Schleifenfehler ansteht
|
||||
# 15.08.2025
|
||||
|
||||
- [x] BUGFIX: Messkurven-Modal lädt jetzt automatisch die Kurve beim Öffnen, Dropdown ist auf 'Alle Messwerte' (DIA0) initialisiert, und Filter werden beim Schließen zurückgesetzt. Dateien: IsoChartView.tsx, LoopChartView.tsx
|
||||
|
||||
# 01.09.2025
|
||||
|
||||
- [x] TODO: In KÜs Display ISO 2 Nachkommastellen und RSL 3 Nachkommastellen
|
||||
- [ ] TODO: Schleife, Timer für jeder KÜ separate und nicht eine für alle, aktuell wird prozentzahl bei allen das gleiche angezeigt
|
||||
- [x] TODO: RSL starten in RSL Messung starten umbenennen
|
||||
- [x] TODO: TDR-Messung starten statt TDR aktivieren in ChartBar
|
||||
- [x] TODO: KÜ TDR-aktiviert alert entfernen
|
||||
- [ ] TODO: Systemdaten unter Detailansicht ein Verlaufsdiagramm hinzufügen mit Datumsauswahl
|
||||
- [ ] TODO: Playwright testen mit der Entwicklung
|
||||
|
||||
# Kai Schmidt:
|
||||
|
||||
# Folgende Erweiterung / Neuerungen:
|
||||
|
||||
[x] TODO: Messverlauf bei Systemwerten (Temperatur und Spannungen) mit Datumsauswahl
|
||||
|
||||
[x] TODO: Formatierung der Kabelüberwachungswerten in den visuellen Einschüben (Isowert mit Komma und 2 Nachkommastellen; RSL mit Komma und 3 Noachkommastellen) Nachkommastellen immer anzeigen und mit Nullen auffüllen.
|
||||
|
||||
[ ] TODO: Admin User nach einer Zeit von einer Stunde löschen (Cookie oder Local Storrage)
|
||||
|
||||
[ ] TODO: lange Modulnamen bei KÜ ermöglichen (48 Zeichen) bei Version ab V4.30. Laufschrift möglich?
|
||||
|
||||
[ ] TODO: Darkmode ermöglichen
|
||||
|
||||
[ ] TODO: Wenn im Browser Darkmode eingschaltet ist muss die Webseite erkennbar sein.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["*"],
|
||||
"@/redux/*": ["redux/*"],
|
||||
"@/utils/*": ["utils/*"],
|
||||
"@/components/*": ["components/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"components/**/*",
|
||||
"redux/**/*",
|
||||
"utils/**/*",
|
||||
"*.js",
|
||||
"*.ts",
|
||||
"*.jsx",
|
||||
"*.tsx"
|
||||
],
|
||||
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
win_da_state = [1, 1, 1, 1];
|
||||
win_da_bezeichnung = ["Ausgang2", "Ausgang2", "Ausgang3", "Ausgang4"];
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"win_da_state": [1, 1, 1, 1],
|
||||
"win_da_bezeichnung": ["Ausgang2", "Ausgang2", "Ausgang3", "Ausgang4"]
|
||||
}
|
||||
"win_da_state": [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"win_da_bezeichnung": [
|
||||
"Ausgang1",
|
||||
"Ausgang2",
|
||||
"Ausgang3",
|
||||
"Ausgang4"
|
||||
]
|
||||
}
|
||||
@@ -92,7 +92,7 @@ var win_kueLoopInterval = [
|
||||
//---------------------------------------------------
|
||||
//KÜ Modul Version soll /100 und davor V angezeigt werden z.B. 4.19V
|
||||
var win_kueVersion = [
|
||||
420, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
|
||||
420, 419, 419, 419, 419, 419, 419, 430, 419, 419, 419, 419, 419, 419, 419,
|
||||
419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419, 419,
|
||||
419, 419,
|
||||
];
|
||||
@@ -142,7 +142,7 @@ var win_kueName = [
|
||||
"Kabel 5",
|
||||
"Kabel 6",
|
||||
"Kabel 7",
|
||||
"Kabel 8",
|
||||
"Kabel_8 in Salzgitter bei Hannover",
|
||||
"Kabel 9",
|
||||
"Kabel 10",
|
||||
"Kabel 11",
|
||||
@@ -269,7 +269,12 @@ var tdrMeasurementEvent = [
|
||||
0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
//Event Abgleich
|
||||
var alignmentEvent = [
|
||||
var comparisonEvent = [
|
||||
// renamed from alignmentEvent
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
// expose for browser simulation
|
||||
if (typeof window !== "undefined") {
|
||||
window.comparisonEvent = comparisonEvent;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// /device-cgi-simulator/SERVICE/SystemMockData.js
|
||||
// /device-cgi-simulator/SERVICE/systemMockData.js
|
||||
var win_appVersion = "0.02";
|
||||
var win_deviceName = "CPLV4 Ismail Rastede";
|
||||
var win_mac1 = "0 48 86 81 46 143";
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"t": "2025-08-13 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 14,
|
||||
"g": 13.545
|
||||
},
|
||||
{
|
||||
"t": "2025-08-12 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 14,
|
||||
"g": 13.519
|
||||
},
|
||||
{
|
||||
"t": "2025-08-11 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-10 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 14,
|
||||
"g": 13.755
|
||||
},
|
||||
{
|
||||
"t": "2025-08-09 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.988
|
||||
},
|
||||
{
|
||||
"t": "2025-08-08 00:00:00",
|
||||
"i": 11.5,
|
||||
"a": 14,
|
||||
"g": 13.733
|
||||
},
|
||||
{
|
||||
"t": "2025-08-07 00:00:00",
|
||||
"i": 11.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-06 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-05 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.493
|
||||
},
|
||||
{
|
||||
"t": "2025-08-04 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-03 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-02 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-01 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 200,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-31 00:00:00",
|
||||
"i": 10,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-30 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-29 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-28 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-27 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-26 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 14,
|
||||
"g": 13.518
|
||||
},
|
||||
{
|
||||
"t": "2025-07-25 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-24 00:00:00",
|
||||
"i": 11.5,
|
||||
"a": 14,
|
||||
"g": 13.522
|
||||
},
|
||||
{
|
||||
"t": "2025-07-23 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-22 00:00:00",
|
||||
"i": 13,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-21 00:00:00",
|
||||
"i": 12,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-20 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-19 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-18 00:00:00",
|
||||
"i": 13,
|
||||
"a": 13.5,
|
||||
"g": 13.498
|
||||
},
|
||||
{
|
||||
"t": "2025-07-17 00:00:00",
|
||||
"i": 13.5,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-16 00:00:00",
|
||||
"i": 13,
|
||||
"a": 13.5,
|
||||
"g": 13.498
|
||||
},
|
||||
{
|
||||
"t": "2025-07-15 00:00:00",
|
||||
"i": 12.5,
|
||||
"a": 14,
|
||||
"g": 13.5
|
||||
},
|
||||
{
|
||||
"t": "2025-07-14 00:00:00",
|
||||
"i": 12,
|
||||
"a": 13.5,
|
||||
"g": 13.5
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"t": "2025-08-13 00:00:00",
|
||||
"i": -0.001,
|
||||
"a": 14.076,
|
||||
"g": 12.978
|
||||
},
|
||||
{
|
||||
"t": "2025-08-12 00:00:00",
|
||||
"i": 13.954,
|
||||
"a": 14.103,
|
||||
"g": 14.041
|
||||
},
|
||||
{
|
||||
"t": "2025-08-11 00:00:00",
|
||||
"i": 14.024,
|
||||
"a": 14.144,
|
||||
"g": 14.108
|
||||
},
|
||||
{
|
||||
"t": "2025-08-10 00:00:00",
|
||||
"i": 13.974,
|
||||
"a": 14.074,
|
||||
"g": 14.035
|
||||
},
|
||||
{
|
||||
"t": "2025-08-09 00:00:00",
|
||||
"i": 14.028,
|
||||
"a": 14.165,
|
||||
"g": 14.114
|
||||
},
|
||||
{
|
||||
"t": "2025-08-08 00:00:00",
|
||||
"i": 13.94,
|
||||
"a": 14.039,
|
||||
"g": 14.007
|
||||
},
|
||||
{
|
||||
"t": "2025-08-07 00:00:00",
|
||||
"i": 13.998,
|
||||
"a": 14.152,
|
||||
"g": 14.052
|
||||
},
|
||||
{
|
||||
"t": "2025-08-06 00:00:00",
|
||||
"i": 13.978,
|
||||
"a": 14.056,
|
||||
"g": 14.028
|
||||
},
|
||||
{
|
||||
"t": "2025-08-05 00:00:00",
|
||||
"i": 13.87,
|
||||
"a": 14.041,
|
||||
"g": 14.006
|
||||
},
|
||||
{
|
||||
"t": "2025-08-04 00:00:00",
|
||||
"i": 13.87,
|
||||
"a": 14.095,
|
||||
"g": 13.995
|
||||
},
|
||||
{
|
||||
"t": "2025-08-03 00:00:00",
|
||||
"i": 14.013,
|
||||
"a": 14.143,
|
||||
"g": 14.055
|
||||
},
|
||||
{
|
||||
"t": "2025-08-02 00:00:00",
|
||||
"i": 14.01,
|
||||
"a": 14.143,
|
||||
"g": 14.088
|
||||
},
|
||||
{
|
||||
"t": "2025-08-01 00:00:00",
|
||||
"i": 13.975,
|
||||
"a": 14.125,
|
||||
"g": 14.045
|
||||
},
|
||||
{
|
||||
"t": "2025-07-31 00:00:00",
|
||||
"i": 13.921,
|
||||
"a": 14.117,
|
||||
"g": 14.052
|
||||
},
|
||||
{
|
||||
"t": "2025-07-30 00:00:00",
|
||||
"i": 13.962,
|
||||
"a": 14.109,
|
||||
"g": 14.04
|
||||
},
|
||||
{
|
||||
"t": "2025-07-29 00:00:00",
|
||||
"i": 13.962,
|
||||
"a": 14.105,
|
||||
"g": 14.039
|
||||
},
|
||||
{
|
||||
"t": "2025-07-28 00:00:00",
|
||||
"i": 14.054,
|
||||
"a": 14.171,
|
||||
"g": 14.081
|
||||
},
|
||||
{
|
||||
"t": "2025-07-27 00:00:00",
|
||||
"i": 14.071,
|
||||
"a": 14.171,
|
||||
"g": 14.123
|
||||
},
|
||||
{
|
||||
"t": "2025-07-26 00:00:00",
|
||||
"i": 14.024,
|
||||
"a": 14.126,
|
||||
"g": 14.071
|
||||
},
|
||||
{
|
||||
"t": "2025-07-25 00:00:00",
|
||||
"i": 14.006,
|
||||
"a": 14.148,
|
||||
"g": 14.093
|
||||
},
|
||||
{
|
||||
"t": "2025-07-24 00:00:00",
|
||||
"i": -0.001,
|
||||
"a": 14.116,
|
||||
"g": 13.567
|
||||
},
|
||||
{
|
||||
"t": "2025-07-23 00:00:00",
|
||||
"i": 13.98,
|
||||
"a": 14.11,
|
||||
"g": 14.042
|
||||
},
|
||||
{
|
||||
"t": "2025-07-22 00:00:00",
|
||||
"i": 13.793,
|
||||
"a": 14.099,
|
||||
"g": 13.97
|
||||
},
|
||||
{
|
||||
"t": "2025-07-21 00:00:00",
|
||||
"i": 13.982,
|
||||
"a": 14.127,
|
||||
"g": 14.044
|
||||
},
|
||||
{
|
||||
"t": "2025-07-20 00:00:00",
|
||||
"i": 13.832,
|
||||
"a": 14.056,
|
||||
"g": 13.971
|
||||
},
|
||||
{
|
||||
"t": "2025-07-19 00:00:00",
|
||||
"i": 13.967,
|
||||
"a": 14.056,
|
||||
"g": 14.027
|
||||
},
|
||||
{
|
||||
"t": "2025-07-18 00:00:00",
|
||||
"i": 13.927,
|
||||
"a": 14.086,
|
||||
"g": 13.989
|
||||
},
|
||||
{
|
||||
"t": "2025-07-17 00:00:00",
|
||||
"i": 13.952,
|
||||
"a": 14.13,
|
||||
"g": 14.067
|
||||
},
|
||||
{
|
||||
"t": "2025-07-16 00:00:00",
|
||||
"i": 13.952,
|
||||
"a": 14.153,
|
||||
"g": 14.071
|
||||
},
|
||||
{
|
||||
"t": "2025-07-15 00:00:00",
|
||||
"i": 13.943,
|
||||
"a": 14.114,
|
||||
"g": 14.062
|
||||
},
|
||||
{
|
||||
"t": "2025-07-14 00:00:00",
|
||||
"i": 13.936,
|
||||
"a": 14.047,
|
||||
"g": 13.989
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"t": "2025-08-13 00:00:00",
|
||||
"i": 40,
|
||||
"a": 49,
|
||||
"g": 46.125
|
||||
},
|
||||
{
|
||||
"t": "2025-08-12 00:00:00",
|
||||
"i": 39.5,
|
||||
"a": 47.5,
|
||||
"g": 46.033
|
||||
},
|
||||
{
|
||||
"t": "2025-08-11 00:00:00",
|
||||
"i": 39.5,
|
||||
"a": 47,
|
||||
"g": 45.995
|
||||
},
|
||||
{
|
||||
"t": "2025-08-10 00:00:00",
|
||||
"i": 25.5,
|
||||
"a": 47,
|
||||
"g": 45.947
|
||||
},
|
||||
{
|
||||
"t": "2025-08-09 00:00:00",
|
||||
"i": 25.5,
|
||||
"a": 47,
|
||||
"g": 45.702
|
||||
},
|
||||
{
|
||||
"t": "2025-08-08 00:00:00",
|
||||
"i": 25.5,
|
||||
"a": 47,
|
||||
"g": 45.835
|
||||
},
|
||||
{
|
||||
"t": "2025-08-07 00:00:00",
|
||||
"i": 33.5,
|
||||
"a": 46.5,
|
||||
"g": 45.642
|
||||
},
|
||||
{
|
||||
"t": "2025-08-06 00:00:00",
|
||||
"i": 39,
|
||||
"a": 46.5,
|
||||
"g": 45.5
|
||||
},
|
||||
{
|
||||
"t": "2025-08-05 00:00:00",
|
||||
"i": 25,
|
||||
"a": 46.5,
|
||||
"g": 45.321
|
||||
},
|
||||
{
|
||||
"t": "2025-08-04 00:00:00",
|
||||
"i": 25,
|
||||
"a": 46.5,
|
||||
"g": 45.059
|
||||
},
|
||||
{
|
||||
"t": "2025-08-03 00:00:00",
|
||||
"i": 39,
|
||||
"a": 47,
|
||||
"g": 45.067
|
||||
},
|
||||
{
|
||||
"t": "2025-08-02 00:00:00",
|
||||
"i": 33,
|
||||
"a": 47.5,
|
||||
"g": 45.09
|
||||
},
|
||||
{
|
||||
"t": "2025-08-01 00:00:00",
|
||||
"i": 25,
|
||||
"a": 47.5,
|
||||
"g": 44.958
|
||||
},
|
||||
{
|
||||
"t": "2025-07-31 00:00:00",
|
||||
"i": 33,
|
||||
"a": 47,
|
||||
"g": 44.891
|
||||
},
|
||||
{
|
||||
"t": "2025-07-30 00:00:00",
|
||||
"i": 39,
|
||||
"a": 46.5,
|
||||
"g": 45.182
|
||||
},
|
||||
{
|
||||
"t": "2025-07-29 00:00:00",
|
||||
"i": 33,
|
||||
"a": 200,
|
||||
"g": 45.536
|
||||
},
|
||||
{
|
||||
"t": "2025-07-28 00:00:00",
|
||||
"i": 25,
|
||||
"a": 48,
|
||||
"g": 45.469
|
||||
},
|
||||
{
|
||||
"t": "2025-07-27 00:00:00",
|
||||
"i": 25.5,
|
||||
"a": 48,
|
||||
"g": 45.79
|
||||
},
|
||||
{
|
||||
"t": "2025-07-26 00:00:00",
|
||||
"i": 33,
|
||||
"a": 47,
|
||||
"g": 45.337
|
||||
},
|
||||
{
|
||||
"t": "2025-07-25 00:00:00",
|
||||
"i": 25,
|
||||
"a": 47.5,
|
||||
"g": 45.172
|
||||
},
|
||||
{
|
||||
"t": "2025-07-24 00:00:00",
|
||||
"i": 25,
|
||||
"a": 200,
|
||||
"g": 45.605
|
||||
},
|
||||
{
|
||||
"t": "2025-07-23 00:00:00",
|
||||
"i": 23,
|
||||
"a": 47.5,
|
||||
"g": 45.979
|
||||
},
|
||||
{
|
||||
"t": "2025-07-22 00:00:00",
|
||||
"i": 39,
|
||||
"a": 47,
|
||||
"g": 45.469
|
||||
},
|
||||
{
|
||||
"t": "2025-07-21 00:00:00",
|
||||
"i": 33.5,
|
||||
"a": 47.5,
|
||||
"g": 45.793
|
||||
},
|
||||
{
|
||||
"t": "2025-07-20 00:00:00",
|
||||
"i": 33.5,
|
||||
"a": 47.5,
|
||||
"g": 45.947
|
||||
},
|
||||
{
|
||||
"t": "2025-07-19 00:00:00",
|
||||
"i": 39,
|
||||
"a": 47,
|
||||
"g": 45.568
|
||||
},
|
||||
{
|
||||
"t": "2025-07-18 00:00:00",
|
||||
"i": 42.5,
|
||||
"a": 46.5,
|
||||
"g": 45.339
|
||||
},
|
||||
{
|
||||
"t": "2025-07-17 00:00:00",
|
||||
"i": 33.5,
|
||||
"a": 47,
|
||||
"g": 45.651
|
||||
},
|
||||
{
|
||||
"t": "2025-07-16 00:00:00",
|
||||
"i": 43,
|
||||
"a": 47,
|
||||
"g": 45.817
|
||||
},
|
||||
{
|
||||
"t": "2025-07-15 00:00:00",
|
||||
"i": 39.5,
|
||||
"a": 47.5,
|
||||
"g": 45.826
|
||||
},
|
||||
{
|
||||
"t": "2025-07-14 00:00:00",
|
||||
"i": 39,
|
||||
"a": 47,
|
||||
"g": 45.3
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"t": "2025-08-13 00:00:00",
|
||||
"i": -0.001,
|
||||
"a": 3.42,
|
||||
"g": 3.228
|
||||
},
|
||||
{
|
||||
"t": "2025-08-12 00:00:00",
|
||||
"i": 3.417,
|
||||
"a": 3.434,
|
||||
"g": 3.426
|
||||
},
|
||||
{
|
||||
"t": "2025-08-11 00:00:00",
|
||||
"i": 3.393,
|
||||
"a": 3.422,
|
||||
"g": 3.413
|
||||
},
|
||||
{
|
||||
"t": "2025-08-10 00:00:00",
|
||||
"i": 3.393,
|
||||
"a": 3.431,
|
||||
"g": 3.42
|
||||
},
|
||||
{
|
||||
"t": "2025-08-09 00:00:00",
|
||||
"i": 3.409,
|
||||
"a": 3.438,
|
||||
"g": 3.419
|
||||
},
|
||||
{
|
||||
"t": "2025-08-08 00:00:00",
|
||||
"i": 3.409,
|
||||
"a": 3.414,
|
||||
"g": 3.412
|
||||
},
|
||||
{
|
||||
"t": "2025-08-07 00:00:00",
|
||||
"i": 3.398,
|
||||
"a": 3.416,
|
||||
"g": 3.412
|
||||
},
|
||||
{
|
||||
"t": "2025-08-06 00:00:00",
|
||||
"i": 3.398,
|
||||
"a": 3.415,
|
||||
"g": 3.407
|
||||
},
|
||||
{
|
||||
"t": "2025-08-05 00:00:00",
|
||||
"i": 3.408,
|
||||
"a": 3.431,
|
||||
"g": 3.415
|
||||
},
|
||||
{
|
||||
"t": "2025-08-04 00:00:00",
|
||||
"i": 3.41,
|
||||
"a": 3.423,
|
||||
"g": 3.415
|
||||
},
|
||||
{
|
||||
"t": "2025-08-03 00:00:00",
|
||||
"i": 3.4,
|
||||
"a": 3.424,
|
||||
"g": 3.412
|
||||
},
|
||||
{
|
||||
"t": "2025-08-02 00:00:00",
|
||||
"i": 3.403,
|
||||
"a": 3.421,
|
||||
"g": 3.411
|
||||
},
|
||||
{
|
||||
"t": "2025-08-01 00:00:00",
|
||||
"i": 3.406,
|
||||
"a": 3.417,
|
||||
"g": 3.413
|
||||
},
|
||||
{
|
||||
"t": "2025-07-31 00:00:00",
|
||||
"i": 3.415,
|
||||
"a": 3.422,
|
||||
"g": 3.419
|
||||
},
|
||||
{
|
||||
"t": "2025-07-30 00:00:00",
|
||||
"i": 3.413,
|
||||
"a": 3.419,
|
||||
"g": 3.415
|
||||
},
|
||||
{
|
||||
"t": "2025-07-29 00:00:00",
|
||||
"i": 3.388,
|
||||
"a": 3.417,
|
||||
"g": 3.403
|
||||
},
|
||||
{
|
||||
"t": "2025-07-28 00:00:00",
|
||||
"i": 3.412,
|
||||
"a": 3.422,
|
||||
"g": 3.416
|
||||
},
|
||||
{
|
||||
"t": "2025-07-27 00:00:00",
|
||||
"i": 3.414,
|
||||
"a": 3.43,
|
||||
"g": 3.423
|
||||
},
|
||||
{
|
||||
"t": "2025-07-26 00:00:00",
|
||||
"i": 3.412,
|
||||
"a": 3.425,
|
||||
"g": 3.417
|
||||
},
|
||||
{
|
||||
"t": "2025-07-25 00:00:00",
|
||||
"i": 3.399,
|
||||
"a": 3.425,
|
||||
"g": 3.414
|
||||
},
|
||||
{
|
||||
"t": "2025-07-24 00:00:00",
|
||||
"i": 3.399,
|
||||
"a": 3.424,
|
||||
"g": 3.41
|
||||
},
|
||||
{
|
||||
"t": "2025-07-23 00:00:00",
|
||||
"i": 3.384,
|
||||
"a": 3.424,
|
||||
"g": 3.408
|
||||
},
|
||||
{
|
||||
"t": "2025-07-22 00:00:00",
|
||||
"i": 3.397,
|
||||
"a": 3.417,
|
||||
"g": 3.407
|
||||
},
|
||||
{
|
||||
"t": "2025-07-21 00:00:00",
|
||||
"i": 3.41,
|
||||
"a": 3.424,
|
||||
"g": 3.418
|
||||
},
|
||||
{
|
||||
"t": "2025-07-20 00:00:00",
|
||||
"i": 3.406,
|
||||
"a": 3.428,
|
||||
"g": 3.416
|
||||
},
|
||||
{
|
||||
"t": "2025-07-19 00:00:00",
|
||||
"i": 3.397,
|
||||
"a": 3.431,
|
||||
"g": 3.411
|
||||
},
|
||||
{
|
||||
"t": "2025-07-18 00:00:00",
|
||||
"i": 3.403,
|
||||
"a": 3.431,
|
||||
"g": 3.411
|
||||
},
|
||||
{
|
||||
"t": "2025-07-17 00:00:00",
|
||||
"i": 3.404,
|
||||
"a": 3.44,
|
||||
"g": 3.417
|
||||
},
|
||||
{
|
||||
"t": "2025-07-16 00:00:00",
|
||||
"i": 3.413,
|
||||
"a": 3.44,
|
||||
"g": 3.421
|
||||
},
|
||||
{
|
||||
"t": "2025-07-15 00:00:00",
|
||||
"i": 3.415,
|
||||
"a": 3.424,
|
||||
"g": 3.421
|
||||
},
|
||||
{
|
||||
"t": "2025-07-14 00:00:00",
|
||||
"i": 3.402,
|
||||
"a": 3.426,
|
||||
"g": 3.413
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"t": "2025-08-13 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-08-12 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-08-11 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-08-10 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-08-09 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-08-08 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10.005
|
||||
},
|
||||
{
|
||||
"t": "2025-08-07 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10.006
|
||||
},
|
||||
{
|
||||
"t": "2025-08-06 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10.005
|
||||
},
|
||||
{
|
||||
"t": "2025-08-05 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10.089
|
||||
},
|
||||
{
|
||||
"t": "2025-08-04 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 9.963
|
||||
},
|
||||
{
|
||||
"t": "2025-08-03 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10.006
|
||||
},
|
||||
{
|
||||
"t": "2025-08-02 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-08-01 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10,
|
||||
"g": 9.979
|
||||
},
|
||||
{
|
||||
"t": "2025-07-31 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-30 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-29 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-28 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10,
|
||||
"g": 9.943
|
||||
},
|
||||
{
|
||||
"t": "2025-07-27 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-26 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-25 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10,
|
||||
"g": 9.875
|
||||
},
|
||||
{
|
||||
"t": "2025-07-24 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-23 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-22 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10,
|
||||
"g": 9.896
|
||||
},
|
||||
{
|
||||
"t": "2025-07-21 00:00:00",
|
||||
"i": 9.5,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-20 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-19 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10.095
|
||||
},
|
||||
{
|
||||
"t": "2025-07-18 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10.062
|
||||
},
|
||||
{
|
||||
"t": "2025-07-17 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-16 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10
|
||||
},
|
||||
{
|
||||
"t": "2025-07-15 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10.174
|
||||
},
|
||||
{
|
||||
"t": "2025-07-14 00:00:00",
|
||||
"i": 10,
|
||||
"a": 10.5,
|
||||
"g": 10.135
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
[
|
||||
{
|
||||
"t": "2025-08-13 00:00:00",
|
||||
"i": 3.665,
|
||||
"a": 3.69,
|
||||
"g": 3.673
|
||||
},
|
||||
{
|
||||
"t": "2025-08-12 00:00:00",
|
||||
"i": 3.681,
|
||||
"a": 3.695,
|
||||
"g": 3.688
|
||||
},
|
||||
{
|
||||
"t": "2025-08-11 00:00:00",
|
||||
"i": 3.679,
|
||||
"a": 3.691,
|
||||
"g": 3.682
|
||||
},
|
||||
{
|
||||
"t": "2025-08-10 00:00:00",
|
||||
"i": 3.675,
|
||||
"a": 3.696,
|
||||
"g": 3.688
|
||||
},
|
||||
{
|
||||
"t": "2025-08-09 00:00:00",
|
||||
"i": 3.675,
|
||||
"a": 3.697,
|
||||
"g": 3.684
|
||||
},
|
||||
{
|
||||
"t": "2025-08-08 00:00:00",
|
||||
"i": 3.678,
|
||||
"a": 3.693,
|
||||
"g": 3.687
|
||||
},
|
||||
{
|
||||
"t": "2025-08-07 00:00:00",
|
||||
"i": 3.676,
|
||||
"a": 3.7,
|
||||
"g": 3.69
|
||||
},
|
||||
{
|
||||
"t": "2025-08-06 00:00:00",
|
||||
"i": 3.676,
|
||||
"a": 3.71,
|
||||
"g": 3.692
|
||||
},
|
||||
{
|
||||
"t": "2025-08-05 00:00:00",
|
||||
"i": 3.675,
|
||||
"a": 3.699,
|
||||
"g": 3.688
|
||||
},
|
||||
{
|
||||
"t": "2025-08-04 00:00:00",
|
||||
"i": 3.677,
|
||||
"a": 3.691,
|
||||
"g": 3.686
|
||||
},
|
||||
{
|
||||
"t": "2025-08-03 00:00:00",
|
||||
"i": 3.663,
|
||||
"a": 3.692,
|
||||
"g": 3.678
|
||||
},
|
||||
{
|
||||
"t": "2025-08-02 00:00:00",
|
||||
"i": 3.673,
|
||||
"a": 3.696,
|
||||
"g": 3.681
|
||||
},
|
||||
{
|
||||
"t": "2025-08-01 00:00:00",
|
||||
"i": 3.674,
|
||||
"a": 3.696,
|
||||
"g": 3.689
|
||||
},
|
||||
{
|
||||
"t": "2025-07-31 00:00:00",
|
||||
"i": 3.676,
|
||||
"a": 3.698,
|
||||
"g": 3.688
|
||||
},
|
||||
{
|
||||
"t": "2025-07-30 00:00:00",
|
||||
"i": 3.676,
|
||||
"a": 3.691,
|
||||
"g": 3.686
|
||||
},
|
||||
{
|
||||
"t": "2025-07-29 00:00:00",
|
||||
"i": 3.682,
|
||||
"a": 3.722,
|
||||
"g": 3.696
|
||||
},
|
||||
{
|
||||
"t": "2025-07-28 00:00:00",
|
||||
"i": 3.681,
|
||||
"a": 3.696,
|
||||
"g": 3.687
|
||||
},
|
||||
{
|
||||
"t": "2025-07-27 00:00:00",
|
||||
"i": 3.681,
|
||||
"a": 3.699,
|
||||
"g": 3.693
|
||||
},
|
||||
{
|
||||
"t": "2025-07-26 00:00:00",
|
||||
"i": 3.673,
|
||||
"a": 3.699,
|
||||
"g": 3.683
|
||||
},
|
||||
{
|
||||
"t": "2025-07-25 00:00:00",
|
||||
"i": 3.684,
|
||||
"a": 3.699,
|
||||
"g": 3.692
|
||||
},
|
||||
{
|
||||
"t": "2025-07-24 00:00:00",
|
||||
"i": 3.68,
|
||||
"a": 3.697,
|
||||
"g": 3.687
|
||||
},
|
||||
{
|
||||
"t": "2025-07-23 00:00:00",
|
||||
"i": 3.679,
|
||||
"a": 3.702,
|
||||
"g": 3.69
|
||||
},
|
||||
{
|
||||
"t": "2025-07-22 00:00:00",
|
||||
"i": 3.67,
|
||||
"a": 3.682,
|
||||
"g": 3.677
|
||||
},
|
||||
{
|
||||
"t": "2025-07-21 00:00:00",
|
||||
"i": 3.682,
|
||||
"a": 3.69,
|
||||
"g": 3.686
|
||||
},
|
||||
{
|
||||
"t": "2025-07-20 00:00:00",
|
||||
"i": 3.673,
|
||||
"a": 3.686,
|
||||
"g": 3.68
|
||||
},
|
||||
{
|
||||
"t": "2025-07-19 00:00:00",
|
||||
"i": 3.682,
|
||||
"a": 3.695,
|
||||
"g": 3.689
|
||||
},
|
||||
{
|
||||
"t": "2025-07-18 00:00:00",
|
||||
"i": 3.682,
|
||||
"a": 3.695,
|
||||
"g": 3.69
|
||||
},
|
||||
{
|
||||
"t": "2025-07-17 00:00:00",
|
||||
"i": 3.682,
|
||||
"a": 3.693,
|
||||
"g": 3.684
|
||||
},
|
||||
{
|
||||
"t": "2025-07-16 00:00:00",
|
||||
"i": 3.681,
|
||||
"a": 3.697,
|
||||
"g": 3.69
|
||||
},
|
||||
{
|
||||
"t": "2025-07-15 00:00:00",
|
||||
"i": 3.688,
|
||||
"a": 3.695,
|
||||
"g": 3.691
|
||||
},
|
||||
{
|
||||
"t": "2025-07-14 00:00:00",
|
||||
"i": 3.67,
|
||||
"a": 3.689,
|
||||
"g": 3.681
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user