updated hydrobuddy web interface
landing page + 1st configuration + overview when configured (hoffentlich lol) ltack kram rausgehauen (Testbench code)
This commit is contained in:
parent
57ad33718c
commit
7a3e2830a0
36 changed files with 5075 additions and 8077 deletions
381
pnpm-lock.yaml
generated
381
pnpm-lock.yaml
generated
|
|
@ -10,10 +10,10 @@ importers:
|
|||
dependencies:
|
||||
'@ant-design/charts':
|
||||
specifier: ^2.6.1
|
||||
version: 2.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
version: 2.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
'@astrojs/preact':
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1(@babel/core@7.28.0)(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(preact@10.26.9)(terser@5.43.1)(yaml@2.8.0)
|
||||
version: 4.1.1(@babel/core@7.28.0)(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(preact@10.26.9)(terser@5.48.0)(yaml@2.8.0)
|
||||
'@astrojs/ts-plugin':
|
||||
specifier: ^1.10.5
|
||||
version: 1.10.5
|
||||
|
|
@ -22,7 +22,7 @@ importers:
|
|||
version: 1.0.0(nanostores@1.0.1)(preact@10.26.9)
|
||||
'@playform/compress':
|
||||
specifier: ^0.1.9
|
||||
version: 0.1.9(@types/node@24.1.0)(jiti@1.21.7)(rollup@4.45.1)(typescript@5.8.3)(yaml@2.8.0)
|
||||
version: 0.1.9(@types/node@25.9.1)(jiti@1.21.7)(rollup@4.45.1)(typescript@5.8.3)(yaml@2.8.0)
|
||||
'@tabler/icons-preact':
|
||||
specifier: ^3.34.1
|
||||
version: 3.34.1(preact@10.26.9)
|
||||
|
|
@ -31,13 +31,13 @@ importers:
|
|||
version: 1.0.10
|
||||
astro:
|
||||
specifier: ^5.14.7
|
||||
version: 5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0)
|
||||
version: 5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0)
|
||||
astro-compressor:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
astro-min:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(astro@5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0))
|
||||
version: 1.3.1(astro@5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0))
|
||||
chart.js:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0
|
||||
|
|
@ -59,7 +59,7 @@ importers:
|
|||
devDependencies:
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(astro@5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0))(tailwindcss@3.4.17)
|
||||
version: 6.0.2(astro@5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0))(tailwindcss@3.4.17)
|
||||
daisyui:
|
||||
specifier: ^4.12.24
|
||||
version: 4.12.24(postcss@8.5.6)
|
||||
|
|
@ -764,6 +764,9 @@ packages:
|
|||
'@jridgewell/gen-mapping@0.3.12':
|
||||
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
@ -771,6 +774,9 @@ packages:
|
|||
'@jridgewell/source-map@0.3.10':
|
||||
resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4':
|
||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
||||
|
||||
|
|
@ -780,6 +786,9 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.29':
|
||||
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@kurkle/color@0.3.4':
|
||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||
|
||||
|
|
@ -1095,6 +1104,9 @@ packages:
|
|||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/fontkit@2.0.8':
|
||||
resolution: {integrity: sha512-wN+8bYxIpJf+5oZdrdtaX04qUuWHcKxcDEgRS9Qm9ZClSHjzEn13SxUC+5eRM+4yXIeTYk8mTzLAWGF64847ew==}
|
||||
|
||||
|
|
@ -1125,6 +1137,9 @@ packages:
|
|||
'@types/node@24.1.0':
|
||||
resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==}
|
||||
|
||||
'@types/node@25.9.1':
|
||||
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
|
||||
|
||||
'@types/stylis@4.2.5':
|
||||
resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==}
|
||||
|
||||
|
|
@ -1205,6 +1220,11 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
|
|
@ -1218,8 +1238,8 @@ packages:
|
|||
peerDependencies:
|
||||
ajv: ^8.8.2
|
||||
|
||||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
ajv@8.20.0:
|
||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
|
@ -1301,6 +1321,11 @@ packages:
|
|||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.10.32:
|
||||
resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
big.js@5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
|
||||
|
|
@ -1330,6 +1355,11 @@ packages:
|
|||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
browserslist@4.28.2:
|
||||
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
bubblesets-js@2.3.4:
|
||||
resolution: {integrity: sha512-DyMjHmpkS2+xcFNtyN00apJYL3ESdp9fTrkDr5+9Qg/GPqFmcWgGsK1akZnttE1XFxJ/VMy4DNNGMGYtmFp1Sg==}
|
||||
|
||||
|
|
@ -1361,6 +1391,9 @@ packages:
|
|||
caniuse-lite@1.0.30001727:
|
||||
resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==}
|
||||
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
|
|
@ -1733,6 +1766,9 @@ packages:
|
|||
electron-to-chromium@1.5.190:
|
||||
resolution: {integrity: sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==}
|
||||
|
||||
electron-to-chromium@1.5.361:
|
||||
resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==}
|
||||
|
||||
emoji-regex@10.4.0:
|
||||
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
|
||||
|
||||
|
|
@ -1746,8 +1782,8 @@ packages:
|
|||
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
enhanced-resolve@5.18.2:
|
||||
resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==}
|
||||
enhanced-resolve@5.22.0:
|
||||
resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
entities@4.5.0:
|
||||
|
|
@ -1834,8 +1870,8 @@ packages:
|
|||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
||||
fast-uri@3.0.6:
|
||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||
fast-uri@3.1.2:
|
||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||
|
||||
fastparse@1.1.2:
|
||||
resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==}
|
||||
|
|
@ -2165,8 +2201,8 @@ packages:
|
|||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
loader-runner@4.3.0:
|
||||
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
|
||||
loader-runner@4.3.2:
|
||||
resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==}
|
||||
engines: {node: '>=6.11.5'}
|
||||
|
||||
loader-utils@2.0.4:
|
||||
|
|
@ -2433,6 +2469,10 @@ packages:
|
|||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
node-releases@2.0.46:
|
||||
resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2613,9 +2653,6 @@ packages:
|
|||
radix3@1.1.2:
|
||||
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
rbush@3.0.1:
|
||||
resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==}
|
||||
|
||||
|
|
@ -2723,17 +2760,14 @@ packages:
|
|||
rw@1.3.3:
|
||||
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
schema-utils@4.3.2:
|
||||
resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==}
|
||||
schema-utils@4.3.3:
|
||||
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
semver@6.3.1:
|
||||
|
|
@ -2745,9 +2779,6 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
serialize-javascript@6.0.2:
|
||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||
|
||||
shallowequal@1.1.0:
|
||||
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
||||
|
||||
|
|
@ -2872,23 +2903,50 @@ packages:
|
|||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
tapable@2.2.2:
|
||||
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
||||
tapable@2.3.3:
|
||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
terser-webpack-plugin@5.3.14:
|
||||
resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==}
|
||||
terser-webpack-plugin@5.6.0:
|
||||
resolution: {integrity: sha512-Eum+5ajkaOhf5KbM26osvv21kLD7BaGqQ1UA4Ami4arYwylmGUQTgHFpHDdmJod1q4QXa66p0to/FBKID+J1vA==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
peerDependencies:
|
||||
'@minify-html/node': '*'
|
||||
'@swc/core': '*'
|
||||
'@swc/css': '*'
|
||||
'@swc/html': '*'
|
||||
clean-css: '*'
|
||||
cssnano: '*'
|
||||
csso: '*'
|
||||
esbuild: '*'
|
||||
html-minifier-terser: '*'
|
||||
lightningcss: '*'
|
||||
postcss: '*'
|
||||
uglify-js: '*'
|
||||
webpack: ^5.1.0
|
||||
peerDependenciesMeta:
|
||||
'@minify-html/node':
|
||||
optional: true
|
||||
'@swc/core':
|
||||
optional: true
|
||||
'@swc/css':
|
||||
optional: true
|
||||
'@swc/html':
|
||||
optional: true
|
||||
clean-css:
|
||||
optional: true
|
||||
cssnano:
|
||||
optional: true
|
||||
csso:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
html-minifier-terser:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
uglify-js:
|
||||
optional: true
|
||||
|
||||
|
|
@ -2897,8 +2955,8 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
terser@5.43.1:
|
||||
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
|
||||
terser@5.48.0:
|
||||
resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -2975,6 +3033,9 @@ packages:
|
|||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
undici-types@7.8.0:
|
||||
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||
|
||||
|
|
@ -3085,6 +3146,12 @@ packages:
|
|||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
utf-8-validate@5.0.10:
|
||||
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
|
||||
engines: {node: '>=6.14.2'}
|
||||
|
|
@ -3160,15 +3227,15 @@ packages:
|
|||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
watchpack@2.4.4:
|
||||
resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==}
|
||||
watchpack@2.5.1:
|
||||
resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
webpack-sources@3.3.3:
|
||||
resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==}
|
||||
webpack-sources@3.5.0:
|
||||
resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
webpack@5.99.9:
|
||||
|
|
@ -3289,9 +3356,9 @@ snapshots:
|
|||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@ant-design/charts@2.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9))':
|
||||
'@ant-design/charts@2.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))':
|
||||
dependencies:
|
||||
'@ant-design/graphs': 2.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@ant-design/graphs': 2.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
'@ant-design/plots': 2.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
lodash: 4.17.21
|
||||
react: 19.1.0
|
||||
|
|
@ -3299,12 +3366,12 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- workerize-loader
|
||||
|
||||
'@ant-design/graphs@2.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9))':
|
||||
'@ant-design/graphs@2.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))':
|
||||
dependencies:
|
||||
'@ant-design/charts-util': 0.0.1-alpha.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@antv/g6': 5.0.49(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@antv/g6-extension-react': 0.2.4(@antv/g6@5.0.49(workerize-loader@2.0.2(webpack@5.99.9)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@antv/graphin': 3.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@antv/g6': 5.0.49(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
'@antv/g6-extension-react': 0.2.4(@antv/g6@5.0.49(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6))))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@antv/graphin': 3.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
lodash: 4.17.21
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
|
@ -3501,15 +3568,15 @@ snapshots:
|
|||
flru: 1.0.2
|
||||
pdfast: 0.2.0
|
||||
|
||||
'@antv/g6-extension-react@0.2.4(@antv/g6@5.0.49(workerize-loader@2.0.2(webpack@5.99.9)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
'@antv/g6-extension-react@0.2.4(@antv/g6@5.0.49(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6))))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@antv/g': 6.1.27
|
||||
'@antv/g-svg': 2.0.41
|
||||
'@antv/g6': 5.0.49(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@antv/g6': 5.0.49(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@antv/g6@5.0.49(workerize-loader@2.0.2(webpack@5.99.9))':
|
||||
'@antv/g6@5.0.49(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))':
|
||||
dependencies:
|
||||
'@antv/algorithm': 0.1.26
|
||||
'@antv/component': 2.1.5
|
||||
|
|
@ -3519,7 +3586,7 @@ snapshots:
|
|||
'@antv/g-plugin-dragndrop': 2.0.37
|
||||
'@antv/graphlib': 2.0.4
|
||||
'@antv/hierarchy': 0.6.14
|
||||
'@antv/layout': 1.2.14-beta.9(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@antv/layout': 1.2.14-beta.9(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
'@antv/util': 3.3.11
|
||||
bubblesets-js: 2.3.4
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -3533,9 +3600,9 @@ snapshots:
|
|||
'@antv/g-web-animations-api': 2.1.27
|
||||
'@babel/runtime': 7.27.6
|
||||
|
||||
'@antv/graphin@3.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9))':
|
||||
'@antv/graphin@3.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))':
|
||||
dependencies:
|
||||
'@antv/g6': 5.0.49(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@antv/g6': 5.0.49(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -3547,12 +3614,12 @@ snapshots:
|
|||
|
||||
'@antv/hierarchy@0.6.14': {}
|
||||
|
||||
'@antv/layout@1.2.14-beta.9(workerize-loader@2.0.2(webpack@5.99.9))':
|
||||
'@antv/layout@1.2.14-beta.9(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))':
|
||||
dependencies:
|
||||
'@antv/event-emitter': 0.1.3
|
||||
'@antv/graphlib': 2.0.4
|
||||
'@antv/util': 3.3.11
|
||||
'@naoak/workerize-transferable': 0.1.0(workerize-loader@2.0.2(webpack@5.99.9))
|
||||
'@naoak/workerize-transferable': 0.1.0(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))
|
||||
comlink: 4.4.2
|
||||
d3-force: 3.0.0
|
||||
d3-force-3d: 3.0.6
|
||||
|
|
@ -3655,13 +3722,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@astrojs/preact@4.1.1(@babel/core@7.28.0)(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(preact@10.26.9)(terser@5.43.1)(yaml@2.8.0)':
|
||||
'@astrojs/preact@4.1.1(@babel/core@7.28.0)(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(preact@10.26.9)(terser@5.48.0)(yaml@2.8.0)':
|
||||
dependencies:
|
||||
'@preact/preset-vite': 2.10.2(@babel/core@7.28.0)(preact@10.26.9)(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))
|
||||
'@preact/preset-vite': 2.10.2(@babel/core@7.28.0)(preact@10.26.9)(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))
|
||||
'@preact/signals': 2.3.2(preact@10.26.9)
|
||||
preact: 10.26.9
|
||||
preact-render-to-string: 6.6.2(preact@10.26.9)
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@types/node'
|
||||
|
|
@ -3681,9 +3748,9 @@ snapshots:
|
|||
dependencies:
|
||||
prismjs: 1.30.0
|
||||
|
||||
'@astrojs/tailwind@6.0.2(astro@5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0))(tailwindcss@3.4.17)':
|
||||
'@astrojs/tailwind@6.0.2(astro@5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0))(tailwindcss@3.4.17)':
|
||||
dependencies:
|
||||
astro: 5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0)
|
||||
astro: 5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0)
|
||||
autoprefixer: 10.4.21(postcss@8.5.6)
|
||||
postcss: 8.5.6
|
||||
postcss-load-config: 4.0.2(postcss@8.5.6)
|
||||
|
|
@ -4126,6 +4193,11 @@ snapshots:
|
|||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.10':
|
||||
|
|
@ -4133,6 +4205,11 @@ snapshots:
|
|||
'@jridgewell/gen-mapping': 0.3.12
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
|
@ -4142,6 +4219,11 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@minify-html/node-darwin-x64@0.15.0':
|
||||
|
|
@ -4158,9 +4240,9 @@ snapshots:
|
|||
nanostores: 1.0.1
|
||||
preact: 10.26.9
|
||||
|
||||
'@naoak/workerize-transferable@0.1.0(workerize-loader@2.0.2(webpack@5.99.9))':
|
||||
'@naoak/workerize-transferable@0.1.0(workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)))':
|
||||
dependencies:
|
||||
workerize-loader: 2.0.2(webpack@5.99.9)
|
||||
workerize-loader: 2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6))
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
|
|
@ -4179,12 +4261,12 @@ snapshots:
|
|||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playform/compress@0.1.9(@types/node@24.1.0)(jiti@1.21.7)(rollup@4.45.1)(typescript@5.8.3)(yaml@2.8.0)':
|
||||
'@playform/compress@0.1.9(@types/node@25.9.1)(jiti@1.21.7)(rollup@4.45.1)(typescript@5.8.3)(yaml@2.8.0)':
|
||||
dependencies:
|
||||
'@playform/pipe': 0.1.3
|
||||
'@types/csso': 5.0.4
|
||||
'@types/html-minifier-terser': 7.0.2
|
||||
astro: 5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0)
|
||||
astro: 5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0)
|
||||
commander: 13.1.0
|
||||
csso: 5.0.5
|
||||
deepmerge-ts: 7.1.5
|
||||
|
|
@ -4234,18 +4316,18 @@ snapshots:
|
|||
deepmerge-ts: 7.1.5
|
||||
fast-glob: 3.3.3
|
||||
|
||||
'@preact/preset-vite@2.10.2(@babel/core@7.28.0)(preact@10.26.9)(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))':
|
||||
'@preact/preset-vite@2.10.2(@babel/core@7.28.0)(preact@10.26.9)(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0)
|
||||
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.0)
|
||||
'@prefresh/vite': 2.4.8(preact@10.26.9)(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))
|
||||
'@prefresh/vite': 2.4.8(preact@10.26.9)(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.0)
|
||||
debug: 4.4.1
|
||||
picocolors: 1.1.1
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)
|
||||
vite-prerender-plugin: 0.5.11(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)
|
||||
vite-prerender-plugin: 0.5.11(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))
|
||||
transitivePeerDependencies:
|
||||
- preact
|
||||
- supports-color
|
||||
|
|
@ -4265,7 +4347,7 @@ snapshots:
|
|||
|
||||
'@prefresh/utils@1.2.1': {}
|
||||
|
||||
'@prefresh/vite@2.4.8(preact@10.26.9)(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))':
|
||||
'@prefresh/vite@2.4.8(preact@10.26.9)(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
'@prefresh/babel-plugin': 0.5.2
|
||||
|
|
@ -4273,7 +4355,7 @@ snapshots:
|
|||
'@prefresh/utils': 1.2.1
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
preact: 10.26.9
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -4463,15 +4545,17 @@ snapshots:
|
|||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
'@types/estree': 1.0.8
|
||||
'@types/estree': 1.0.9
|
||||
|
||||
'@types/eslint@9.6.1':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
'@types/estree': 1.0.9
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/fontkit@2.0.8':
|
||||
dependencies:
|
||||
'@types/node': 24.1.0
|
||||
|
|
@ -4504,6 +4588,10 @@ snapshots:
|
|||
dependencies:
|
||||
undici-types: 7.8.0
|
||||
|
||||
'@types/node@25.9.1':
|
||||
dependencies:
|
||||
undici-types: 7.24.6
|
||||
|
||||
'@types/stylis@4.2.5': {}
|
||||
|
||||
'@types/trusted-types@2.0.7':
|
||||
|
|
@ -4611,19 +4699,21 @@ snapshots:
|
|||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.20.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
ajv: 8.20.0
|
||||
|
||||
ajv-keywords@5.1.0(ajv@8.17.1):
|
||||
ajv-keywords@5.1.0(ajv@8.20.0):
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
ajv: 8.20.0
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
ajv@8.17.1:
|
||||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.0.6
|
||||
fast-uri: 3.1.2
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
|
|
@ -4658,13 +4748,13 @@ snapshots:
|
|||
|
||||
astro-compressor@1.0.0: {}
|
||||
|
||||
astro-min@1.3.1(astro@5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0)):
|
||||
astro-min@1.3.1(astro@5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0)):
|
||||
dependencies:
|
||||
'@redwerkz/html-min': 0.15.1
|
||||
astro: 5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0)
|
||||
astro: 5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0)
|
||||
svgo: 3.3.2
|
||||
|
||||
astro@5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0):
|
||||
astro@5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.39.0)(typescript@5.8.3)(yaml@2.8.0):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.12.2
|
||||
'@astrojs/internal-helpers': 0.7.4
|
||||
|
|
@ -4720,8 +4810,8 @@ snapshots:
|
|||
unist-util-visit: 5.0.0
|
||||
unstorage: 1.17.1
|
||||
vfile: 6.0.3
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.39.0)(yaml@2.8.0)
|
||||
vitefu: 1.1.1(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.39.0)(yaml@2.8.0)
|
||||
vitefu: 1.1.1(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
|
|
@ -4765,7 +4855,7 @@ snapshots:
|
|||
- uploadthing
|
||||
- yaml
|
||||
|
||||
astro@5.14.7(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.43.1)(typescript@5.8.3)(yaml@2.8.0):
|
||||
astro@5.14.7(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(rollup@4.45.1)(terser@5.48.0)(typescript@5.8.3)(yaml@2.8.0):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.12.2
|
||||
'@astrojs/internal-helpers': 0.7.4
|
||||
|
|
@ -4821,8 +4911,8 @@ snapshots:
|
|||
unist-util-visit: 5.0.0
|
||||
unstorage: 1.17.1
|
||||
vfile: 6.0.3
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)
|
||||
vitefu: 1.1.1(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0))
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)
|
||||
vitefu: 1.1.1(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
|
|
@ -4890,6 +4980,8 @@ snapshots:
|
|||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.10.32: {}
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
|
@ -4926,6 +5018,14 @@ snapshots:
|
|||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.1)
|
||||
|
||||
browserslist@4.28.2:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.10.32
|
||||
caniuse-lite: 1.0.30001793
|
||||
electron-to-chromium: 1.5.361
|
||||
node-releases: 2.0.46
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
||||
|
||||
bubblesets-js@2.3.4: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
|
@ -4951,6 +5051,8 @@ snapshots:
|
|||
|
||||
caniuse-lite@1.0.30001727: {}
|
||||
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chalk@5.4.1: {}
|
||||
|
|
@ -5286,6 +5388,8 @@ snapshots:
|
|||
|
||||
electron-to-chromium@1.5.190: {}
|
||||
|
||||
electron-to-chromium@1.5.361: {}
|
||||
|
||||
emoji-regex@10.4.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
|
@ -5294,10 +5398,10 @@ snapshots:
|
|||
|
||||
emojis-list@3.0.0: {}
|
||||
|
||||
enhanced-resolve@5.18.2:
|
||||
enhanced-resolve@5.22.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.2
|
||||
tapable: 2.3.3
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
|
|
@ -5407,7 +5511,7 @@ snapshots:
|
|||
merge2: 1.4.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
fast-uri@3.0.6: {}
|
||||
fast-uri@3.1.2: {}
|
||||
|
||||
fastparse@1.1.2: {}
|
||||
|
||||
|
|
@ -5670,7 +5774,7 @@ snapshots:
|
|||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 24.1.0
|
||||
'@types/node': 25.9.1
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
|
|
@ -5745,7 +5849,7 @@ snapshots:
|
|||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
loader-runner@4.3.0: {}
|
||||
loader-runner@4.3.2: {}
|
||||
|
||||
loader-utils@2.0.4:
|
||||
dependencies:
|
||||
|
|
@ -6188,6 +6292,8 @@ snapshots:
|
|||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
node-releases@2.0.46: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
|
@ -6352,10 +6458,6 @@ snapshots:
|
|||
|
||||
radix3@1.1.2: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
rbush@3.0.1:
|
||||
dependencies:
|
||||
quickselect: 2.0.0
|
||||
|
|
@ -6526,27 +6628,21 @@ snapshots:
|
|||
|
||||
rw@1.3.3: {}
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.26.0: {}
|
||||
|
||||
schema-utils@4.3.2:
|
||||
schema-utils@4.3.3:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
ajv: 8.17.1
|
||||
ajv-formats: 2.1.1(ajv@8.17.1)
|
||||
ajv-keywords: 5.1.0(ajv@8.17.1)
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 2.1.1(ajv@8.20.0)
|
||||
ajv-keywords: 5.1.0(ajv@8.20.0)
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
serialize-javascript@6.0.2:
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
|
||||
shallowequal@1.1.0: {}
|
||||
|
||||
sharp@0.33.5:
|
||||
|
|
@ -6759,16 +6855,18 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
tapable@2.2.2: {}
|
||||
tapable@2.3.3: {}
|
||||
|
||||
terser-webpack-plugin@5.3.14(webpack@5.99.9):
|
||||
terser-webpack-plugin@5.6.0(lightningcss@1.29.3)(postcss@8.5.6)(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.2
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.43.1
|
||||
webpack: 5.99.9
|
||||
schema-utils: 4.3.3
|
||||
terser: 5.48.0
|
||||
webpack: 5.99.9(lightningcss@1.29.3)(postcss@8.5.6)
|
||||
optionalDependencies:
|
||||
lightningcss: 1.29.3
|
||||
postcss: 8.5.6
|
||||
|
||||
terser@5.39.0:
|
||||
dependencies:
|
||||
|
|
@ -6777,10 +6875,10 @@ snapshots:
|
|||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
terser@5.43.1:
|
||||
terser@5.48.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.10
|
||||
acorn: 8.15.0
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.16.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
|
|
@ -6837,6 +6935,8 @@ snapshots:
|
|||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
undici-types@7.8.0: {}
|
||||
|
||||
unicode-properties@1.4.1:
|
||||
|
|
@ -6924,6 +7024,12 @@ snapshots:
|
|||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
dependencies:
|
||||
browserslist: 4.28.2
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
utf-8-validate@5.0.10:
|
||||
dependencies:
|
||||
node-gyp-build: 4.8.4
|
||||
|
|
@ -6945,7 +7051,7 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.2
|
||||
|
||||
vite-prerender-plugin@0.5.11(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)):
|
||||
vite-prerender-plugin@0.5.11(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)):
|
||||
dependencies:
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.17
|
||||
|
|
@ -6953,9 +7059,9 @@ snapshots:
|
|||
simple-code-frame: 1.3.0
|
||||
source-map: 0.7.4
|
||||
stack-trace: 1.0.0-pre2
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)
|
||||
|
||||
vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.39.0)(yaml@2.8.0):
|
||||
vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.39.0)(yaml@2.8.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.8
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
|
|
@ -6964,14 +7070,14 @@ snapshots:
|
|||
rollup: 4.45.1
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 24.1.0
|
||||
'@types/node': 25.9.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
lightningcss: 1.29.3
|
||||
terser: 5.39.0
|
||||
yaml: 2.8.0
|
||||
|
||||
vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0):
|
||||
vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.8
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
|
|
@ -6980,59 +7086,68 @@ snapshots:
|
|||
rollup: 4.45.1
|
||||
tinyglobby: 0.2.14
|
||||
optionalDependencies:
|
||||
'@types/node': 24.1.0
|
||||
'@types/node': 25.9.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
lightningcss: 1.29.3
|
||||
terser: 5.43.1
|
||||
terser: 5.48.0
|
||||
yaml: 2.8.0
|
||||
|
||||
vitefu@1.1.1(vite@6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)):
|
||||
vitefu@1.1.1(vite@6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)):
|
||||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@24.1.0)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.43.1)(yaml@2.8.0)
|
||||
vite: 6.4.1(@types/node@25.9.1)(jiti@1.21.7)(lightningcss@1.29.3)(terser@5.48.0)(yaml@2.8.0)
|
||||
|
||||
vscode-languageserver-textdocument@1.0.12: {}
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
watchpack@2.4.4:
|
||||
watchpack@2.5.1:
|
||||
dependencies:
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
webpack-sources@3.3.3: {}
|
||||
webpack-sources@3.5.0: {}
|
||||
|
||||
webpack@5.99.9:
|
||||
webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
'@types/estree': 1.0.9
|
||||
'@types/json-schema': 7.0.15
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
browserslist: 4.25.1
|
||||
acorn: 8.16.0
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.2
|
||||
enhanced-resolve: 5.22.0
|
||||
es-module-lexer: 1.7.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
loader-runner: 4.3.0
|
||||
loader-runner: 4.3.2
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.2
|
||||
tapable: 2.2.2
|
||||
terser-webpack-plugin: 5.3.14(webpack@5.99.9)
|
||||
watchpack: 2.4.4
|
||||
webpack-sources: 3.3.3
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.3
|
||||
terser-webpack-plugin: 5.6.0(lightningcss@1.29.3)(postcss@8.5.6)(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6))
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.5.0
|
||||
transitivePeerDependencies:
|
||||
- '@minify-html/node'
|
||||
- '@swc/core'
|
||||
- '@swc/css'
|
||||
- '@swc/html'
|
||||
- clean-css
|
||||
- cssnano
|
||||
- csso
|
||||
- esbuild
|
||||
- html-minifier-terser
|
||||
- lightningcss
|
||||
- postcss
|
||||
- uglify-js
|
||||
|
||||
websocket@1.0.35:
|
||||
|
|
@ -7056,10 +7171,10 @@ snapshots:
|
|||
dependencies:
|
||||
string-width: 7.2.0
|
||||
|
||||
workerize-loader@2.0.2(webpack@5.99.9):
|
||||
workerize-loader@2.0.2(webpack@5.99.9(lightningcss@1.29.3)(postcss@8.5.6)):
|
||||
dependencies:
|
||||
loader-utils: 2.0.4
|
||||
webpack: 5.99.9
|
||||
webpack: 5.99.9(lightningcss@1.29.3)(postcss@8.5.6)
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
|
|
|
|||
209
src/components/AccessPointConfigModal.astro
Normal file
209
src/components/AccessPointConfigModal.astro
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
---
|
||||
import IconEye from "../icons/IconEye.astro";
|
||||
import IconEyeClosed from "../icons/IconEyeClosed.astro";
|
||||
---
|
||||
|
||||
<dialog id="ap-config-modal" class="modal modal-middle">
|
||||
<div class="modal-box w-11/12 sm:w-6/12 max-w-5xl flex flex-col">
|
||||
<h1 class="text-lg font-bold">Access Point Configuration</h1>
|
||||
<div class="divider divider-primary"></div>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
|
||||
<!-- SSID -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-semibold ml-1">Network SSID</span>
|
||||
<label class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary" id="ap-ssid-wrap">
|
||||
<input type="text" class="w-full border-none outline-none bg-transparent" id="ap-ssid" placeholder="AP-Name" />
|
||||
</label>
|
||||
<p id="ap-ssid-error" class="text-sm text-error hidden"></p>
|
||||
<span class="text-sm opacity-60 ml-1">Name of your WiFi hotspot</span>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-semibold ml-1">Password</span>
|
||||
<div class="relative">
|
||||
<label class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary pr-10" id="ap-password-wrap">
|
||||
<input type="password" class="w-full border-none outline-none bg-transparent" id="ap-password" placeholder="Leave empty for open network" />
|
||||
</label>
|
||||
<button type="button" id="toggle-ap-password-btn" class="absolute right-3 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs p-1 min-h-0 h-7 hover:bg-primary hover:text-primary-content">
|
||||
<span id="ap-eye-icon-open">
|
||||
<IconEye size={20} class="opacity-70 hover:opacity-100" />
|
||||
</span>
|
||||
<span id="ap-eye-icon-closed" class="hidden">
|
||||
<IconEyeClosed size={20} class="opacity-70 hover:opacity-100" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p id="ap-password-error" class="text-sm text-error hidden"></p>
|
||||
<span class="text-sm opacity-60 ml-1">Leave this empty for an open (unsecured) network or set a Password (min. 8 symbols)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-semibold ml-1">WiFi Channel</span>
|
||||
<label class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary p-0 overflow-hidden">
|
||||
<select class="w-full h-full bg-transparent border-none outline-none pl-4 pr-8 cursor-pointer" id="ap-channel">
|
||||
<option value="0">Auto</option>
|
||||
{Array.from({ length: 13 }, (_, i) => i + 1).map(ch => (
|
||||
<option value={ch}>Channel {ch}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<span class="text-sm opacity-60 ml-1">Select 'Auto' for automatic channel selection</span>
|
||||
</div>
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
<div class="alert alert-info py-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold">Access Point Mode Info</p>
|
||||
<p>When in AP mode, your device creates a WiFi network. Other devices can connect to it using the SSID and password you configure above.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<form method="dialog">
|
||||
<button class="btn">Cancel</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" id="save-ap-btn">Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
import {
|
||||
$current_network_configuration,
|
||||
send_new_network_configuration_to_alpha,
|
||||
ConnectionMode,
|
||||
} from "../backend_components/NetworkManagerStore";
|
||||
|
||||
const modal = document.getElementById("ap-config-modal") as HTMLDialogElement;
|
||||
const apSSID = document.getElementById("ap-ssid") as HTMLInputElement;
|
||||
const apSSIDWrap = document.getElementById("ap-ssid-wrap");
|
||||
const apSSIDError = document.getElementById("ap-ssid-error");
|
||||
const apPassword = document.getElementById("ap-password") as HTMLInputElement;
|
||||
const apPasswordWrap = document.getElementById("ap-password-wrap");
|
||||
const apPasswordError = document.getElementById("ap-password-error");
|
||||
const apChannel = document.getElementById("ap-channel") as HTMLSelectElement;
|
||||
const toggleApPasswordBtn = document.getElementById("toggle-ap-password-btn");
|
||||
const apEyeOpen = document.getElementById("ap-eye-icon-open");
|
||||
const apEyeClosed = document.getElementById("ap-eye-icon-closed");
|
||||
|
||||
function resetModal() {
|
||||
if (apSSID) apSSID.value = "";
|
||||
if (apPassword) { apPassword.value = ""; apPassword.type = "password"; }
|
||||
if (apChannel) apChannel.value = "0";
|
||||
apSSIDWrap?.classList.remove("input-error");
|
||||
apSSIDError?.classList.add("hidden");
|
||||
apPasswordWrap?.classList.remove("input-error");
|
||||
apPasswordError?.classList.add("hidden");
|
||||
apEyeOpen?.classList.remove("hidden");
|
||||
apEyeClosed?.classList.add("hidden");
|
||||
}
|
||||
|
||||
function prefillFromConfig() {
|
||||
const cfg = $current_network_configuration.get();
|
||||
if (apSSID) apSSID.value = cfg.SSID_AP || "";
|
||||
if (apPassword) apPassword.value = cfg.Password_AP || "";
|
||||
if (apChannel) apChannel.value = String(cfg.AP_Channel ?? 0);
|
||||
}
|
||||
|
||||
if (modal) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "open" && modal.open) {
|
||||
resetModal();
|
||||
prefillFromConfig();
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(modal, { attributes: true, attributeFilter: ["open"] });
|
||||
}
|
||||
|
||||
toggleApPasswordBtn?.addEventListener("click", () => {
|
||||
if (!apPassword) return;
|
||||
const isVisible = apPassword.type === "text";
|
||||
apPassword.type = isVisible ? "password" : "text";
|
||||
apEyeOpen?.classList.toggle("hidden", !isVisible);
|
||||
apEyeClosed?.classList.toggle("hidden", isVisible);
|
||||
});
|
||||
|
||||
apSSID?.addEventListener("input", () => {
|
||||
apSSIDWrap?.classList.remove("input-error");
|
||||
apSSIDError?.classList.add("hidden");
|
||||
});
|
||||
|
||||
apPassword?.addEventListener("input", () => {
|
||||
apPasswordWrap?.classList.remove("input-error");
|
||||
apPasswordError?.classList.add("hidden");
|
||||
});
|
||||
|
||||
modal?.addEventListener("close", resetModal);
|
||||
|
||||
const saveApBtn = document.getElementById("save-ap-btn");
|
||||
saveApBtn?.addEventListener("click", () => {
|
||||
let valid = true;
|
||||
|
||||
if (!apSSID?.value || apSSID.value.length === 0) {
|
||||
apSSIDWrap?.classList.add("input-error");
|
||||
if (apSSIDError) { apSSIDError.textContent = "SSID is required."; apSSIDError.classList.remove("hidden"); }
|
||||
valid = false;
|
||||
} else if (apSSID.value.length > 31) {
|
||||
apSSIDWrap?.classList.add("input-error");
|
||||
if (apSSIDError) { apSSIDError.textContent = "SSID must be shorter than 32 characters."; apSSIDError.classList.remove("hidden"); }
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (apPassword?.value && apPassword.value.length < 8) {
|
||||
apPasswordWrap?.classList.add("input-error");
|
||||
if (apPasswordError) { apPasswordError.textContent = "Password must be at least 8 characters."; apPasswordError.classList.remove("hidden"); }
|
||||
valid = false;
|
||||
} else if (apPassword?.value && apPassword.value.length > 31) {
|
||||
apPasswordWrap?.classList.add("input-error");
|
||||
if (apPasswordError) { apPasswordError.textContent = "Password must be shorter than 32 characters."; apPasswordError.classList.remove("hidden"); }
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) return;
|
||||
|
||||
const base = $current_network_configuration.get();
|
||||
|
||||
// Derive the APSTA connection mode: keep ETH DHCP/static, switch WiFi to APSTA
|
||||
let apstaMode: ConnectionMode;
|
||||
switch (base.Connection_Mode) {
|
||||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI:
|
||||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA:
|
||||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP:
|
||||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC:
|
||||
apstaMode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA;
|
||||
break;
|
||||
default:
|
||||
apstaMode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA;
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...base,
|
||||
Connection_Mode: apstaMode,
|
||||
WiFi_Enabled: true,
|
||||
SSID_AP: apSSID.value,
|
||||
Password_AP: apPassword?.value ?? "",
|
||||
AP_Channel: parseInt(apChannel?.value ?? "0", 10),
|
||||
};
|
||||
|
||||
const sent = send_new_network_configuration_to_alpha(updatedConfig);
|
||||
if (sent) {
|
||||
window.dispatchEvent(new CustomEvent("network-config-applying"));
|
||||
modal?.close();
|
||||
} else {
|
||||
console.error("Failed to send AP configuration to device.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,689 +1,13 @@
|
|||
import { atom } from 'nanostores';
|
||||
import pkg from "websocket";
|
||||
const { w3cwebsocket } = pkg;
|
||||
import { atom } from "nanostores";
|
||||
|
||||
// Update Status Enum - matches backend enum in update_notifier.h
|
||||
export enum UpdateStatus {
|
||||
ERROR = -1,
|
||||
STARTED = 0,
|
||||
IN_PROGRESS = 1,
|
||||
SUCCESS = 2,
|
||||
NO_UPDATE = 3
|
||||
}
|
||||
|
||||
export const $websocket = atom<WebSocket | null>(null);
|
||||
export const $activePage = atom<string>('dashboard');
|
||||
export const $freeInternalHeap = atom<number>(375 * 1024);
|
||||
export const $totalInternalHeap = atom<number>(375 * 1024); // ESP32-P4 has 375KB internal heap (heap_caps_get_info mit MALLOC_CAP_INTERNAL)
|
||||
export const $modulationInput = atom<number>(0);
|
||||
export const $inputVoltage = atom<number>(0);
|
||||
export const $driverTemp = atom<number>(0);
|
||||
export const $counter = atom<number>(0);
|
||||
export const $ref = atom<number>(0);
|
||||
export const $dcdcState = atom<number>(0);
|
||||
export const $dcdc_errorStatus = atom<string>('--');
|
||||
export const $idCountDashboard = atom<number>(2);
|
||||
export const $wsConnectionStatus = atom<string>('Disconnected');
|
||||
export const $modulationInputMethod = atom<string>('--');
|
||||
export const $gpioState = atom<{ [key: number]: boolean }>({
|
||||
4: false,
|
||||
5: false,
|
||||
6: false,
|
||||
7: false,
|
||||
8: false,
|
||||
9: false,
|
||||
});
|
||||
export const $validGpioKeys = atom<number[]>([4, 5, 6, 7, 8, 9]);
|
||||
/* DCDC */
|
||||
export const $dcdc_state = atom<number>(0);
|
||||
export const $dcdc_state_string = atom<string>('--');
|
||||
export const $dcdc_state_desc_string = atom<string>('--');
|
||||
export const $dcdc_error_flag = atom<number>(0);
|
||||
export const $POWERGOOD = atom<number>(0);
|
||||
export const $POWERGOOD_string = atom<string>('--');
|
||||
export const $ref_value = atom<number>(0);
|
||||
/* LASER */
|
||||
export const $laser_state = atom<number>(2); // LASER_STOP
|
||||
export const $laser_state_string = atom<string>('--');
|
||||
export const $laser_state_desc_string = atom<string>('--');
|
||||
export const $is_REF_calibrated = atom<number>(0);
|
||||
export const $has_duty_lookup_table = atom<number>(0);
|
||||
export const $laser_error_flag = atom<number>(0);
|
||||
export const $laser_error_flag_string = atom<string>('--');
|
||||
export const $LASERGOOD = atom<number>(0);
|
||||
export const $LASERGOOD_string = atom<string>('--');
|
||||
export const $laser_set_temp = atom<number>(0);
|
||||
export const $laser_max_temp = atom<number>(0);
|
||||
export const $interlock = atom<number>(0);
|
||||
export const $set_current = atom<number>(0);
|
||||
export const $max_current = atom<number>(0);
|
||||
export const $threshold_current = atom<number>(0);
|
||||
export const $current_reading = atom<number>(0);
|
||||
export const $current_reading_string = atom<string>('--');
|
||||
export const $laser_temp = atom<number>(0);
|
||||
export const $laser_temp_string = atom<string>('--');
|
||||
|
||||
/* TEMPERATURE CHART DATA */
|
||||
export const $temperature_chart_data = atom<{
|
||||
timestamps: string[];
|
||||
driverTemps: number[];
|
||||
laserTemps: number[];
|
||||
}>({
|
||||
timestamps: [],
|
||||
driverTemps: [],
|
||||
laserTemps: []
|
||||
});
|
||||
|
||||
/* DRIVER */
|
||||
export const $driver_temp = atom<number>(0);
|
||||
export const $driver_temp_string = atom<string>('--');
|
||||
export const $driver_max_temp = atom<number>(0);
|
||||
export const $driver_max_temp_string = atom<string>('--');
|
||||
export const $vcc_voltage = atom<number>(0);
|
||||
export const $vcc_voltage_string = atom<string>('--');
|
||||
export const $pd_voltage = atom<number>(0);
|
||||
export const $pd_voltage_string = atom<string>('--');
|
||||
/* CALIBRATION TABLE */
|
||||
export const $calibration_table_data = atom<{
|
||||
values: number[];
|
||||
min_duty: number;
|
||||
max_duty: number;
|
||||
points_count: number;
|
||||
}>
|
||||
({
|
||||
values: [],
|
||||
min_duty: 0,
|
||||
max_duty: 0,
|
||||
points_count: 0,
|
||||
});
|
||||
|
||||
/* UPDATE SYSTEM */
|
||||
export interface UpdateInfo {
|
||||
core: string;
|
||||
currentVersion: string;
|
||||
newVersion: string;
|
||||
icon: string;
|
||||
hasProgressBar: boolean;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
// Neue Status-Definitionen
|
||||
export enum TargetUpdateStatus {
|
||||
WAITING = 'waiting',
|
||||
IN_PROGRESS = 'in_progress',
|
||||
COMPLETED = 'completed',
|
||||
ERROR = 'error',
|
||||
NO_UPDATE = 'no_update'
|
||||
}
|
||||
|
||||
export enum TotalUpdateStatus {
|
||||
INITIAL = 'initial',
|
||||
STARTED = 'started',
|
||||
UPDATING = 'updating',
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
RESTARTING = 'restarting'
|
||||
}
|
||||
|
||||
export const $availableUpdates = atom<UpdateInfo[]>([]);
|
||||
export const $updateChangelog = atom<string>('');
|
||||
export const $updateInProgress = atom<boolean>(false);
|
||||
|
||||
// Separate progress stores for each update type
|
||||
// [progress, status] where status uses UpdateStatus enum
|
||||
export const $p4FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
export const $p4WebinterfaceProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
export const $stm32FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
export const $c6FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
|
||||
|
||||
// Neuer Store für den Gesamt-Update-Status
|
||||
export const $totalUpdateStatus = atom<TotalUpdateStatus>(TotalUpdateStatus.INITIAL);
|
||||
|
||||
export const $currentUpdateTargetIndex = atom<number>(0);
|
||||
export const $updateErrorMessages = atom<{ [key: number]: string }>({});
|
||||
export const $updatedTargets = atom<boolean[]>([false, false, false, false]);
|
||||
|
||||
// Add update state to global store
|
||||
export const $updateState = atom<'initial' | 'updating' | 'success' | 'error' | 'reloading' | 'restarting'>('initial');
|
||||
// Global modal state
|
||||
export const $isUpdaterModalOpen = atom<boolean>(false);
|
||||
export const $isSuccessPopupOpen = atom<boolean>(false);
|
||||
export const $isErrorPopupOpen = atom<boolean>(false);
|
||||
|
||||
export const getDcdcStateString = (state: number): string => {
|
||||
switch (state) {
|
||||
case 0: return "IDLE";
|
||||
case 1: return "INIT";
|
||||
case 2: return "START";
|
||||
case 3: return "RUN";
|
||||
case 4: return "STOP";
|
||||
case 5: return "ERROR";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
export const getDcdcStateDescString = (state: number): string => {
|
||||
|
||||
switch (state) {
|
||||
case 0: return "DCDC is idle";
|
||||
case 1: return "Initializing Voltage ramping";
|
||||
case 2: return "Ramping to target voltage";
|
||||
case 3: return "DCDC is running";
|
||||
case 4: return "DCDC is stopped";
|
||||
case 5: return "DCDC is in error state"; // TODO: add error description
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
export const getLaserStateString = (state: number): string => {
|
||||
switch (state) {
|
||||
case 0: return "INTERNAL MODULATION";
|
||||
case 1: return "EXTERNAL MODULATION";
|
||||
case 2: return "STOP";
|
||||
case 3: return "ERROR";
|
||||
case 4: return "REF CALIBRATION";
|
||||
case 5: return "LASER CALIBRATION";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
};
|
||||
|
||||
export const getLaserStateDescString = (state: number): string => {
|
||||
switch (state) {
|
||||
case 0: return "Internal laser modulation running";
|
||||
case 1: return "External laser modulation running";
|
||||
case 2: return "Laser stopped";
|
||||
case 3: return "Laser in error state"; // TODO: add error description
|
||||
case 4: return "Calibrating REF valie";
|
||||
case 5: return "Calibrating laser";
|
||||
default: return "Unknown laser state";
|
||||
}
|
||||
};
|
||||
|
||||
const setDCDCState = (state: number) => {
|
||||
$dcdc_state.set(state);
|
||||
$dcdc_state_string.set(getDcdcStateString(state));
|
||||
$dcdc_state_desc_string.set(getDcdcStateDescString(state));
|
||||
}
|
||||
|
||||
const setLaserState = (state: number) => {
|
||||
$laser_state.set(state);
|
||||
$laser_state_string.set(getLaserStateString(state));
|
||||
$laser_state_desc_string.set(getLaserStateDescString(state));
|
||||
}
|
||||
|
||||
const setPOWERGOOD = (state: number) => {
|
||||
$POWERGOOD.set(state);
|
||||
$POWERGOOD_string.set(state ? "Yes" : "No");
|
||||
}
|
||||
|
||||
const setLASERGOOD = (state: number) => {
|
||||
$LASERGOOD.set(state);
|
||||
$LASERGOOD_string.set(state ? "Yes" : "No");
|
||||
}
|
||||
|
||||
|
||||
const setDcdcErrorStatus = (state: number) => {
|
||||
$dcdc_error_flag.set(state);
|
||||
$dcdc_errorStatus.set(state ? "Error" : "No Errors");
|
||||
}
|
||||
const setLaserErrorStatus = (state: number) => {
|
||||
$laser_error_flag.set(state);
|
||||
$laser_error_flag_string.set(state ? "Error" : "No Errors");
|
||||
}
|
||||
|
||||
const setREF_value = (state: number) => {
|
||||
$ref_value.set(state);
|
||||
}
|
||||
|
||||
const setModulationInputMethod = (state: number) => {
|
||||
$modulationInputMethod.set(state === 0 ? "INTERNAL MODULATION" : state === 1 ? "EXTERNAL MODULATION" : "---");
|
||||
}
|
||||
|
||||
const setREF_calibrated = (state: number) => {
|
||||
$is_REF_calibrated.set(state);
|
||||
}
|
||||
const setHAS_DUTY_LOOKUP_TABLE = (state: number) => {
|
||||
$has_duty_lookup_table.set(state);
|
||||
}
|
||||
|
||||
const setLaserSetTemp = (state: number) => {
|
||||
$laser_set_temp.set(state);
|
||||
}
|
||||
const setLaserMaxTemp = (state: number) => {
|
||||
$laser_max_temp.set(state);
|
||||
}
|
||||
const setInterlock = (state: number) => {
|
||||
$interlock.set(state);
|
||||
}
|
||||
|
||||
const setModulationInput = (state: number) => {
|
||||
$modulationInput.set(state);
|
||||
}
|
||||
const setSetCurrent = (state: number) => {
|
||||
$set_current.set(state);
|
||||
}
|
||||
const setMaxCurrent = (state: number) => {
|
||||
$max_current.set(state);
|
||||
}
|
||||
|
||||
const setThresholdCurrent = (state: number) => {
|
||||
$threshold_current.set(state);
|
||||
}
|
||||
const setCurrentReading = (state: number) => {
|
||||
$current_reading.set(state);
|
||||
$current_reading_string.set(state + " mA");
|
||||
}
|
||||
const setLaserTemp = (state: number) => {
|
||||
$laser_temp.set(state);
|
||||
$laser_temp_string.set(state + " °C");
|
||||
}
|
||||
|
||||
const setDriverTemp = (state: number) => {
|
||||
$driver_temp.set(state);
|
||||
$driver_temp_string.set(state + " °C");
|
||||
}
|
||||
const setDriverMaxTemp = (state: number) => {
|
||||
$driver_max_temp.set(state);
|
||||
$driver_max_temp_string.set(state + " °C");
|
||||
}
|
||||
|
||||
// Temperature chart data management
|
||||
const maxTemperatureDataPoints = 300; // 5 minutes worth of data at 1 second intervals
|
||||
|
||||
export const addTemperatureDataPoint = (driverTemp: number, laserTemp: number) => {
|
||||
const currentData = $temperature_chart_data.get();
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString();
|
||||
|
||||
const newData = {
|
||||
timestamps: [...currentData.timestamps, timeString],
|
||||
driverTemps: [...currentData.driverTemps, driverTemp],
|
||||
laserTemps: [...currentData.laserTemps, laserTemp]
|
||||
};
|
||||
|
||||
// Remove old data points to maintain 5-minute window
|
||||
if (newData.timestamps.length > maxTemperatureDataPoints) {
|
||||
newData.timestamps = newData.timestamps.slice(-maxTemperatureDataPoints);
|
||||
newData.driverTemps = newData.driverTemps.slice(-maxTemperatureDataPoints);
|
||||
newData.laserTemps = newData.laserTemps.slice(-maxTemperatureDataPoints);
|
||||
}
|
||||
|
||||
$temperature_chart_data.set(newData);
|
||||
}
|
||||
|
||||
export const clearTemperatureData = () => {
|
||||
$temperature_chart_data.set({
|
||||
timestamps: [],
|
||||
driverTemps: [],
|
||||
laserTemps: []
|
||||
});
|
||||
}
|
||||
const setVccVoltage = (state: number) => {
|
||||
$vcc_voltage.set(state);
|
||||
$vcc_voltage_string.set(state + " V");
|
||||
}
|
||||
const setPdVoltage = (state: number) => {
|
||||
$pd_voltage.set(state);
|
||||
$pd_voltage_string.set(state + " V");
|
||||
}
|
||||
|
||||
const setCalibrationTableData = (values: number[], min_duty: number, max_duty: number, points_count: number) => {
|
||||
$calibration_table_data.set({
|
||||
values: values,
|
||||
min_duty: min_duty,
|
||||
max_duty: max_duty,
|
||||
points_count: points_count
|
||||
});
|
||||
}
|
||||
|
||||
const setAvailableUpdates = (updates: UpdateInfo[]) => {
|
||||
$availableUpdates.set(updates);
|
||||
}
|
||||
const setUpdateChangelog = (changelog: string) => {
|
||||
$updateChangelog.set(changelog);
|
||||
}
|
||||
const setUpdateInProgress = (inProgress: boolean) => {
|
||||
$updateInProgress.set(inProgress);
|
||||
}
|
||||
|
||||
const setP4FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$p4FirmwareProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setP4WebinterfaceProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$p4WebinterfaceProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setStm32FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$stm32FirmwareProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setC6FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
|
||||
$c6FirmwareProgress.set([progress, status]);
|
||||
}
|
||||
|
||||
const setTotalUpdateStatus = (status: TotalUpdateStatus) => {
|
||||
console.log(`🎯 Total update status change: ${$totalUpdateStatus.get()} → ${status}`);
|
||||
$totalUpdateStatus.set(status);
|
||||
}
|
||||
|
||||
const startUpdate = () => {
|
||||
console.log(`🚀 Starting update process`);
|
||||
setTotalUpdateStatus(TotalUpdateStatus.STARTED);
|
||||
}
|
||||
|
||||
const setCurrentUpdateTargetIndex = (index: number) => {
|
||||
$currentUpdateTargetIndex.set(index);
|
||||
}
|
||||
|
||||
const setUpdateErrorMessages = (errorMessages: { [key: number]: string }) => {
|
||||
$updateErrorMessages.set(errorMessages);
|
||||
}
|
||||
|
||||
const setUpdateState = (state: 'initial' | 'updating' | 'success' | 'error' | 'restarting') => {
|
||||
console.log(`🎯 State change: ${$updateState.get()} → ${state}`);
|
||||
$updateState.set(state);
|
||||
|
||||
// Synchronize with total update status
|
||||
switch (state) {
|
||||
case 'initial':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
|
||||
break;
|
||||
case 'updating':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
break;
|
||||
case 'success':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
|
||||
break;
|
||||
case 'error':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.ERROR);
|
||||
break;
|
||||
|
||||
case 'restarting':
|
||||
setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const setUpdaterModalOpen = (open: boolean) => {
|
||||
console.log(`🚀 Setting updater modal: ${open ? 'OPEN' : 'CLOSED'}`);
|
||||
$isUpdaterModalOpen.set(open);
|
||||
}
|
||||
// reset all states to offiline states, when websocket is disconnected
|
||||
const setWebSocketConnectionStatus = (status: string) => {
|
||||
$wsConnectionStatus.set(status);
|
||||
// set states to offline states! But do not reset calibration table/update states etc.
|
||||
if (status === "Disconnected") {
|
||||
$modulationInput.set(0);
|
||||
$inputVoltage.set(0);
|
||||
$driverTemp.set(0);
|
||||
$counter.set(0);
|
||||
$ref.set(0);
|
||||
$dcdcState.set(0);
|
||||
$dcdc_errorStatus.set('--');
|
||||
$modulationInputMethod.set('--');
|
||||
$gpioState.set({
|
||||
4: false,
|
||||
5: false,
|
||||
6: false,
|
||||
7: false,
|
||||
8: false,
|
||||
9: false,
|
||||
});
|
||||
/* DCDC */
|
||||
$dcdc_state.set(0);
|
||||
$dcdc_state_string.set('--');
|
||||
$dcdc_state_desc_string.set('--');
|
||||
$dcdc_error_flag.set(0);
|
||||
$POWERGOOD.set(0);
|
||||
$POWERGOOD_string.set('--');
|
||||
$ref_value.set(0);
|
||||
/* LASER */
|
||||
$laser_state.set(2); // LASER_STOP
|
||||
$laser_state_string.set('--');
|
||||
$laser_state_desc_string.set('--');
|
||||
$is_REF_calibrated.set(0);
|
||||
$has_duty_lookup_table.set(0);
|
||||
$laser_error_flag.set(0);
|
||||
$laser_error_flag_string.set('--');
|
||||
$LASERGOOD.set(0);
|
||||
$LASERGOOD_string.set('--');
|
||||
$laser_set_temp.set(0);
|
||||
$laser_max_temp.set(0);
|
||||
$interlock.set(0);
|
||||
$set_current.set(0);
|
||||
$max_current.set(0);
|
||||
$threshold_current.set(0);
|
||||
$current_reading.set(0);
|
||||
$current_reading_string.set('--');
|
||||
$laser_temp.set(0);
|
||||
$laser_temp_string.set('--');
|
||||
/* TEMPERATURE CHART DATA */
|
||||
clearTemperatureData();
|
||||
/* DRIVER */
|
||||
$driver_temp.set(0);
|
||||
$driver_temp_string.set('--');
|
||||
$driver_max_temp.set(0);
|
||||
$driver_max_temp_string.set('--');
|
||||
$vcc_voltage.set(0);
|
||||
$vcc_voltage_string.set('--');
|
||||
$pd_voltage.set(0);
|
||||
$pd_voltage_string.set('--');
|
||||
}
|
||||
}
|
||||
|
||||
const resetUpdateStatus = () => {
|
||||
console.log("Resetting update status");
|
||||
$updateState.set('initial');
|
||||
$updateInProgress.set(false);
|
||||
$updateErrorMessages.set({});
|
||||
$currentUpdateTargetIndex.set(0);
|
||||
$p4FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$p4WebinterfaceProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$stm32FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$c6FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
|
||||
$availableUpdates.set([]);
|
||||
$updateChangelog.set('');
|
||||
$updatedTargets.set([false, false, false, false]);
|
||||
}
|
||||
|
||||
const setErrorPopupOpen = (isOpen: boolean) => {
|
||||
console.log(`❌ Setting error popup: ${isOpen ? 'OPEN' : 'CLOSED'}`);
|
||||
$isErrorPopupOpen.set(isOpen);
|
||||
}
|
||||
|
||||
const setSuccessPopupOpen = (isOpen: boolean) => {
|
||||
console.log(`🎉 Setting success popup: ${isOpen ? 'OPEN' : 'CLOSED'}`);
|
||||
$isSuccessPopupOpen.set(isOpen);
|
||||
}
|
||||
|
||||
const setFreeInternalHeap = (heap: number) => {
|
||||
$freeInternalHeap.set(heap);
|
||||
}
|
||||
|
||||
const setUpdatedTargets = (targets: boolean[]) => {
|
||||
console.log("🎯 Setting updated targets:", targets);
|
||||
$updatedTargets.set(targets);
|
||||
}
|
||||
// WebSocket connection state — set by WebSocketProvider on open/close/error.
|
||||
// Consumers can subscribe to react to connectivity (e.g. show a toast, gate UI actions).
|
||||
export const $wsConnectionStatus = atom<string>("Disconnected");
|
||||
|
||||
// Centralised mutators so WebSocketProvider doesn't need to know about every atom.
|
||||
// More setters can be added here as the app gains state beyond the WiFi config UI.
|
||||
export const stateSetters = {
|
||||
setWebSocketConnectionStatus,
|
||||
setDCDCState,
|
||||
setLaserState,
|
||||
setPOWERGOOD,
|
||||
setLASERGOOD,
|
||||
setDcdcErrorStatus,
|
||||
setLaserErrorStatus,
|
||||
setREF_value,
|
||||
setModulationInputMethod,
|
||||
setREF_calibrated,
|
||||
setHAS_DUTY_LOOKUP_TABLE,
|
||||
setLaserSetTemp,
|
||||
setLaserMaxTemp,
|
||||
setInterlock,
|
||||
setModulationInput,
|
||||
setSetCurrent,
|
||||
setMaxCurrent,
|
||||
setThresholdCurrent,
|
||||
setCurrentReading,
|
||||
setLaserTemp,
|
||||
setDriverTemp,
|
||||
setDriverMaxTemp,
|
||||
setVccVoltage,
|
||||
setPdVoltage,
|
||||
setCalibrationTableData,
|
||||
setAvailableUpdates,
|
||||
setUpdateChangelog,
|
||||
setUpdateInProgress,
|
||||
setP4FirmwareProgress,
|
||||
setP4WebinterfaceProgress,
|
||||
setStm32FirmwareProgress,
|
||||
setC6FirmwareProgress,
|
||||
setTotalUpdateStatus,
|
||||
startUpdate,
|
||||
setCurrentUpdateTargetIndex,
|
||||
setUpdateErrorMessages,
|
||||
setUpdateState,
|
||||
setUpdaterModalOpen,
|
||||
setSuccessPopupOpen,
|
||||
setErrorPopupOpen,
|
||||
resetUpdateStatus,
|
||||
setFreeInternalHeap,
|
||||
setUpdatedTargets
|
||||
setWebSocketConnectionStatus: (status: string) => {
|
||||
$wsConnectionStatus.set(status);
|
||||
},
|
||||
};
|
||||
|
||||
const send_uart_cmd_without_data = (command: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array([0xAB, command]);
|
||||
$websocket.get()?.send(buffer);
|
||||
} else {
|
||||
console.error("WebSocket ist nicht verbunden");
|
||||
}
|
||||
};
|
||||
|
||||
const send_uart_cmd_with_data = (command: number, data: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array(6);
|
||||
buffer[0] = 0xAA;
|
||||
buffer[1] = command;
|
||||
buffer[2] = (data >> 24) & 0xFF;
|
||||
buffer[3] = (data >> 16) & 0xFF;
|
||||
buffer[4] = (data >> 8) & 0xFF;
|
||||
buffer[5] = data & 0xFF;
|
||||
$websocket.get()?.send(buffer);
|
||||
}
|
||||
};
|
||||
|
||||
const send_network_cmd = (command: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array([0xBA, command]);
|
||||
$websocket.get()?.send(buffer);
|
||||
} else {
|
||||
console.error("WebSocket ist nicht verbunden");
|
||||
}
|
||||
};
|
||||
|
||||
const send_firmware_update_cmd = (command: number) => {
|
||||
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
|
||||
const buffer = new Uint8Array([0xBB, command]);
|
||||
$websocket.get()?.send(buffer);
|
||||
} else {
|
||||
console.error("WebSocket ist nicht verbunden");
|
||||
}
|
||||
}
|
||||
|
||||
export const start_DCDC = () => {
|
||||
send_uart_cmd_without_data(0x00);
|
||||
}
|
||||
|
||||
export const stop_DCDC = () => {
|
||||
send_uart_cmd_without_data(0x01);
|
||||
}
|
||||
|
||||
export const reset_DCDC = () => {
|
||||
send_uart_cmd_without_data(0x02);
|
||||
}
|
||||
|
||||
export const run_Laser_External = () => {
|
||||
send_uart_cmd_without_data(0x03);
|
||||
}
|
||||
|
||||
export const run_Laser_Internal = () => {
|
||||
send_uart_cmd_without_data(0x04);
|
||||
}
|
||||
|
||||
export const stop_Laser = () => {
|
||||
send_uart_cmd_without_data(0x05);
|
||||
}
|
||||
|
||||
export const reset_Laser = () => {
|
||||
send_uart_cmd_without_data(0x06);
|
||||
}
|
||||
|
||||
export const start_REF_Calibration = () => {
|
||||
send_uart_cmd_without_data(0x07);
|
||||
}
|
||||
|
||||
export const start_Laser_Calibration = () => {
|
||||
send_uart_cmd_without_data(0x08);
|
||||
}
|
||||
|
||||
export const set_Laser_Threshold = (threshold: number) => {
|
||||
send_uart_cmd_with_data(0x09, threshold);
|
||||
}
|
||||
|
||||
export const set_Laser_MAX_Current = (current: number) => {
|
||||
send_uart_cmd_with_data(0x0A, current);
|
||||
}
|
||||
|
||||
export const set_Laser_Set_Current = (current: number) => {
|
||||
send_uart_cmd_with_data(0x0B, current);
|
||||
}
|
||||
|
||||
export const set_Laser_Set_Temp = (temp: number) => {
|
||||
send_uart_cmd_with_data(0x0C, temp);
|
||||
}
|
||||
export const set_Laser_Max_Temp = (temp: number) => {
|
||||
send_uart_cmd_with_data(0x0D, temp);
|
||||
}
|
||||
export const set_Driver_Max_Temp = (temp: number) => {
|
||||
send_uart_cmd_with_data(0x0E, temp);
|
||||
}
|
||||
export const get_calibration_table = () => {
|
||||
console.log("Getting calibration table");
|
||||
send_uart_cmd_without_data(0x0F);
|
||||
}
|
||||
export const set_REF_value = (value: number) => {
|
||||
send_uart_cmd_with_data(0x10, value);
|
||||
}
|
||||
export const set_Overcurrent_Protection_Value = (value: number) => {
|
||||
send_uart_cmd_with_data(0x11, value);
|
||||
}
|
||||
|
||||
// UPDATE SYSTEM COMMANDS
|
||||
export const request_available_updates = () => {
|
||||
console.log("Requesting available updates");
|
||||
send_firmware_update_cmd(0x00);
|
||||
}
|
||||
|
||||
|
||||
export const start_firmware_update_p4_fw = () => {
|
||||
console.log("Starting P4 Firmware update");
|
||||
send_firmware_update_cmd(0x02);
|
||||
}
|
||||
export const start_firmware_update_p4_web = () => {
|
||||
console.log("Starting P4 Webinterface update");
|
||||
send_firmware_update_cmd(0x03);
|
||||
}
|
||||
export const start_firmware_update_stm32_fw = () => {
|
||||
console.log("Starting STM32 Firmware update");
|
||||
send_firmware_update_cmd(0x04);
|
||||
}
|
||||
export const start_firmware_update_c6_fw = () => {
|
||||
console.log("Starting C6 Firmware update");
|
||||
send_firmware_update_cmd(0x05);
|
||||
}
|
||||
export const request_update_progress = () => {
|
||||
console.log("Requesting update progress");
|
||||
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { $wsConnectionStatus } from './AppProviderStore';
|
||||
|
||||
export default function ConnectionStatusBadge() {
|
||||
const status = useStore($wsConnectionStatus);
|
||||
|
||||
let colorClass = 'bg-gray-400 text-white';
|
||||
if (status.startsWith('Connected')) colorClass = 'bg-green-500 text-white';
|
||||
else if (status.startsWith('Connecting')) colorClass = 'bg-yellow-400 text-black';
|
||||
else if (
|
||||
status.startsWith('Error') ||
|
||||
status.startsWith('Failed') ||
|
||||
status.startsWith('Disconnected')
|
||||
)
|
||||
colorClass = 'bg-red-500 text-white';
|
||||
|
||||
// Passe die Breite ggf. an den längsten Status an!
|
||||
const maxText = 'Connecting ...';
|
||||
|
||||
return (
|
||||
<span
|
||||
class={`badge ${colorClass} font-mono relative inline-block transition-all duration-200`}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px', padding: 0 }}
|
||||
>
|
||||
{/* Unsichtbarer Platzhalter für feste Breite und Position */}
|
||||
<span
|
||||
class="opacity-0 select-none pointer-events-none"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{maxText}
|
||||
</span>
|
||||
{/* Sichtbarer Status absolut darüber */}
|
||||
<span
|
||||
class="absolute left-0 top-0 w-full h-full flex items-center"
|
||||
style={{ justifyContent: 'left', paddingLeft: '0.75rem' /* ggf. anpassen */ }}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
---
|
||||
const pageTitle = "Laser Control";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- DCDC Controls -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">DCDC Control</h2>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div class="join w-full mb-4">
|
||||
<button class="btn join-item flex-1" id="dcdc-start">Start</button>
|
||||
<button class="btn join-item flex-1 btn-success" id="dcdc-run">Run</button>
|
||||
<button class="btn join-item flex-1 btn-error" id="dcdc-stop">Stop</button>
|
||||
<button class="btn join-item flex-1 btn-error error" id="dcdc-error">Error</button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Target Voltage (V)</span>
|
||||
<span class="label-text-alt" id="voltage-display">25V</span>
|
||||
</label>
|
||||
<input type="range" min="10" max="50" value="25" class="range range-primary" step="1" id="voltage-slider" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>10V</span>
|
||||
<span>20V</span>
|
||||
<span>30V</span>
|
||||
<span>40V</span>
|
||||
<span>50V</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-6">
|
||||
<div class="badge badge-success gap-2 p-4">
|
||||
<span class="font-semibold">Input Voltage:</span> <span id="input-voltage">24.5V</span>
|
||||
</div>
|
||||
<div class="badge badge-error gap-2 p-4">
|
||||
<span class="font-semibold">PD:</span> <span id="pd-voltage">3.2V</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-2">
|
||||
<button class="btn btn-primary" id="apply-dcdc">Apply Settings</button>
|
||||
<button class="btn btn-outline" id="reset-dcdc">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Laser Control -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Laser Control</h2>
|
||||
|
||||
<div class="join w-full mb-4">
|
||||
<button class="btn join-item flex-1" id="laser-start">Start</button>
|
||||
<button class="btn join-item flex-1 btn-success" id="laser-run">Run</button>
|
||||
<button class="btn join-item flex-1 btn-error" id="laser-stop">Stop</button>
|
||||
<button class="btn join-item flex-1 btn-error error" id="laser-error">Error</button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<span class="text-lg">Interlock Status:</span>
|
||||
<div class="badge badge-success gap-2 p-4" id="interlock-status">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||
</svg>
|
||||
<span>Unlocked</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Response Time</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" id="response-time">
|
||||
<option>Fast (High Bandwidth)</option>
|
||||
<option selected>Normal</option>
|
||||
<option>Slow (Low Noise)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Stability Mode</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button class="btn join-item flex-1 btn-active" id="stability-normal">Normal</button>
|
||||
<button class="btn join-item flex-1" id="stability-high">High Stability</button>
|
||||
<button class="btn join-item flex-1" id="stability-low">Low Noise</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>The laser mode is dependent on the DCDC status and safety settings.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Safety Settings -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Safety Settings</h2>
|
||||
|
||||
<!-- Horizontal row of toggles -->
|
||||
<div class="flex flex-wrap gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Interlock Check</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-interlock" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Temperature Protection</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-temp" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">OverCurrent Protection</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-overcurrent" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Input Voltage Protection</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-voltage" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Autostart DCDC</span>
|
||||
<input type="checkbox" class="toggle toggle-error" id="toggle-autostart-dcdc" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Autostart Laser</span>
|
||||
<input type="checkbox" class="toggle toggle-error" id="toggle-autostart-laser" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>Disabling safety features may damage your hardware or create hazardous conditions.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Voltage slider interaction
|
||||
const voltageSlider = document.getElementById('voltage-slider') as HTMLInputElement;
|
||||
const voltageDisplay = document.getElementById('voltage-display');
|
||||
|
||||
if (voltageSlider && voltageDisplay) {
|
||||
voltageSlider.addEventListener('input', function() {
|
||||
voltageDisplay.textContent = this.value + 'V';
|
||||
});
|
||||
}
|
||||
|
||||
// DCDC button interactions
|
||||
const dcdcButtons = {
|
||||
start: document.getElementById('dcdc-start'),
|
||||
run: document.getElementById('dcdc-run'),
|
||||
stop: document.getElementById('dcdc-stop'),
|
||||
error: document.getElementById('dcdc-error')
|
||||
};
|
||||
|
||||
Object.entries(dcdcButtons).forEach(([action, btn]) => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
console.log('DCDC:', action);
|
||||
// Send WebSocket command
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Laser button interactions
|
||||
const laserButtons = {
|
||||
start: document.getElementById('laser-start'),
|
||||
run: document.getElementById('laser-run'),
|
||||
stop: document.getElementById('laser-stop'),
|
||||
error: document.getElementById('laser-error')
|
||||
};
|
||||
|
||||
Object.entries(laserButtons).forEach(([action, btn]) => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
console.log('Laser:', action);
|
||||
// Send WebSocket command
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Safety toggle interactions
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
toggle.addEventListener('change', () => {
|
||||
updateToggleClass(toggle as HTMLInputElement);
|
||||
});
|
||||
updateToggleClass(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
function updateToggleClass(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,549 +0,0 @@
|
|||
---
|
||||
import {
|
||||
$wsConnectionStatus,
|
||||
$dcdc_state_string,
|
||||
$dcdc_state_desc_string,
|
||||
$laser_state_string,
|
||||
$laser_state_desc_string,
|
||||
$laser_temp,
|
||||
$laser_temp_string,
|
||||
$driver_temp,
|
||||
$driver_temp_string,
|
||||
$driver_max_temp,
|
||||
$driver_max_temp_string,
|
||||
$temperature_chart_data,
|
||||
addTemperatureDataPoint,
|
||||
} from "./AppProviderStore";
|
||||
const pageTitle = "Dashboard";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Status Cards - Static HTML that gets updated via WebSocket -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 bg-base-100">
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">DCDC State</div>
|
||||
<div class="stat-value" id="dcdc-state-value">
|
||||
{$dcdc_state_string.get()}
|
||||
</div>
|
||||
<div class="stat-desc flex items-center">
|
||||
<span class="text-info mr-1" id="dcdc-state-icon">● </span>
|
||||
<span id="dcdc-state-text">{$dcdc_state_desc_string.get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">Laser State</div>
|
||||
<div class="stat-value" id="laser-state-value">
|
||||
{$laser_state_string.get()}
|
||||
</div>
|
||||
<div class="stat-desc flex items-center">
|
||||
<span class="text-error mr-1" id="laser-state-icon">●</span>
|
||||
<span id="laser-state-text">{$laser_state_desc_string.get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">Driver Temperature</div>
|
||||
<div class="stat-value" id="driver-temp-value1">{$driver_temp_string.get()}</div>
|
||||
<div class="stat-desc flex items-center">
|
||||
<span class="text-info mr-1" id="driver-max-temp-icon">●</span>
|
||||
<span id="driver-max-temp-text">Maximal allowed: {$driver_max_temp_string.get()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200">
|
||||
<div class="stat-title">WebSocket</div>
|
||||
<div class="stat-value" id="ws-status-value">
|
||||
{$wsConnectionStatus.get()}
|
||||
</div>
|
||||
<div class="stat-desc flex items-center" id="ws-status-desc">
|
||||
<span class="text-error mr-1" id="ws-status-icon">●</span>
|
||||
Connection status
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Controls - Interactive Parts -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- DCDC Controls -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">DCDC Control</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button class="btn btn-primary" id="dcdc-start-btn">Start DCDC</button
|
||||
>
|
||||
<button class="btn btn-warning" id="dcdc-stop-btn">Stop DCDC</button>
|
||||
<button class="btn btn-outline" id="dcdc-reset-btn">Reset DCDC</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="text-sm"
|
||||
>Power Good: <span id="power-good-status" class="font-bold">--</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Laser Controls -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Laser Control</h2>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-3">
|
||||
<button class="btn btn-primary flex-1 rounded-lg shadow-md transition hover:scale-105" id="ref-calibrate-btn">Calibrate REF</button>
|
||||
<button class="btn btn-primary flex-1 rounded-lg shadow-md transition hover:scale-105" id="laser-calibrate-btn">Calibrate Laser</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3">
|
||||
<button
|
||||
class="btn btn-success flex-1 rounded-lg shadow-md transition hover:scale-105"
|
||||
id="laser-start-external-btn">Start Laser External</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-success flex-1 rounded-lg shadow-md transition hover:scale-105"
|
||||
id="laser-start-internal-btn">Start Laser Internal</button
|
||||
>
|
||||
</div>
|
||||
<button class="btn btn-error" id="laser-stop-btn">Stop Laser</button>
|
||||
<button class="btn btn-outline" id="laser-reset-btn"
|
||||
>Reset Laser</button
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="text-sm"
|
||||
>Laser Good: <span id="laser-good-status" class="font-bold">--</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-100 rounded-lg">
|
||||
<h2 class="card-title">System Status</h2>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span>Driver Temp:</span>
|
||||
<span id="driver-temp-value2" class="font-mono">--°C</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>VCC Voltage:</span>
|
||||
<span id="vcc-voltage-value" class="font-mono">--V</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>PD Voltage:</span>
|
||||
<span id="pd-voltage-value" class="font-mono">--V</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Laser Current:</span>
|
||||
<span id="laser-current-value" class="font-mono">--mA</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-100 rounded-lg relative">
|
||||
<h2 class="card-title">Calibration Table</h2>
|
||||
|
||||
<!-- Chart container with placeholder -->
|
||||
<div class="flex-1 min-h-[300px] max-h-[500px] relative">
|
||||
<!-- Placeholder when no data -->
|
||||
<div id="chart-placeholder" class="flex items-center justify-center h-full bg-base-200 rounded-lg">
|
||||
<div class="text-center">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chart-line mx-auto"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 19l16 0" /><path d="M4 15l4 -6l4 2l4 -5l4 4" /></svg>
|
||||
|
||||
<div class="text-lg font-semibold">No Calibration Data</div>
|
||||
<div class="text-sm text-base-content/70">Click the button below to request data from device</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart canvas (hidden initially) -->
|
||||
<canvas id="calibration-table-canvas" class="hidden w-full h-full"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Small button positioned bottom right -->
|
||||
<button class="btn btn-primary btn-xs absolute bottom-2 right-2" id="calibration-table-btn">
|
||||
Request Calibration Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temperature Chart -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-100 rounded-lg relative">
|
||||
<h2 class="card-title">Temperature Monitor</h2>
|
||||
|
||||
<!-- Chart container -->
|
||||
<div class="flex-1 min-h-[300px] max-h-[500px] relative">
|
||||
<!-- Chart canvas -->
|
||||
<canvas id="temperature-chart-canvas" class="w-full h-full"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Status indicators -->
|
||||
<div class="flex justify-between items-center mt-2 text-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-3 h-3 bg-blue-500 rounded"></div>
|
||||
<span>Driver Temp: <span id="driver-temp-current" class="font-mono">--°C</span></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-3 h-3 bg-red-500 rounded"></div>
|
||||
<span>Laser Temp: <span id="laser-temp-current" class="font-mono">--°C</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-base-content/70">
|
||||
Last 5 minutes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hydrate only the interactive parts -->
|
||||
<script>
|
||||
import {
|
||||
$wsConnectionStatus,
|
||||
$dcdc_state_string,
|
||||
$dcdc_state_desc_string,
|
||||
$laser_state_string,
|
||||
$laser_state_desc_string,
|
||||
start_DCDC,
|
||||
$laser_temp,
|
||||
$laser_temp_string,
|
||||
$driver_temp,
|
||||
$driver_temp_string,
|
||||
$driver_max_temp,
|
||||
$driver_max_temp_string,
|
||||
$temperature_chart_data,
|
||||
addTemperatureDataPoint,
|
||||
$vcc_voltage,
|
||||
$vcc_voltage_string,
|
||||
$pd_voltage,
|
||||
$pd_voltage_string,
|
||||
$current_reading,
|
||||
$current_reading_string,
|
||||
$POWERGOOD_string,
|
||||
$LASERGOOD_string,
|
||||
stop_DCDC,
|
||||
reset_DCDC,
|
||||
run_Laser_External,
|
||||
run_Laser_Internal,
|
||||
stop_Laser,
|
||||
reset_Laser,
|
||||
get_calibration_table,
|
||||
$calibration_table_data,
|
||||
start_REF_Calibration,
|
||||
start_Laser_Calibration,
|
||||
} from "./AppProviderStore";
|
||||
import { Chart, type ChartItem } from "chart.js/auto";
|
||||
// subscribe to the wsConnectionStatus store and update the value of the ws-status-value element
|
||||
$wsConnectionStatus.subscribe((value) => {
|
||||
const el = document.getElementById("ws-status-value");
|
||||
const elIcon = document.getElementById("ws-status-icon");
|
||||
// overwrite the value of the ws-status-value element
|
||||
if (el) el.textContent = value;
|
||||
// overwrite the icon color of the ws-status-icon element
|
||||
if (elIcon && value === "Connected") {
|
||||
elIcon.classList.remove("text-error");
|
||||
elIcon.classList.add("text-success");
|
||||
} else if (elIcon && value === "Disconnected") {
|
||||
elIcon.classList.remove("text-success");
|
||||
elIcon.classList.add("text-error");
|
||||
} else if (elIcon && value === "Error") {
|
||||
elIcon.classList.remove("text-success");
|
||||
elIcon.classList.add("text-error");
|
||||
} else if (elIcon && value === "Failed") {
|
||||
elIcon.classList.remove("text-success");
|
||||
elIcon.classList.add("text-error");
|
||||
} else if (elIcon && value === "Connecting") {
|
||||
elIcon.classList.remove("text-error");
|
||||
elIcon.classList.add("text-warning");
|
||||
}
|
||||
});
|
||||
// subscribe to the dcdcStateString store and update the value of the dcdc-state-value element
|
||||
$dcdc_state_string.subscribe((value) => {
|
||||
const el = document.getElementById("dcdc-state-value");
|
||||
console.log("dcdcStateString", value);
|
||||
if (el) el.textContent = value;
|
||||
});
|
||||
// subscribe to the laserStateString store and update the value of the laser-state-value element
|
||||
$laser_state_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-state-value");
|
||||
if (el) el.textContent = value;
|
||||
});
|
||||
$driver_temp.subscribe((value) => {
|
||||
const el1 = document.getElementById("driver-temp-value1");
|
||||
const el2 = document.getElementById("driver-temp-value2");
|
||||
const elMax = document.getElementById("driver-max-temp-text");
|
||||
if (el1) el1.textContent = $driver_temp_string.get();
|
||||
if (el2) el2.textContent = $driver_temp_string.get();
|
||||
});
|
||||
$driver_max_temp_string.subscribe((value) => {
|
||||
const elMax = document.getElementById("driver-max-temp-text");
|
||||
if (elMax) elMax.textContent = "Maximal allowed: " + $driver_max_temp_string.get();
|
||||
});
|
||||
|
||||
// Temperature chart subscriptions
|
||||
$driver_temp.subscribe((value) => {
|
||||
const el = document.getElementById("driver-temp-current");
|
||||
if (el) el.textContent = $driver_temp_string.get();
|
||||
|
||||
// Add data point to temperature chart using store function
|
||||
addTemperatureDataPoint($driver_temp.get(), $laser_temp.get());
|
||||
});
|
||||
|
||||
$laser_temp.subscribe((value) => {
|
||||
const el = document.getElementById("laser-temp-current");
|
||||
if (el) el.textContent = $laser_temp_string.get();
|
||||
|
||||
// Temperature data will be added when driver temp updates to avoid duplicate entries
|
||||
});
|
||||
$vcc_voltage.subscribe((value) => {
|
||||
const el = document.getElementById("vcc-voltage-value");
|
||||
if (el) el.textContent = $vcc_voltage_string.get();
|
||||
});
|
||||
$pd_voltage_string.subscribe((value) => {
|
||||
const el = document.getElementById("pd-voltage-value");
|
||||
if (el) el.textContent = $pd_voltage_string.get();
|
||||
});
|
||||
$current_reading_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-current-value");
|
||||
if (el) el.textContent = $current_reading_string.get();
|
||||
});
|
||||
|
||||
$POWERGOOD_string.subscribe((value) => {
|
||||
const el = document.getElementById("power-good-status");
|
||||
if (el) el.textContent = $POWERGOOD_string.get();
|
||||
});
|
||||
$LASERGOOD_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-good-status");
|
||||
if (el) el.textContent = $LASERGOOD_string.get();
|
||||
});
|
||||
$dcdc_state_desc_string.subscribe((value) => {
|
||||
const el = document.getElementById("dcdc-state-text");
|
||||
if (el) el.textContent = $dcdc_state_desc_string.get();
|
||||
});
|
||||
$laser_state_desc_string.subscribe((value) => {
|
||||
const el = document.getElementById("laser-state-text");
|
||||
if (el) el.textContent = $laser_state_desc_string.get();
|
||||
});
|
||||
// Button click handlers
|
||||
document
|
||||
.getElementById("dcdc-start-btn")
|
||||
?.addEventListener("click", start_DCDC);
|
||||
document
|
||||
.getElementById("dcdc-stop-btn")
|
||||
?.addEventListener("click", stop_DCDC);
|
||||
document
|
||||
.getElementById("dcdc-reset-btn")
|
||||
?.addEventListener("click", reset_DCDC);
|
||||
document
|
||||
.getElementById("laser-start-external-btn")
|
||||
?.addEventListener("click", run_Laser_External);
|
||||
document
|
||||
.getElementById("laser-start-internal-btn")
|
||||
?.addEventListener("click", run_Laser_Internal);
|
||||
document
|
||||
.getElementById("laser-stop-btn")
|
||||
?.addEventListener("click", stop_Laser);
|
||||
document
|
||||
.getElementById("laser-reset-btn")
|
||||
?.addEventListener("click", reset_Laser);
|
||||
|
||||
document.getElementById("ref-calibrate-btn")?.addEventListener("click", start_REF_Calibration);
|
||||
document.getElementById("laser-calibrate-btn")?.addEventListener("click", start_Laser_Calibration);
|
||||
|
||||
// Chart management
|
||||
let chart: Chart | null = null;
|
||||
const canvas = document.getElementById("calibration-table-canvas");
|
||||
const placeholder = document.getElementById("chart-placeholder");
|
||||
|
||||
function showPlaceholder() {
|
||||
if (placeholder) placeholder.classList.remove("hidden");
|
||||
if (canvas) canvas.classList.add("hidden");
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
chart = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showChart() {
|
||||
if (placeholder) placeholder.classList.add("hidden");
|
||||
if (canvas) canvas.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function createChart(data: any) {
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
}
|
||||
|
||||
chart = new Chart(canvas as ChartItem, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: [500,1000,1500,2000,2500,3000,3500],
|
||||
datasets: [
|
||||
{
|
||||
label: "Calibration Curve",
|
||||
data: data,
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // Disable animations to prevent text stretching during resize
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Duty Cycle'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Current'
|
||||
},
|
||||
min:0,
|
||||
max:6000,
|
||||
ticks: {
|
||||
stepSize: 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// create chart when calibration table data is available
|
||||
$calibration_table_data.subscribe((value) => {
|
||||
console.log("Calibration table data updated");
|
||||
createChart(value.values);
|
||||
showChart();
|
||||
});
|
||||
|
||||
// Request calibration data button
|
||||
document.getElementById("calibration-table-btn")?.addEventListener("click", () => {
|
||||
// reuest table
|
||||
get_calibration_table();
|
||||
});
|
||||
|
||||
// Temperature Chart Management
|
||||
let temperatureChart: Chart | null = null;
|
||||
const temperatureCanvas = document.getElementById("temperature-chart-canvas");
|
||||
|
||||
function createTemperatureChart() {
|
||||
if (temperatureChart) {
|
||||
temperatureChart.destroy();
|
||||
}
|
||||
|
||||
const chartData = $temperature_chart_data.get();
|
||||
|
||||
temperatureChart = new Chart(temperatureCanvas as ChartItem, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: chartData.timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: "Driver Temperature",
|
||||
data: chartData.driverTemps,
|
||||
borderColor: "#3B82F6", // blue-500
|
||||
backgroundColor: "#3B82F680",
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
{
|
||||
label: "Laser Temperature",
|
||||
data: chartData.laserTemps,
|
||||
borderColor: "#EF4444", // red-500
|
||||
backgroundColor: "#EF444480",
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // Disable animations for real-time performance
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 10 // Show only every nth label to avoid crowding
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Temperature (°C)'
|
||||
},
|
||||
min: 0,
|
||||
max: 100,
|
||||
ticks: {
|
||||
stepSize: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top' as const
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index' as const
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize temperature chart
|
||||
createTemperatureChart();
|
||||
|
||||
// Subscribe to temperature chart data updates
|
||||
$temperature_chart_data.subscribe((chartData) => {
|
||||
if (temperatureChart) {
|
||||
temperatureChart.data.labels = chartData.timestamps;
|
||||
temperatureChart.data.datasets[0].data = chartData.driverTemps;
|
||||
temperatureChart.data.datasets[1].data = chartData.laserTemps;
|
||||
temperatureChart.update('none'); // No animation for real-time
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize with placeholder
|
||||
showPlaceholder();
|
||||
</script>
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
---
|
||||
const pageTitle = "DCDC Control";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Mode Control -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Mode Control</h2>
|
||||
<div class="join w-full">
|
||||
<button class="btn join-item flex-1 btn-primary" id="dcdc-start">Start</button>
|
||||
<button class="btn join-item flex-1 btn-success" id="dcdc-run">Run</button>
|
||||
<button class="btn join-item flex-1 btn-error" id="dcdc-stop">Stop</button>
|
||||
<button class="btn join-item flex-1 btn-error error-striped" id="dcdc-error">Error</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voltage Control & Monitoring -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Voltage Control</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Target Output Voltage</span>
|
||||
<span class="label-text-alt" id="voltage-display">25V</span>
|
||||
</label>
|
||||
<input type="range" min="10" max="50" value="25" class="range range-primary" step="0.1" id="voltage-slider" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>10V</span>
|
||||
<span>20V</span>
|
||||
<span>30V</span>
|
||||
<span>40V</span>
|
||||
<span>50V</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-6">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Input Voltage</div>
|
||||
<div class="stat-value text-sm" id="input-voltage">24.5V</div>
|
||||
<div class="stat-desc">Range: 18-28V</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Output Voltage</div>
|
||||
<div class="stat-value text-sm" id="output-voltage">25.0V</div>
|
||||
<div class="stat-desc" id="voltage-accuracy">±0.1V</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-primary mr-2" id="apply-voltage">Apply</button>
|
||||
<button class="btn btn-outline" id="reset-voltage">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Current & Power</h2>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Current</div>
|
||||
<div class="stat-value text-sm" id="output-current">2.1A</div>
|
||||
<div class="stat-desc">Max: 5.0A</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Power</div>
|
||||
<div class="stat-value text-sm" id="output-power">52.5W</div>
|
||||
<div class="stat-desc">Max: 150W</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>Efficiency</span>
|
||||
<span id="efficiency">92.5%</span>
|
||||
</div>
|
||||
<progress class="progress progress-success w-full" value="92.5" max="100" id="efficiency-progress"></progress>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span>Temperature</span>
|
||||
<span id="dcdc-temp">45.2°C</span>
|
||||
</div>
|
||||
<progress class="progress progress-warning w-full" value="45.2" max="85" id="temp-progress"></progress>
|
||||
<div class="text-xs text-gray-500 mt-1">Warning at 85°C</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protection Settings -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Protection Settings</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Over-Current Limit (A)</span>
|
||||
</label>
|
||||
<input type="range" min="1" max="6" value="5" step="0.1" class="range range-error" id="overcurrent-limit" />
|
||||
<div class="w-full flex justify-between text-xs px-2">
|
||||
<span>1A</span>
|
||||
<span>3A</span>
|
||||
<span>6A</span>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt" id="overcurrent-display">5.0A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Over-Temperature (°C)</span>
|
||||
</label>
|
||||
<input type="range" min="60" max="100" value="85" step="1" class="range range-error" id="overtemp-limit" />
|
||||
<div class="w-full flex justify-between text-xs px-2">
|
||||
<span>60°C</span>
|
||||
<span>80°C</span>
|
||||
<span>100°C</span>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt" id="overtemp-display">85°C</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Under-Voltage (V)</span>
|
||||
</label>
|
||||
<input type="range" min="15" max="22" value="18" step="0.1" class="range range-warning" id="undervoltage-limit" />
|
||||
<div class="w-full flex justify-between text-xs px-2">
|
||||
<span>15V</span>
|
||||
<span>18V</span>
|
||||
<span>22V</span>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt" id="undervoltage-display">18.0V</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4 mt-6">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Auto-Start on Power</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" id="toggle-autostart" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Soft-Start Enable</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-softstart" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status & Diagnostics -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Status & Diagnostics</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">Operating Status</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span>PWM Frequency:</span>
|
||||
<span id="pwm-freq">100kHz</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Duty Cycle:</span>
|
||||
<span id="duty-cycle">65.2%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Switching Losses:</span>
|
||||
<span id="switching-losses">2.1W</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Uptime:</span>
|
||||
<span id="dcdc-uptime">2h 45m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">Error Log</h3>
|
||||
<div class="bg-base-200 p-3 rounded-lg h-32 overflow-y-auto" id="error-log">
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="text-success">14:32 - System started</div>
|
||||
<div class="text-info">14:33 - Voltage ramped to 25V</div>
|
||||
<div class="text-warning">14:35 - Temp spike to 60°C</div>
|
||||
<div class="text-success">14:36 - Normal operation</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-striped {
|
||||
background-image: repeating-linear-gradient(45deg, hsl(var(--er)) 0, hsl(var(--er)) 10px, hsl(var(--er) / 0.8) 10px, hsl(var(--er) / 0.8) 20px);
|
||||
animation: error-blink 0.25s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes error-blink {
|
||||
0% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.7; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Voltage slider
|
||||
const voltageSlider = document.getElementById('voltage-slider') as HTMLInputElement;
|
||||
const voltageDisplay = document.getElementById('voltage-display');
|
||||
|
||||
if (voltageSlider && voltageDisplay) {
|
||||
voltageSlider.addEventListener('input', function() {
|
||||
voltageDisplay.textContent = parseFloat(this.value).toFixed(1) + 'V';
|
||||
});
|
||||
}
|
||||
|
||||
// Protection limit sliders
|
||||
const sliders = [
|
||||
{ id: 'overcurrent-limit', display: 'overcurrent-display', unit: 'A', decimals: 1 },
|
||||
{ id: 'overtemp-limit', display: 'overtemp-display', unit: '°C', decimals: 0 },
|
||||
{ id: 'undervoltage-limit', display: 'undervoltage-display', unit: 'V', decimals: 1 }
|
||||
];
|
||||
|
||||
sliders.forEach(slider => {
|
||||
const element = document.getElementById(slider.id) as HTMLInputElement;
|
||||
const display = document.getElementById(slider.display);
|
||||
|
||||
if (element && display) {
|
||||
element.addEventListener('input', function() {
|
||||
const value = parseFloat(this.value).toFixed(slider.decimals);
|
||||
display.textContent = value + slider.unit;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DCDC mode buttons
|
||||
const modeButtons = ['dcdc-start', 'dcdc-run', 'dcdc-stop', 'dcdc-error'];
|
||||
modeButtons.forEach(buttonId => {
|
||||
const button = document.getElementById(buttonId);
|
||||
if (button) {
|
||||
button.addEventListener('click', function() {
|
||||
console.log('DCDC mode:', buttonId.replace('dcdc-', ''));
|
||||
// Send WebSocket command
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to update DCDC values (called from WebSocket)
|
||||
(window as any).updateDcdcValues = function(data: any) {
|
||||
const fields = [
|
||||
'input-voltage', 'output-voltage', 'output-current', 'output-power',
|
||||
'efficiency', 'dcdc-temp', 'pwm-freq', 'duty-cycle', 'switching-losses', 'dcdc-uptime'
|
||||
];
|
||||
|
||||
fields.forEach(field => {
|
||||
const element = document.getElementById(field);
|
||||
if (element && data[field.replace('-', '_')]) {
|
||||
element.textContent = data[field.replace('-', '_')];
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { useStore } from '@nanostores/preact';
|
||||
import { $wsConnectionStatus, $updateState } from './AppProviderStore';
|
||||
import UpdaterModal from './UpdaterModal';
|
||||
import UpdateSuccessPopup from './UpdateSuccessPopup';
|
||||
import UpdateErrorPopup from './UpdateErrorPopup';
|
||||
import { $isUpdaterModalOpen, $isSuccessPopupOpen, $isErrorPopupOpen, stateSetters } from './AppProviderStore';
|
||||
|
||||
// Extend window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
openStatusModal?: () => void;
|
||||
openFirmwareModal?: () => void;
|
||||
openUpdaterModal?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export default function GlobalModals() {
|
||||
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
|
||||
const [isFirmwareModalOpen, setIsFirmwareModalOpen] = useState(false);
|
||||
|
||||
const updateState = useStore($updateState);
|
||||
const isUpdaterModalOpen = useStore($isUpdaterModalOpen);
|
||||
const isSuccessPopupOpen = useStore($isSuccessPopupOpen);
|
||||
const isErrorPopupOpen = useStore($isErrorPopupOpen);
|
||||
|
||||
// Auto-open success/error popups when update state changes
|
||||
useEffect(() => {
|
||||
console.log("🎭 GlobalModals useEffect triggered:", {
|
||||
updateState,
|
||||
isSuccessPopupOpen,
|
||||
isErrorPopupOpen,
|
||||
isUpdaterModalOpen
|
||||
});
|
||||
|
||||
if (updateState === 'success' && !isSuccessPopupOpen) {
|
||||
console.log("🟢 Opening SUCCESS popup - update completed");
|
||||
stateSetters.setSuccessPopupOpen(true);
|
||||
} else if (updateState === 'restarting') {
|
||||
console.log("🔄 RESTARTING state - ensuring SUCCESS popup stays open for countdown");
|
||||
// Ensure success popup is open for restarting state (it will show restart UI)
|
||||
if (!isSuccessPopupOpen) {
|
||||
console.log("🔄 Opening SUCCESS popup for RESTARTING state - will show countdown");
|
||||
stateSetters.setSuccessPopupOpen(true);
|
||||
} else {
|
||||
console.log("🔄 SUCCESS popup already open - countdown should be visible");
|
||||
}
|
||||
} else if (updateState === 'error' && !isErrorPopupOpen) {
|
||||
console.log("❌ Opening ERROR popup - update failed");
|
||||
stateSetters.setErrorPopupOpen(true);
|
||||
} else {
|
||||
console.log("🎭 No modal action needed - conditions not met");
|
||||
}
|
||||
}, [updateState, isSuccessPopupOpen, isErrorPopupOpen]);
|
||||
|
||||
// Listen for global modal events
|
||||
if (typeof window !== 'undefined') {
|
||||
window.openStatusModal = () => setIsStatusModalOpen(true);
|
||||
window.openFirmwareModal = () => setIsFirmwareModalOpen(true);
|
||||
window.openUpdaterModal = () => {
|
||||
if (!$isUpdaterModalOpen.get()) {
|
||||
stateSetters.setUpdaterModalOpen(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Status Modal */}
|
||||
{isStatusModalOpen && (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">System Status</h3>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-bold">WebSocket Status</td>
|
||||
<td>{useStore($wsConnectionStatus)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">P4 Firmware Version</td>
|
||||
<td>1.2.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">C6 Firmware Version</td>
|
||||
<td>0.9.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">G4 Firmware Version</td>
|
||||
<td>2.1.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Activated Plugins</td>
|
||||
<td>- none -</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Serial Number</td>
|
||||
<td>HydroBuddy-00001</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Operating Hours</td>
|
||||
<td>127.5 h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button class="btn btn-primary">Customer Service</button>
|
||||
<button class="btn" onClick={() => setIsStatusModalOpen(false)}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={() => setIsStatusModalOpen(false)}></div>
|
||||
</dialog>
|
||||
)}
|
||||
|
||||
{/* Firmware Update Modal */}
|
||||
{isFirmwareModalOpen && (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg mb-4">Firmware Update</h3>
|
||||
|
||||
<div class="flex flex-col items-center justify-center py-6">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<p class="mt-4 text-center">Checking for firmware updates...</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn" onClick={() => setIsFirmwareModalOpen(false)}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={() => setIsFirmwareModalOpen(false)}></div>
|
||||
</dialog>
|
||||
)}
|
||||
|
||||
{/* Updater Modal */}
|
||||
<UpdaterModal
|
||||
isOpen={isUpdaterModalOpen}
|
||||
onClose={() => { stateSetters.setUpdaterModalOpen(false); }}
|
||||
/>
|
||||
|
||||
{/* Update Success Popup */}
|
||||
<UpdateSuccessPopup
|
||||
isOpen={isSuccessPopupOpen}
|
||||
onClose={() => {
|
||||
stateSetters.setSuccessPopupOpen(false);
|
||||
// Reset update state after popup is closed
|
||||
stateSetters.setUpdateState('initial');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Update Error Popup */}
|
||||
<UpdateErrorPopup
|
||||
isOpen={isErrorPopupOpen}
|
||||
onClose={() => {
|
||||
stateSetters.setErrorPopupOpen(false);
|
||||
// Reset update state after popup is closed
|
||||
stateSetters.setUpdateState('initial');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
src/components/HostnameChangedModal.astro
Normal file
43
src/components/HostnameChangedModal.astro
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<dialog id="hostname-changed-modal" class="modal modal-middle">
|
||||
<div class="modal-box w-11/12 sm:w-5/12 max-w-2xl flex flex-col">
|
||||
<h1 class="text-lg font-bold">Hostname Changed</h1>
|
||||
<div class="divider divider-primary"></div>
|
||||
|
||||
<p class="text-sm text-base-content/70 mb-4">
|
||||
The device hostname has been updated. You will be automatically redirected to the new address.
|
||||
</p>
|
||||
|
||||
<div class="bg-base-200 rounded-lg p-4 mb-5">
|
||||
<p class="text-xs text-base-content/50 uppercase tracking-wide mb-2">New address</p>
|
||||
<a
|
||||
id="hostname-changed-link"
|
||||
href="#"
|
||||
class="text-base font-mono font-bold text-primary hover:underline break-all"
|
||||
target="_self"
|
||||
>
|
||||
http://<span id="hostname-changed-new-name">...</span>.local
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-baseline text-sm mb-2">
|
||||
<span class="text-base-content/60">Redirecting automatically in</span>
|
||||
<span class="font-mono font-bold text-base">
|
||||
<span id="hostname-redirect-countdown">10</span>s
|
||||
</span>
|
||||
</div>
|
||||
<progress
|
||||
id="hostname-redirect-progress"
|
||||
class="progress progress-primary w-full"
|
||||
value="100"
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-auto">
|
||||
<a id="hostname-redirect-now-btn" href="#" class="btn btn-primary" target="_self">
|
||||
Redirect Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
---
|
||||
const pageTitle = "Logs";
|
||||
---
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">System Logs</h1>
|
||||
<div class="join">
|
||||
<button class="btn join-item">Export</button>
|
||||
<button class="btn join-item btn-primary">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-base-200 shadow-lg mb-4">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-lg">Filters</h2>
|
||||
|
||||
<form class="filter grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Log Level</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="All" checked />
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Error" />
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Warning" />
|
||||
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Info" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Date Range</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full">
|
||||
<option selected>Today</option>
|
||||
<option>Last 3 Days</option>
|
||||
<option>Last Week</option>
|
||||
<option>Last Month</option>
|
||||
<option>Custom Range</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Search</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<input type="text" placeholder="Search logs..." class="input input-bordered join-item w-full" />
|
||||
<button class="btn join-item">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 md:col-span-3">
|
||||
<button type="reset" class="btn btn-sm btn-outline">Reset Filters</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card bg-base-200 shadow-lg">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Level</th>
|
||||
<th>Component</th>
|
||||
<th>Message</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>2025-03-15 14:32:45</td>
|
||||
<td><div class="badge badge-error">ERROR</div></td>
|
||||
<td>Driver</td>
|
||||
<td>Failed to initialize temperature sensor</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:30:12</td>
|
||||
<td><div class="badge badge-warning">WARN</div></td>
|
||||
<td>Modulator</td>
|
||||
<td>PWM frequency outside optimal range</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:28:05</td>
|
||||
<td><div class="badge badge-info">INFO</div></td>
|
||||
<td>System</td>
|
||||
<td>User authentication successful</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:25:59</td>
|
||||
<td><div class="badge">DEBUG</div></td>
|
||||
<td>Control</td>
|
||||
<td>PID controller parameters updated</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:20:33</td>
|
||||
<td><div class="badge badge-info">INFO</div></td>
|
||||
<td>System</td>
|
||||
<td>Laser driver initialization complete</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:18:22</td>
|
||||
<td><div class="badge badge-success">SUCCESS</div></td>
|
||||
<td>Hardware</td>
|
||||
<td>All hardware components detected</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2025-03-15 14:15:07</td>
|
||||
<td><div class="badge badge-info">INFO</div></td>
|
||||
<td>System</td>
|
||||
<td>System boot initiated</td>
|
||||
<td><button class="btn btn-xs btn-ghost">Details</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div>
|
||||
<span class="text-sm">Showing 1-7 of 42 entries</span>
|
||||
</div>
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm">«</button>
|
||||
<button class="join-item btn btn-sm btn-active">1</button>
|
||||
<button class="join-item btn btn-sm">2</button>
|
||||
<button class="join-item btn btn-sm">3</button>
|
||||
<button class="join-item btn btn-sm">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { $freeInternalHeap, $totalInternalHeap } from './AppProviderStore';
|
||||
|
||||
interface MemoryProgressBarProps {
|
||||
showDetails?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function MemoryProgressBar({ showDetails = true, className = "" }: MemoryProgressBarProps) {
|
||||
const freeInternalHeap = useStore($freeInternalHeap);
|
||||
const totalInternalHeap = useStore($totalInternalHeap);
|
||||
|
||||
// Calculate used and percentage
|
||||
const usedInternalHeap = totalInternalHeap - freeInternalHeap;
|
||||
const usagePercentage = totalInternalHeap > 0 ? (usedInternalHeap / totalInternalHeap) * 100 : 0;
|
||||
const freePercentage = 100 - usagePercentage;
|
||||
|
||||
// Determine color based on usage
|
||||
const getProgressColor = (percentage: number) => {
|
||||
if (percentage < 50) return 'bg-green-500';
|
||||
if (percentage < 75) return 'bg-yellow-500';
|
||||
if (percentage < 90) return 'bg-orange-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
const getTextColor = (percentage: number) => {
|
||||
if (percentage < 50) return 'text-green-600';
|
||||
if (percentage < 75) return 'text-yellow-600';
|
||||
if (percentage < 90) return 'text-orange-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Create segments for the memory bar (like Linux htop)
|
||||
const totalSegments = 20;
|
||||
const usedSegments = Math.round((usagePercentage / 100) * totalSegments);
|
||||
|
||||
const getSegmentColor = (segmentIndex: number) => {
|
||||
if (segmentIndex >= usedSegments) return 'bg-gray-300'; // Empty
|
||||
|
||||
const percentageAtSegment = (segmentIndex / totalSegments) * 100;
|
||||
if (percentageAtSegment < 50) return 'bg-green-500';
|
||||
if (percentageAtSegment < 75) return 'bg-yellow-500';
|
||||
if (percentageAtSegment < 90) return 'bg-orange-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`space-y-2 ${className}`}>
|
||||
{/* Linux-style Memory Bar with segments */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-gray-700">Internal Heap</span>
|
||||
<div className="text-right">
|
||||
<span className={`text-sm font-medium ${getTextColor(usagePercentage)}`}>
|
||||
{usagePercentage.toFixed(1)}%
|
||||
</span>
|
||||
<div className="text-xs text-gray-600">
|
||||
{formatBytes(usedInternalHeap)}/{formatBytes(totalInternalHeap)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Memory segments */}
|
||||
<div className="flex space-x-0.5">
|
||||
{Array.from({ length: totalSegments }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-3 flex-1 transition-colors duration-300 ${getSegmentColor(i)}`}
|
||||
style={{ minWidth: '2px' }}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Stats */}
|
||||
{showDetails && (
|
||||
<div className="grid grid-cols-3 gap-3 text-xs text-gray-600">
|
||||
<div className="flex justify-between">
|
||||
<span>Used:</span>
|
||||
<span className="font-medium">{formatBytes(usedInternalHeap)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Free:</span>
|
||||
<span className="font-medium text-green-600">{formatBytes(freeInternalHeap)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Total:</span>
|
||||
<span className="font-medium">{formatBytes(totalInternalHeap)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,505 +0,0 @@
|
|||
---
|
||||
// This is server-side - runs at build time
|
||||
const pageTitle = "Modulation";
|
||||
---
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">Laser Modulation</h1>
|
||||
|
||||
<!-- Modulation Mode Selection -->
|
||||
<div class="card bg-base-200 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Modulation Mode</h2>
|
||||
|
||||
<!-- Main Mode Tabs - Keep horizontal even on mobile -->
|
||||
<div class="tabs tabs-boxed mb-4">
|
||||
<button class="tab tab-active flex-1" id="internal-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 12l2 2 4-4"/>
|
||||
<path d="M21 12c-1 0-3-1-3-3s2-3 3-3 3 1 3 3-2 3-3 3"/>
|
||||
<path d="M3 12c1 0 3-1 3-3s-2-3-3-3-3 1-3 3 2 3 3 3"/>
|
||||
<path d="M3 12h6m6 0h6"/>
|
||||
</svg>
|
||||
Internal
|
||||
</button>
|
||||
<button class="tab flex-1" id="external-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 3v12"/>
|
||||
<path d="M18 3v12"/>
|
||||
<path d="M6 21l12-9-12-3z"/>
|
||||
</svg>
|
||||
External
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Internal Modes - Stack on mobile -->
|
||||
<div id="internal-modes" class="mb-4">
|
||||
<h3 class="font-medium mb-3">Internal Modulation Modes</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
<button class="btn btn-active" id="cc-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
Constant Current
|
||||
</button>
|
||||
<button class="btn" id="cp-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 20v2"/>
|
||||
<path d="m4.93 4.93 1.41 1.41"/>
|
||||
<path d="m17.66 17.66 1.41 1.41"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="M20 12h2"/>
|
||||
<path d="m6.34 17.66-1.41 1.41"/>
|
||||
<path d="m19.07 4.93-1.41 1.41"/>
|
||||
</svg>
|
||||
Constant Power
|
||||
</button>
|
||||
<button class="btn" id="pattern-continuous-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l4 8 4-16 4 8h6"/>
|
||||
</svg>
|
||||
Pattern Continuous
|
||||
</button>
|
||||
<button class="btn" id="pattern-triggered-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 6l4-4 4 4"/>
|
||||
<path d="M12 3v9"/>
|
||||
<path d="M3 12h6l4 8 4-8h6"/>
|
||||
</svg>
|
||||
Pattern Triggered
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External Modes - Stack on mobile -->
|
||||
<div id="external-modes" class="mb-4 hidden">
|
||||
<h3 class="font-medium mb-3">External Modulation Modes</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<button class="btn btn-active" id="ttl-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M5 12v7"/>
|
||||
<path d="M19 12v7"/>
|
||||
<path d="M5 5v7"/>
|
||||
<path d="M19 5v7"/>
|
||||
</svg>
|
||||
TTL
|
||||
</button>
|
||||
<button class="btn" id="mod2current-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M7 12l4-4 4 4 4-4"/>
|
||||
</svg>
|
||||
Mod2Current
|
||||
</button>
|
||||
<button class="btn" id="schmitt-trigger-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l2-8 2 16 2-8h6"/>
|
||||
</svg>
|
||||
Schmitt Trigger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Configuration Panels -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
|
||||
<!-- Constant Current Panel -->
|
||||
<div id="cc-panel" class="card bg-base-200 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
Constant Current Mode
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Current (A)</span>
|
||||
<span class="label-text-alt">5.0A</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="10" value="5" step="0.1" class="range range-primary" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>0A</span>
|
||||
<span>2.5A</span>
|
||||
<span>5.0A</span>
|
||||
<span>7.5A</span>
|
||||
<span>10.0A</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Soft Start</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Constant Current mode provides stable current output regardless of load changes.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Constant Power Panel -->
|
||||
<div id="cp-panel" class="card bg-base-200 shadow hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 20v2"/>
|
||||
<path d="m4.93 4.93 1.41 1.41"/>
|
||||
<path d="m17.66 17.66 1.41 1.41"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="M20 12h2"/>
|
||||
<path d="m6.34 17.66-1.41 1.41"/>
|
||||
<path d="m19.07 4.93-1.41 1.41"/>
|
||||
</svg>
|
||||
Constant Power Mode
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Power (W)</span>
|
||||
<span class="label-text-alt">5.0W</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="10" value="5" step="0.1" class="range range-secondary" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>0W</span>
|
||||
<span>2.5W</span>
|
||||
<span>5.0W</span>
|
||||
<span>7.5W</span>
|
||||
<span>10.0W</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Feedback Control</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Constant Power mode uses feedback from the photodiode to maintain stable output power.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pattern Continuous Panel -->
|
||||
<div id="pattern-continuous-panel" class="card bg-base-200 shadow hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l4 8 4-16 4 8h6"/>
|
||||
</svg>
|
||||
Pattern Continuous Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-base-200 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2v20"/>
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">Pattern configuration interface under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pattern Triggered Panel -->
|
||||
<div id="pattern-triggered-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-warning hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-warning">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 6l4-4 4 4"/>
|
||||
<path d="M12 3v9"/>
|
||||
<path d="M3 12h6l4 8 4-8h6"/>
|
||||
</svg>
|
||||
Pattern Triggered Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-warning/5 border border-warning/20 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-warning opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M8 6l4-4 4 4"/>
|
||||
<path d="M12 3v9"/>
|
||||
<path d="M3 12h6l4 8 4-8h6"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">Triggered pattern configuration interface under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TTL Panel -->
|
||||
<div id="ttl-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-info hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-info">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M5 12v7"/>
|
||||
<path d="M19 12v7"/>
|
||||
<path d="M5 5v7"/>
|
||||
<path d="M19 5v7"/>
|
||||
</svg>
|
||||
TTL Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-info/5 border border-info/20 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-info opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M5 12v7"/>
|
||||
<path d="M19 12v7"/>
|
||||
<path d="M5 5v7"/>
|
||||
<path d="M19 5v7"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">TTL interface configuration under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mod2Current Panel -->
|
||||
<div id="mod2current-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-success hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-success">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3v18h18"/>
|
||||
<path d="M7 12l4-4 4 4 4-4"/>
|
||||
</svg>
|
||||
Mod2Current Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-success/5 border border-success/20 rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
|
||||
<button class="btn btn-active" onclick="if('vibrate' in navigator) navigator.vibrate(15)">0-5V</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">0-10V</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Custom</button>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Current Limit (A)</span>
|
||||
<span class="label-text-alt">7.5A</span>
|
||||
</label>
|
||||
<input type="range" min="0" max="10" value="7.5" step="0.1" class="range range-success" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Mod2Current converts an analog input voltage to a proportional current output.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schmitt Trigger Panel -->
|
||||
<div id="schmitt-trigger-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-error hidden">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-error">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l2-8 2 16 2-8h6"/>
|
||||
</svg>
|
||||
Schmitt Trigger Mode
|
||||
</h2>
|
||||
|
||||
<div class="bg-error/5 border border-error/20 rounded-lg p-6">
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 text-error opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h6l2-8 2 16 2-8h6"/>
|
||||
</svg>
|
||||
<p class="text-lg font-bold">Coming Soon</p>
|
||||
<p class="text-sm mt-2">Schmitt Trigger configuration interface under development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Advanced Parameters -->
|
||||
<div class="card bg-base-200 shadow-xl border-l-4 border-l-primary">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/>
|
||||
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 22v-2"/>
|
||||
<path d="m17 20.66-1-1.73"/>
|
||||
<path d="M11 10.27 7 3.34"/>
|
||||
<path d="m20.66 17-1.73-1"/>
|
||||
<path d="m3.34 7 1.73 1"/>
|
||||
<path d="M14 12h8"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="m20.66 7-1.73 1"/>
|
||||
<path d="m3.34 17 1.73-1"/>
|
||||
<path d="m17 3.34-1 1.73"/>
|
||||
<path d="m11 13.73-4 6.93"/>
|
||||
</svg>
|
||||
Advanced Parameters
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Response Time</span>
|
||||
</label>
|
||||
<label class="select select-bordered w-full flex items-center">
|
||||
<svg class="w-4 h-4 opacity-70 absolute left-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
<select class="pl-10">
|
||||
<option>Fast (High Bandwidth)</option>
|
||||
<option selected>Normal</option>
|
||||
<option>Slow (Low Noise)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Stability Mode</span>
|
||||
</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<button class="btn btn-active" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Normal</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">High Stability</button>
|
||||
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Low Noise</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-end mt-4 gap-2">
|
||||
<button class="btn btn-outline" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Save Preset</button>
|
||||
<button class="btn btn-primary" onclick="if('vibrate' in navigator) navigator.vibrate([100, 200, 100])">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
// Tab switching logic
|
||||
const internalTab = document.getElementById('internal-tab');
|
||||
const externalTab = document.getElementById('external-tab');
|
||||
const internalModes = document.getElementById('internal-modes');
|
||||
const externalModes = document.getElementById('external-modes');
|
||||
|
||||
// Mode panels
|
||||
const panels = {
|
||||
'cc': document.getElementById('cc-panel'),
|
||||
'cp': document.getElementById('cp-panel'),
|
||||
'pattern-continuous': document.getElementById('pattern-continuous-panel'),
|
||||
'pattern-triggered': document.getElementById('pattern-triggered-panel'),
|
||||
'ttl': document.getElementById('ttl-panel'),
|
||||
'mod2current': document.getElementById('mod2current-panel'),
|
||||
'schmitt-trigger': document.getElementById('schmitt-trigger-panel')
|
||||
};
|
||||
|
||||
// Mode buttons
|
||||
const modeButtons = {
|
||||
'cc': document.getElementById('cc-tab'),
|
||||
'cp': document.getElementById('cp-tab'),
|
||||
'pattern-continuous': document.getElementById('pattern-continuous-tab'),
|
||||
'pattern-triggered': document.getElementById('pattern-triggered-tab'),
|
||||
'ttl': document.getElementById('ttl-tab'),
|
||||
'mod2current': document.getElementById('mod2current-tab'),
|
||||
'schmitt-trigger': document.getElementById('schmitt-trigger-tab')
|
||||
};
|
||||
|
||||
// Show/hide panels
|
||||
function showPanel(panelId: string) {
|
||||
Object.values(panels).forEach(panel => panel?.classList.add('hidden'));
|
||||
panels[panelId as keyof typeof panels]?.classList.remove('hidden');
|
||||
|
||||
// Update button states
|
||||
Object.values(modeButtons).forEach(btn => btn?.classList.remove('btn-active'));
|
||||
modeButtons[panelId as keyof typeof modeButtons]?.classList.add('btn-active');
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
if (internalTab && externalTab && internalModes && externalModes) {
|
||||
internalTab.addEventListener('click', function() {
|
||||
internalTab.classList.add('tab-active');
|
||||
externalTab.classList.remove('tab-active');
|
||||
internalModes.classList.remove('hidden');
|
||||
externalModes.classList.add('hidden');
|
||||
showPanel('cc'); // Show default internal mode
|
||||
});
|
||||
|
||||
externalTab.addEventListener('click', function() {
|
||||
externalTab.classList.add('tab-active');
|
||||
internalTab.classList.remove('tab-active');
|
||||
externalModes.classList.remove('hidden');
|
||||
internalModes.classList.add('hidden');
|
||||
showPanel('ttl'); // Show default external mode
|
||||
});
|
||||
}
|
||||
|
||||
// Mode button event listeners
|
||||
Object.entries(modeButtons).forEach(([mode, button]) => {
|
||||
if (button) {
|
||||
button.addEventListener('click', () => showPanel(mode));
|
||||
}
|
||||
});
|
||||
|
||||
// Stability mode buttons
|
||||
const stabilityButtons = document.querySelectorAll('.grid .btn');
|
||||
stabilityButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function(this: HTMLElement) {
|
||||
// Only update buttons in the same grid
|
||||
const grid = this.closest('.grid');
|
||||
if (grid) {
|
||||
grid.querySelectorAll('.btn').forEach(b => b.classList.remove('btn-active'));
|
||||
this.classList.add('btn-active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize with Constant Current mode
|
||||
showPanel('cc');
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
---
|
||||
// This is server-side - runs at build time
|
||||
const pageTitle = "Monitoring";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Real-time Metrics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Temperature</h2>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<div class="radial-progress text-primary" style="--value:65; --size:8rem;" id="temp-progress">65%</div>
|
||||
<div class="mt-2" id="temp-reading">25.6°C</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-2">
|
||||
<span>Min: <span id="temp-min">22.1°C</span></span>
|
||||
<span>Max: <span id="temp-max">28.3°C</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Current</h2>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<div class="radial-progress text-secondary" style="--value:42; --size:8rem;" id="current-progress">42%</div>
|
||||
<div class="mt-2" id="current-reading">125mA</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-2">
|
||||
<span>Min: <span id="current-min">0mA</span></span>
|
||||
<span>Max: <span id="current-max">300mA</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Output Power</h2>
|
||||
<div class="flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<div class="radial-progress text-accent" style="--value:50; --size:8rem;" id="power-progress">50%</div>
|
||||
<div class="mt-2" id="power-reading">500mW</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-2">
|
||||
<span>Min: <span id="power-min">0mW</span></span>
|
||||
<span>Max: <span id="power-max">1000mW</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Health -->
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">System Health</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CPU Temperature</td>
|
||||
<td class="w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full h-4 bg-base-300 rounded-lg relative">
|
||||
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
|
||||
<span>0°C</span>
|
||||
<span>30°C</span>
|
||||
<span>60°C</span>
|
||||
<span>90°C</span>
|
||||
<span>120°C</span>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
|
||||
style="width: 35.25%; background: linear-gradient(90deg, #3b82f6 0%, #22c55e 37.5%, #f59e0b 75%, #ef4444 100%);" id="cpu-temp-bar">
|
||||
</div>
|
||||
</div>
|
||||
<span id="cpu-temp">42.3°C</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GaN Temperature</td>
|
||||
<td class="w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full h-4 bg-base-300 rounded-lg relative">
|
||||
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
|
||||
<span>0°C</span>
|
||||
<span>30°C</span>
|
||||
<span>60°C</span>
|
||||
<span>90°C</span>
|
||||
<span>120°C</span>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
|
||||
style="width: 54.8%; background: linear-gradient(90deg, #3b82f6 0%, #22c55e 37.5%, #f59e0b 75%, #ef4444 100%);" id="gan-temp-bar">
|
||||
</div>
|
||||
</div>
|
||||
<span id="gan-temp">65.8°C</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WiFi Signal Strength</td>
|
||||
<td class="w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full h-4 bg-base-300 rounded-lg relative">
|
||||
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
|
||||
<span>-120dBm</span>
|
||||
<span>-90dBm</span>
|
||||
<span>-60dBm</span>
|
||||
<span>-30dBm</span>
|
||||
<span>0dBm</span>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
|
||||
style="width: 65%; background: linear-gradient(90deg, #ef4444 0%, #f59e0b 40%, #22c55e 80%, #22c55e 100%);" id="wifi-signal-bar">
|
||||
</div>
|
||||
</div>
|
||||
<span id="wifi-signal">-42dBm</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Data -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Temperature History</h2>
|
||||
<div class="flex items-center justify-center h-64 bg-base-200 rounded-box">
|
||||
<p class="text-base-content opacity-50">Temperature Chart Placeholder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Power Output History</h2>
|
||||
<div class="flex items-center justify-center h-64 bg-base-200 rounded-box">
|
||||
<p class="text-base-content opacity-50">Power Chart Placeholder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update radial progress values
|
||||
function updateRadialProgress(elementId: string, value: number) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.setProperty('--value', value.toString());
|
||||
element.textContent = value + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update monitoring values (called from WebSocket)
|
||||
(window as any).updateMonitoringValues = function(data: any) {
|
||||
if (data.temperature) {
|
||||
const tempProgress = Math.round((data.temperature / 100) * 100);
|
||||
updateRadialProgress('temp-progress', tempProgress);
|
||||
|
||||
const tempReading = document.getElementById('temp-reading');
|
||||
if (tempReading) tempReading.textContent = data.temperature + '°C';
|
||||
}
|
||||
|
||||
if (data.current) {
|
||||
const currentProgress = Math.round((data.current / 300) * 100);
|
||||
updateRadialProgress('current-progress', currentProgress);
|
||||
|
||||
const currentReading = document.getElementById('current-reading');
|
||||
if (currentReading) currentReading.textContent = data.current + 'mA';
|
||||
}
|
||||
|
||||
if (data.power) {
|
||||
const powerProgress = Math.round((data.power / 1000) * 100);
|
||||
updateRadialProgress('power-progress', powerProgress);
|
||||
|
||||
const powerReading = document.getElementById('power-reading');
|
||||
if (powerReading) powerReading.textContent = data.power + 'mW';
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
234
src/components/NetworkManagerStore.tsx
Normal file
234
src/components/NetworkManagerStore.tsx
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import { atom } from "nanostores";
|
||||
import { $websocket } from "./WebSocketAtom";
|
||||
|
||||
// Mirrors network_config_command_id in components/types_and_commands/inc/command_interface.h.
|
||||
// UI only uses APPLY_CONFIG, SET_MDNS_HOSTNAME, and GET_WIFI_NETWORKS — the rest are legacy single-purpose
|
||||
// commands that APPLY_CONFIG supersedes.
|
||||
export enum NetworkManagerCommands {
|
||||
NETWORK_CMD_SET_WIFI_STATION_MODE = 0, // legacy: configure WiFi Station mode
|
||||
NETWORK_CMD_SET_WIFI_AP_MODE = 1, // legacy: configure WiFi Access Point mode
|
||||
NETWORK_CMD_SET_STATIC_IP_STATION_MODE = 2, // legacy: set static IP for Station mode
|
||||
NETWORK_CMD_SET_MDNS_HOSTNAME = 3, // Set MDNS hostname
|
||||
NETWORK_CMD_GET_WIFI_NETWORKS = 4, // Scan and return available WiFi networks
|
||||
NETWORK_CMD_APPLY_CONFIG = 5, // Apply full network configuration (mode + IPs + credentials)
|
||||
NETWORK_CMD_UNKNOWN = 6, // Unknown Command
|
||||
}
|
||||
|
||||
// Mirrors connection_mode_t in components/types_and_commands/inc/data_types.h.
|
||||
// No ethernet on this board — only WiFi APSTA or WiFi station.
|
||||
export enum ConnectionMode {
|
||||
CONNECTION_MODE_UNKNOWN = 0, // Not yet configured; firmware defaults to APSTA on boot
|
||||
CONNECTION_MODE_APSTA = 1, // WiFi APSTA (default before user configures STA)
|
||||
CONNECTION_MODE_STA_DHCP = 2, // WiFi station, DHCP
|
||||
CONNECTION_MODE_STA_STATIC = 3, // WiFi station, static IP
|
||||
}
|
||||
|
||||
// Mirrors network_config_nvs in components/nvs_management/inc/nvs_management.h.
|
||||
export type NetworkConfigNVS = {
|
||||
Connection_Mode: ConnectionMode;
|
||||
DHCP_Mode_Station: boolean; // true = DHCP, false = Static IP (DHCP off)
|
||||
AP_Channel: number; // AP Channel, if AP is configured
|
||||
IP_Address_Station: string; // actual Station IP Address
|
||||
Gateway_IP_Address_Station: string; // actual Gateway IP Address for Station Mode
|
||||
Netmask_Station: string; // actual Netmask for Station Mode
|
||||
Static_Cfg_IP_Address_Station: string; // saved configuration static IP Address for Station Mode
|
||||
Static_Cfg_Gateway_IP_Address_Station: string; // saved configuration Gateway IP Address for Station Mode
|
||||
Static_Cfg_Netmask_Station: string; // saved configuration Netmask for Station Mode
|
||||
SSID_Station: string; // Station SSID saved
|
||||
Password_Station: string; // Station Password saved
|
||||
SSID_AP: string; // AP SSID saved
|
||||
Password_AP: string; // AP Password saved
|
||||
MDNS_Hostname: string; // MDNS Hostname saved
|
||||
};
|
||||
|
||||
export const $current_network_configuration = atom<NetworkConfigNVS>({
|
||||
Connection_Mode: ConnectionMode.CONNECTION_MODE_UNKNOWN,
|
||||
DHCP_Mode_Station: true,
|
||||
AP_Channel: 0,
|
||||
IP_Address_Station: "",
|
||||
Gateway_IP_Address_Station: "",
|
||||
Netmask_Station: "",
|
||||
Static_Cfg_IP_Address_Station: "",
|
||||
Static_Cfg_Gateway_IP_Address_Station: "",
|
||||
Static_Cfg_Netmask_Station: "",
|
||||
SSID_Station: "",
|
||||
Password_Station: "",
|
||||
SSID_AP: "",
|
||||
Password_AP: "",
|
||||
MDNS_Hostname: "hydrobuddy",
|
||||
});
|
||||
|
||||
export type WiFiNetwork = {
|
||||
SSID: string; // SSID of the network
|
||||
RSSI: number; // Signal strength in dBm
|
||||
Channel: number; // Channel number
|
||||
SecurityType: string; // Security type (WPA, WPA2, WEP, etc.)
|
||||
};
|
||||
|
||||
export function authmodeToSecurity(mode: number): string {
|
||||
switch (mode) {
|
||||
case 0:
|
||||
return "Open";
|
||||
case 1:
|
||||
return "WEP";
|
||||
case 2:
|
||||
return "WPA";
|
||||
case 3:
|
||||
return "WPA2";
|
||||
case 4:
|
||||
return "WPA/WPA2";
|
||||
case 5:
|
||||
return "WPA3";
|
||||
case 6:
|
||||
return "WPA2-Enterprise";
|
||||
case 7:
|
||||
return "WPA3-Enterprise";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export const $scanned_wifi_networks = atom<WiFiNetwork[]>([]);
|
||||
|
||||
// Live status emitted by network_manager_task ~every 500 ms via "network_status_update".
|
||||
// Fields with no ethernet equivalent and no ws_connected_clients (intentionally dropped — single-purpose web UI).
|
||||
export type NetworkStatusDisplay = {
|
||||
Connection_Mode: ConnectionMode;
|
||||
AP_Channel: number;
|
||||
IP_Address_Station: string;
|
||||
Gateway_IP_Address_Station: string;
|
||||
Netmask_Station: string;
|
||||
MDNS_Hostname: string;
|
||||
MAC_Address_WiFi: string;
|
||||
rssi: number | null; // null when WiFi STA not connected
|
||||
};
|
||||
|
||||
export const $network_status_display = atom<NetworkStatusDisplay>({
|
||||
Connection_Mode: ConnectionMode.CONNECTION_MODE_UNKNOWN,
|
||||
AP_Channel: 0,
|
||||
IP_Address_Station: "",
|
||||
Gateway_IP_Address_Station: "",
|
||||
Netmask_Station: "",
|
||||
MDNS_Hostname: "",
|
||||
MAC_Address_WiFi: "",
|
||||
rssi: null,
|
||||
});
|
||||
|
||||
/* StationConfigModal / wizard helper nanostores */
|
||||
|
||||
// depending on radio button in modal or on current network configuration, the toggle shall be set/unset. Used for state saving/retrieval.
|
||||
export const $wifi_static_ip_config_enabled = atom<boolean>(false);
|
||||
export const $selected_ssid = atom<string>("");
|
||||
export const $wifi_network_pw = atom<string>("");
|
||||
export const $temporary_static_ip_address = atom<string>("");
|
||||
export const $temporary_static_ip_subnet_mask = atom<string>("");
|
||||
export const $temporary_static_ip_gateway_ip_address = atom<string>("");
|
||||
|
||||
export const $hostname_input_field = atom<string>("");
|
||||
|
||||
function wsOpen(): WebSocket | null {
|
||||
const ws = $websocket.get();
|
||||
return ws && (ws as WebSocket).readyState === WebSocket.OPEN ? (ws as WebSocket) : null;
|
||||
}
|
||||
|
||||
export function send_new_network_configuration_to_alpha(network_config: NetworkConfigNVS): boolean {
|
||||
// send the network configuration to the device
|
||||
const ws = wsOpen();
|
||||
if (!ws) {
|
||||
console.error("WebSocket not connected, cannot send new network configuration!");
|
||||
return false;
|
||||
}
|
||||
console.log("Sending new network configuration!");
|
||||
console.log("Current configuration: ", $current_network_configuration.get());
|
||||
console.log("New configuration: ", network_config);
|
||||
|
||||
const mode = network_config.Connection_Mode;
|
||||
const hasApsta = mode === ConnectionMode.CONNECTION_MODE_APSTA;
|
||||
const hasSta = mode === ConnectionMode.CONNECTION_MODE_STA_DHCP || mode === ConnectionMode.CONNECTION_MODE_STA_STATIC;
|
||||
const hasStaticSta = mode === ConnectionMode.CONNECTION_MODE_STA_STATIC;
|
||||
|
||||
if (mode === ConnectionMode.CONNECTION_MODE_UNKNOWN) {
|
||||
console.error("Cannot send config with UNKNOWN connection mode!");
|
||||
return false;
|
||||
}
|
||||
if (hasApsta && (network_config.SSID_AP === "" || network_config.SSID_AP.length >= 32 || network_config.Password_AP.length >= 32)) {
|
||||
console.error("APSTA AP SSID/password invalid!");
|
||||
return false;
|
||||
}
|
||||
if (hasSta && (network_config.SSID_Station === "" || network_config.SSID_Station.length >= 32 || network_config.Password_Station.length >= 32)) {
|
||||
console.error("STA SSID/password invalid!");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
hasStaticSta &&
|
||||
(network_config.Static_Cfg_IP_Address_Station === "" ||
|
||||
network_config.Static_Cfg_Netmask_Station === "" ||
|
||||
network_config.Static_Cfg_Gateway_IP_Address_Station === "")
|
||||
) {
|
||||
console.error("Static STA IP fields missing!");
|
||||
return false;
|
||||
}
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
message_type: "network_cmd",
|
||||
network_command: NetworkManagerCommands.NETWORK_CMD_APPLY_CONFIG,
|
||||
network_config: network_config,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Request the firmware to broadcast its current network_config (mode + saved SSID etc.).
|
||||
// Called on WebSocket open so the UI knows whether to show the wizard or the overview.
|
||||
export const request_network_config = (): void => {
|
||||
wsOpen()?.send(JSON.stringify({ message_type: "request_network_config" }));
|
||||
};
|
||||
|
||||
export const scan_wifi_networks = (): void => {
|
||||
$scanned_wifi_networks.set([]);
|
||||
wsOpen()?.send(
|
||||
JSON.stringify({
|
||||
message_type: "network_cmd",
|
||||
network_command: NetworkManagerCommands.NETWORK_CMD_GET_WIFI_NETWORKS,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const set_hostname = (hostname: string): boolean => {
|
||||
const ws = wsOpen();
|
||||
if (!ws) {
|
||||
console.error("WebSocket not connected, cannot set hostname!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hostname || hostname.length === 0 || hostname.length > 31) {
|
||||
console.error("Invalid hostname! Must be 1-31 characters.");
|
||||
return false;
|
||||
} else if (hostname === $current_network_configuration.get().MDNS_Hostname) {
|
||||
return false;
|
||||
}
|
||||
console.log("Setting MDNS hostname:", hostname);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
message_type: "set_network_hostname",
|
||||
network_command: NetworkManagerCommands.NETWORK_CMD_SET_MDNS_HOSTNAME,
|
||||
hostname: hostname,
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const clear_scanned_wifi_networks = (): void => {
|
||||
$scanned_wifi_networks.set([]);
|
||||
};
|
||||
|
||||
export type NetworkConfigError = {
|
||||
reason: string;
|
||||
failed_ssid: string;
|
||||
connection_mode: ConnectionMode;
|
||||
};
|
||||
|
||||
export const $network_config_error = atom<NetworkConfigError | null>(null);
|
||||
543
src/components/NetworkOverview.astro
Normal file
543
src/components/NetworkOverview.astro
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
---
|
||||
import { IconPlant } from "@tabler/icons-preact";
|
||||
---
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header strip: title + current mode badge + reconfigure shortcut -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body flex flex-row items-center justify-between flex-wrap gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-emerald-600"><IconPlant size={28} strokeWidth={1.75} /></span>
|
||||
<div>
|
||||
<h2 class="card-title text-xl text-emerald-700">HydroBuddy is online</h2>
|
||||
<p class="text-sm opacity-70" id="overview-subtitle">Loading mode…</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="overview-forget-btn" class="btn btn-sm btn-ghost">Re-setup WiFi</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats grid (Mode / WiFi info / Station IP / Hostname) -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<!-- Connection Mode -->
|
||||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0">
|
||||
<div class="card-body p-4 gap-1">
|
||||
<h3 class="text-sm opacity-60 font-medium">Connection Mode</h3>
|
||||
<div class="text-xl font-bold text-emerald-700 leading-tight wrap-break-word" id="mode-display">—</div>
|
||||
<div class="text-xs opacity-60" id="mode-sub">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WiFi (SSID + signal/security) -->
|
||||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0" id="wifi-card">
|
||||
<div class="card-body p-4 gap-1">
|
||||
<h3 class="text-sm opacity-60 font-medium" id="wifi-card-title">WiFi</h3>
|
||||
<div class="text-xl font-bold text-emerald-700 truncate" id="ssid-display">—</div>
|
||||
<div class="text-xs opacity-70 items-center gap-1 hidden" id="password-row">
|
||||
<span class="flex items-center gap-1">
|
||||
PW: <span id="password-display" data-password="">—</span>
|
||||
<button type="button" class="btn btn-ghost btn-xs p-0 min-h-0 h-4" id="toggle-password-visibility">
|
||||
<span id="pw-eye-text" class="text-xs opacity-70">show</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs opacity-70" id="rssi-row">RSSI: <span id="signal-display">—</span></div>
|
||||
<div class="text-xs opacity-60" id="signal-quality">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Station IP (hidden in APSTA mode — no STA IP exists then) -->
|
||||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0" id="station-ip-card">
|
||||
<div class="card-body p-4 gap-1">
|
||||
<h3 class="text-sm opacity-60 font-medium">Station IP</h3>
|
||||
<div class="text-xl font-bold text-emerald-700 truncate" id="sta-ip-display">—</div>
|
||||
<div class="text-xs opacity-70 truncate">GW: <span id="sta-gw-display">—</span></div>
|
||||
<div class="text-xs opacity-70 truncate">Mask: <span id="sta-mask-display">—</span></div>
|
||||
<div class="text-xs opacity-70 truncate">MAC: <span id="sta-mac-display">—</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hostname -->
|
||||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0">
|
||||
<div class="card-body p-4 gap-1">
|
||||
<h3 class="text-sm opacity-60 font-medium">Hostname</h3>
|
||||
<div class="text-xl font-bold text-emerald-700 truncate" id="hostname-display">—</div>
|
||||
<a
|
||||
class="text-xs opacity-70 truncate hover:opacity-100 hover:text-emerald-700 transition-opacity"
|
||||
id="hostname-link"
|
||||
href="http://hydrobuddy.local"
|
||||
target="_blank"
|
||||
>
|
||||
http://<span id="hostname-preview-card">hydrobuddy</span>.local
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hostname editor -->
|
||||
<div class="card bg-base-100 card-border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="font-semibold text-base mb-1">Device name</h3>
|
||||
<p class="text-sm opacity-70 mb-3">Reach the web interface at this name.local from any device on the same network.</p>
|
||||
<div class="flex gap-2 w-full">
|
||||
<label
|
||||
class="input flex-1 flex items-center gap-2 font-mono text-sm focus-within:bg-emerald-50 focus-within:outline-emerald-500"
|
||||
>
|
||||
<span class="opacity-60 select-none">http://</span>
|
||||
<input
|
||||
type="text"
|
||||
id="overview-hostname-input"
|
||||
placeholder="hydrobuddy"
|
||||
maxlength="31"
|
||||
class="grow min-w-0 bg-transparent border-none outline-none"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<span class="opacity-60 select-none">.local</span>
|
||||
</label>
|
||||
<button id="overview-hostname-apply" class="btn btn-primary" disabled>Apply</button>
|
||||
</div>
|
||||
<p id="overview-hostname-error" class="text-sm text-error hidden mt-2"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Assignment (STA only) -->
|
||||
<div class="card bg-base-100 card-border border-base-300" id="ip-assignment-card">
|
||||
<div class="card-body">
|
||||
<h3 class="font-semibold text-base mb-1">IP Address Assignment</h3>
|
||||
<p class="text-sm opacity-70 mb-3">DHCP is the safe default. Static IP only if your network requires it.</p>
|
||||
|
||||
<div class="join w-full mb-3">
|
||||
<input class="join-item btn flex-1" type="radio" name="ip-mode" aria-label="Automatic (DHCP)" value="dhcp" id="ip-mode-dhcp" />
|
||||
<input class="join-item btn flex-1" type="radio" name="ip-mode" aria-label="Static IP" value="static" id="ip-mode-static" />
|
||||
</div>
|
||||
|
||||
<div id="static-ip-fields" class="hidden space-y-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">IP Address</span>
|
||||
<input type="text" id="ov-static-ip" placeholder="192.168.1.50" class="input input-bordered input-sm font-mono" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">Netmask</span>
|
||||
<input type="text" id="ov-static-mask" placeholder="255.255.255.0" class="input input-bordered input-sm font-mono" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">Gateway</span>
|
||||
<input type="text" id="ov-static-gw" placeholder="192.168.1.1" class="input input-bordered input-sm font-mono" />
|
||||
</label>
|
||||
</div>
|
||||
<p id="ov-static-error" class="text-sm text-error hidden"></p>
|
||||
<button id="ov-static-apply" class="btn btn-primary btn-sm">Apply Static IP</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode switch -->
|
||||
<div class="card bg-base-100 card-border border-base-300">
|
||||
<div class="card-body">
|
||||
<h3 class="font-semibold text-base mb-1">WiFi Mode</h3>
|
||||
<p class="text-sm opacity-70 mb-3">Choose how the device connects.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<button
|
||||
class="h-auto py-4 mode-card bg-base-200 hover:bg-emerald-50 transition-all rounded-lg border-2 border-transparent hover:border-emerald-500"
|
||||
data-mode="station"
|
||||
id="overview-mode-station"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="font-semibold text-emerald-700">WiFi Station</span>
|
||||
<span class="text-xs opacity-70">Join an existing WiFi network</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="h-auto py-4 mode-card bg-base-200 hover:bg-emerald-50 transition-all rounded-lg border-2 border-transparent hover:border-emerald-500"
|
||||
data-mode="apsta"
|
||||
id="overview-mode-apsta"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="font-semibold text-emerald-700">Access Point</span>
|
||||
<span class="text-xs opacity-70">Create a HydroBuddy hotspot</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inline APSTA form (shown when "Access Point" is clicked) -->
|
||||
<div id="apsta-form" class="hidden mt-4 space-y-3 p-4 rounded-lg bg-base-200/50">
|
||||
<h4 class="font-semibold text-sm">Configure Access Point</h4>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">AP SSID</span>
|
||||
<input type="text" id="ov-ap-ssid" maxlength="31" class="input input-bordered input-sm" />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">AP Password (leave blank for open)</span>
|
||||
<input type="text" id="ov-ap-password" maxlength="31" class="input input-bordered input-sm font-mono" />
|
||||
</label>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button id="ov-ap-cancel" class="btn btn-ghost btn-sm">Cancel</button>
|
||||
<button id="ov-ap-apply" class="btn btn-primary btn-sm">Apply AP Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network-error dialog (raised when network_config_failed arrives) -->
|
||||
<dialog id="overview-network-error-modal" class="modal modal-middle">
|
||||
<div class="modal-box w-11/12 sm:w-5/12 max-w-lg">
|
||||
<h3 class="text-lg font-bold mb-1 text-error">Network configuration failed</h3>
|
||||
<div class="divider mt-2 mb-3"></div>
|
||||
<div class="alert alert-error py-3">
|
||||
<span id="ov-error-text">Configuration could not be applied.</span>
|
||||
</div>
|
||||
<p class="text-sm opacity-70 mt-3">
|
||||
The device has restored the previous working configuration. Check your credentials or IP settings and try again.
|
||||
</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog"><button class="btn btn-primary">OK</button></form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
import {
|
||||
ConnectionMode,
|
||||
$current_network_configuration,
|
||||
$network_status_display,
|
||||
$network_config_error,
|
||||
type NetworkConfigNVS,
|
||||
send_new_network_configuration_to_alpha,
|
||||
set_hostname,
|
||||
} from "../components/NetworkManagerStore";
|
||||
import {
|
||||
isValidHostname,
|
||||
normalizeHostnameValue,
|
||||
} from "../components/helper_scripts/hostname_validation";
|
||||
import {
|
||||
is_valid_ip_format,
|
||||
isValidNetmask,
|
||||
isPracticalNetmask,
|
||||
isInSameSubnet,
|
||||
isInValidHostRange,
|
||||
} from "../components/helper_scripts/IP_Subnetmask_validation";
|
||||
|
||||
// ---------- DOM lookup ----------
|
||||
const subtitleEl = document.getElementById("overview-subtitle");
|
||||
const modeDisplay = document.getElementById("mode-display");
|
||||
const modeSub = document.getElementById("mode-sub");
|
||||
const wifiCardTitle = document.getElementById("wifi-card-title");
|
||||
const ssidDisplay = document.getElementById("ssid-display");
|
||||
const passwordRow = document.getElementById("password-row");
|
||||
const passwordDisplay = document.getElementById("password-display") as HTMLSpanElement | null;
|
||||
const passwordToggle = document.getElementById("toggle-password-visibility");
|
||||
const pwEyeText = document.getElementById("pw-eye-text");
|
||||
const rssiRow = document.getElementById("rssi-row");
|
||||
const signalDisplay = document.getElementById("signal-display");
|
||||
const signalQuality = document.getElementById("signal-quality");
|
||||
|
||||
const stationIpCard = document.getElementById("station-ip-card");
|
||||
const staIpDisplay = document.getElementById("sta-ip-display");
|
||||
const staGwDisplay = document.getElementById("sta-gw-display");
|
||||
const staMaskDisplay = document.getElementById("sta-mask-display");
|
||||
const staMacDisplay = document.getElementById("sta-mac-display");
|
||||
|
||||
const hostnameDisplay = document.getElementById("hostname-display");
|
||||
const hostnameLink = document.getElementById("hostname-link") as HTMLAnchorElement | null;
|
||||
const hostnamePreviewCard = document.getElementById("hostname-preview-card");
|
||||
const hostnameInput = document.getElementById("overview-hostname-input") as HTMLInputElement | null;
|
||||
const hostnameError = document.getElementById("overview-hostname-error");
|
||||
const hostnameApply = document.getElementById("overview-hostname-apply") as HTMLButtonElement | null;
|
||||
|
||||
const ipAssignmentCard = document.getElementById("ip-assignment-card");
|
||||
const ipModeDhcp = document.getElementById("ip-mode-dhcp") as HTMLInputElement | null;
|
||||
const ipModeStatic = document.getElementById("ip-mode-static") as HTMLInputElement | null;
|
||||
const staticFields = document.getElementById("static-ip-fields");
|
||||
const ovStaticIp = document.getElementById("ov-static-ip") as HTMLInputElement | null;
|
||||
const ovStaticMask = document.getElementById("ov-static-mask") as HTMLInputElement | null;
|
||||
const ovStaticGw = document.getElementById("ov-static-gw") as HTMLInputElement | null;
|
||||
const ovStaticError = document.getElementById("ov-static-error");
|
||||
const ovStaticApply = document.getElementById("ov-static-apply") as HTMLButtonElement | null;
|
||||
|
||||
const modeStationBtn = document.getElementById("overview-mode-station");
|
||||
const modeApstaBtn = document.getElementById("overview-mode-apsta");
|
||||
const apstaForm = document.getElementById("apsta-form");
|
||||
const ovApSsid = document.getElementById("ov-ap-ssid") as HTMLInputElement | null;
|
||||
const ovApPassword = document.getElementById("ov-ap-password") as HTMLInputElement | null;
|
||||
const ovApCancel = document.getElementById("ov-ap-cancel");
|
||||
const ovApApply = document.getElementById("ov-ap-apply") as HTMLButtonElement | null;
|
||||
|
||||
const forgetBtn = document.getElementById("overview-forget-btn");
|
||||
const errorModal = document.getElementById("overview-network-error-modal") as HTMLDialogElement | null;
|
||||
const ovErrorText = document.getElementById("ov-error-text");
|
||||
|
||||
// Track password visibility per render — defaults to hidden each config refresh.
|
||||
let isPasswordVisible = false;
|
||||
|
||||
function modeLabel(mode: ConnectionMode): string {
|
||||
switch (mode) {
|
||||
case ConnectionMode.CONNECTION_MODE_APSTA:
|
||||
return "Access Point";
|
||||
case ConnectionMode.CONNECTION_MODE_STA_DHCP:
|
||||
return "WiFi Station (DHCP)";
|
||||
case ConnectionMode.CONNECTION_MODE_STA_STATIC:
|
||||
return "WiFi Station (Static IP)";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function isSta(mode: ConnectionMode): boolean {
|
||||
return mode === ConnectionMode.CONNECTION_MODE_STA_DHCP || mode === ConnectionMode.CONNECTION_MODE_STA_STATIC;
|
||||
}
|
||||
|
||||
function renderPassword(pw: string) {
|
||||
if (!passwordDisplay) return;
|
||||
passwordDisplay.dataset.password = pw;
|
||||
passwordDisplay.textContent = isPasswordVisible ? pw || "—" : pw ? "•".repeat(Math.min(pw.length, 12)) : "—";
|
||||
if (pwEyeText) pwEyeText.textContent = isPasswordVisible ? "hide" : "show";
|
||||
}
|
||||
|
||||
passwordToggle?.addEventListener("click", () => {
|
||||
isPasswordVisible = !isPasswordVisible;
|
||||
const pw = passwordDisplay?.dataset.password ?? "";
|
||||
renderPassword(pw);
|
||||
});
|
||||
|
||||
// ---------- Config subscription ----------
|
||||
$current_network_configuration.subscribe((cfg) => {
|
||||
const apsta = cfg.Connection_Mode === ConnectionMode.CONNECTION_MODE_APSTA;
|
||||
const sta = isSta(cfg.Connection_Mode);
|
||||
|
||||
if (modeDisplay) modeDisplay.textContent = modeLabel(cfg.Connection_Mode);
|
||||
if (modeSub)
|
||||
modeSub.textContent = apsta
|
||||
? "Your device is a hotspot"
|
||||
: sta
|
||||
? "Joined an existing network"
|
||||
: "Not yet configured";
|
||||
|
||||
if (wifiCardTitle) wifiCardTitle.textContent = apsta ? "WiFi Access Point" : "WiFi Station";
|
||||
if (ssidDisplay) ssidDisplay.textContent = apsta ? cfg.SSID_AP || "—" : sta ? cfg.SSID_Station || "—" : "—";
|
||||
|
||||
// Password row visible whenever a credential exists (STA or APSTA)
|
||||
const pw = apsta ? cfg.Password_AP : sta ? cfg.Password_Station : "";
|
||||
if (passwordRow) {
|
||||
const hasPw = !!pw;
|
||||
passwordRow.classList.toggle("hidden", !hasPw);
|
||||
passwordRow.classList.toggle("flex", hasPw);
|
||||
renderPassword(pw);
|
||||
}
|
||||
|
||||
// RSSI row only meaningful in STA mode (signal strength to the upstream AP).
|
||||
rssiRow?.classList.toggle("hidden", !sta);
|
||||
if (signalQuality && !sta) signalQuality.textContent = "";
|
||||
|
||||
// Station IP card only in STA modes
|
||||
stationIpCard?.classList.toggle("hidden", !sta);
|
||||
|
||||
// Hostname card + edit form
|
||||
if (hostnameDisplay) hostnameDisplay.textContent = cfg.MDNS_Hostname || "—";
|
||||
if (hostnamePreviewCard) hostnamePreviewCard.textContent = cfg.MDNS_Hostname || "hydrobuddy";
|
||||
if (hostnameLink) hostnameLink.href = `http://${cfg.MDNS_Hostname || "hydrobuddy"}.local`;
|
||||
// Prime the input only if the user hasn't started typing.
|
||||
if (hostnameInput && (!hostnameInput.value || hostnameInput.value === hostnameInput.defaultValue)) {
|
||||
hostnameInput.value = cfg.MDNS_Hostname || "";
|
||||
hostnameInput.defaultValue = hostnameInput.value;
|
||||
}
|
||||
|
||||
// IP Assignment card — only in STA mode
|
||||
ipAssignmentCard?.classList.toggle("hidden", !sta);
|
||||
if (sta) {
|
||||
if (cfg.DHCP_Mode_Station) {
|
||||
if (ipModeDhcp) ipModeDhcp.checked = true;
|
||||
staticFields?.classList.add("hidden");
|
||||
} else {
|
||||
if (ipModeStatic) ipModeStatic.checked = true;
|
||||
staticFields?.classList.remove("hidden");
|
||||
}
|
||||
if (ovStaticIp) ovStaticIp.value = cfg.Static_Cfg_IP_Address_Station || "";
|
||||
if (ovStaticMask) ovStaticMask.value = cfg.Static_Cfg_Netmask_Station || "";
|
||||
if (ovStaticGw) ovStaticGw.value = cfg.Static_Cfg_Gateway_IP_Address_Station || "";
|
||||
}
|
||||
|
||||
// Subtitle in the header strip
|
||||
if (subtitleEl) subtitleEl.textContent = `${modeLabel(cfg.Connection_Mode)}`;
|
||||
});
|
||||
|
||||
// ---------- Status subscription (IP/RSSI/MAC live updates) ----------
|
||||
$network_status_display.subscribe((s) => {
|
||||
if (staIpDisplay) staIpDisplay.textContent = s.IP_Address_Station || "—";
|
||||
if (staGwDisplay) staGwDisplay.textContent = s.Gateway_IP_Address_Station || "—";
|
||||
if (staMaskDisplay) staMaskDisplay.textContent = s.Netmask_Station || "—";
|
||||
if (staMacDisplay) staMacDisplay.textContent = s.MAC_Address_WiFi || "—";
|
||||
|
||||
if (signalDisplay) signalDisplay.textContent = s.rssi != null ? `${s.rssi} dBm` : "—";
|
||||
if (signalQuality) {
|
||||
if (s.rssi == null) {
|
||||
signalQuality.textContent = isSta(s.Connection_Mode) ? "Not connected" : "";
|
||||
} else {
|
||||
const r = s.rssi;
|
||||
signalQuality.textContent = r >= -50 ? "Excellent" : r >= -60 ? "Good" : r >= -70 ? "Fair" : "Weak";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- Hostname editing ----------
|
||||
function validateHostnameField() {
|
||||
if (!hostnameInput || !hostnameError || !hostnameApply) return;
|
||||
const raw = hostnameInput.value;
|
||||
const normalized = normalizeHostnameValue(raw);
|
||||
if (normalized !== raw) hostnameInput.value = normalized;
|
||||
if (!normalized) {
|
||||
hostnameError.classList.add("hidden");
|
||||
hostnameApply.disabled = true;
|
||||
return;
|
||||
}
|
||||
if (!isValidHostname(normalized)) {
|
||||
hostnameError.textContent = "Invalid hostname. Use a-z, 0-9, hyphens. No leading/trailing hyphen.";
|
||||
hostnameError.classList.remove("hidden");
|
||||
hostnameApply.disabled = true;
|
||||
return;
|
||||
}
|
||||
hostnameError.classList.add("hidden");
|
||||
// Don't enable if it matches what the device already has.
|
||||
hostnameApply.disabled = normalized === $current_network_configuration.get().MDNS_Hostname;
|
||||
}
|
||||
hostnameInput?.addEventListener("input", validateHostnameField);
|
||||
hostnameApply?.addEventListener("click", () => {
|
||||
if (!hostnameInput) return;
|
||||
set_hostname(hostnameInput.value.trim());
|
||||
});
|
||||
|
||||
// ---------- Static IP form ----------
|
||||
ipModeDhcp?.addEventListener("change", () => {
|
||||
staticFields?.classList.add("hidden");
|
||||
// Apply DHCP immediately — no fields needed.
|
||||
const baseCfg = $current_network_configuration.get();
|
||||
if (baseCfg.Connection_Mode !== ConnectionMode.CONNECTION_MODE_STA_DHCP) {
|
||||
send_new_network_configuration_to_alpha({
|
||||
...baseCfg,
|
||||
Connection_Mode: ConnectionMode.CONNECTION_MODE_STA_DHCP,
|
||||
DHCP_Mode_Station: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipModeStatic?.addEventListener("change", () => {
|
||||
staticFields?.classList.remove("hidden");
|
||||
});
|
||||
|
||||
function validateOvStaticIp(): { ok: boolean; ip: string; mask: string; gw: string } {
|
||||
if (!ovStaticIp || !ovStaticMask || !ovStaticGw || !ovStaticError) {
|
||||
return { ok: false, ip: "", mask: "", gw: "" };
|
||||
}
|
||||
const ip = ovStaticIp.value.trim();
|
||||
const mask = ovStaticMask.value.trim();
|
||||
const gw = ovStaticGw.value.trim();
|
||||
if (!ip || !mask || !gw) {
|
||||
ovStaticError.textContent = "All three fields are required.";
|
||||
ovStaticError.classList.remove("hidden");
|
||||
return { ok: false, ip, mask, gw };
|
||||
}
|
||||
if (!is_valid_ip_format(ip) || !is_valid_ip_format(gw)) {
|
||||
ovStaticError.textContent = "Invalid IPv4 format.";
|
||||
ovStaticError.classList.remove("hidden");
|
||||
return { ok: false, ip, mask, gw };
|
||||
}
|
||||
if (!isValidNetmask(mask) || !isPracticalNetmask(mask)) {
|
||||
ovStaticError.textContent = "Netmask is not practical (use e.g. 255.255.255.0).";
|
||||
ovStaticError.classList.remove("hidden");
|
||||
return { ok: false, ip, mask, gw };
|
||||
}
|
||||
if (!isInSameSubnet(ip, gw, mask)) {
|
||||
ovStaticError.textContent = "Gateway must be in the same subnet as the IP.";
|
||||
ovStaticError.classList.remove("hidden");
|
||||
return { ok: false, ip, mask, gw };
|
||||
}
|
||||
if (!isInValidHostRange(ip, mask) || !isInValidHostRange(gw, mask)) {
|
||||
ovStaticError.textContent = "Pick host addresses (not network/broadcast).";
|
||||
ovStaticError.classList.remove("hidden");
|
||||
return { ok: false, ip, mask, gw };
|
||||
}
|
||||
ovStaticError.classList.add("hidden");
|
||||
return { ok: true, ip, mask, gw };
|
||||
}
|
||||
ovStaticIp?.addEventListener("input", validateOvStaticIp);
|
||||
ovStaticMask?.addEventListener("input", validateOvStaticIp);
|
||||
ovStaticGw?.addEventListener("input", validateOvStaticIp);
|
||||
|
||||
ovStaticApply?.addEventListener("click", () => {
|
||||
const v = validateOvStaticIp();
|
||||
if (!v.ok) return;
|
||||
const baseCfg = $current_network_configuration.get();
|
||||
send_new_network_configuration_to_alpha({
|
||||
...baseCfg,
|
||||
Connection_Mode: ConnectionMode.CONNECTION_MODE_STA_STATIC,
|
||||
DHCP_Mode_Station: false,
|
||||
Static_Cfg_IP_Address_Station: v.ip,
|
||||
Static_Cfg_Netmask_Station: v.mask,
|
||||
Static_Cfg_Gateway_IP_Address_Station: v.gw,
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- Mode switch ----------
|
||||
modeStationBtn?.addEventListener("click", () => {
|
||||
// "Switch to Station" = re-run the wizard. Easiest path is to nuke SSID_Station
|
||||
// locally so index.astro shows the wizard. We don't push to the device yet —
|
||||
// the wizard's Apply step does that.
|
||||
const baseCfg = $current_network_configuration.get();
|
||||
$current_network_configuration.set({ ...baseCfg, SSID_Station: "" });
|
||||
});
|
||||
|
||||
modeApstaBtn?.addEventListener("click", () => {
|
||||
// Open inline AP config form, prefilled with current AP values
|
||||
const cfg = $current_network_configuration.get();
|
||||
if (ovApSsid) ovApSsid.value = cfg.SSID_AP || "";
|
||||
if (ovApPassword) ovApPassword.value = cfg.Password_AP || "";
|
||||
apstaForm?.classList.remove("hidden");
|
||||
apstaForm?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
});
|
||||
|
||||
ovApCancel?.addEventListener("click", () => {
|
||||
apstaForm?.classList.add("hidden");
|
||||
});
|
||||
|
||||
ovApApply?.addEventListener("click", () => {
|
||||
const ssid = ovApSsid?.value.trim() ?? "";
|
||||
if (!ssid) {
|
||||
// Minimal inline feedback: focus the field
|
||||
ovApSsid?.focus();
|
||||
return;
|
||||
}
|
||||
const baseCfg = $current_network_configuration.get();
|
||||
send_new_network_configuration_to_alpha({
|
||||
...baseCfg,
|
||||
Connection_Mode: ConnectionMode.CONNECTION_MODE_APSTA,
|
||||
SSID_AP: ssid,
|
||||
Password_AP: ovApPassword?.value ?? "",
|
||||
});
|
||||
apstaForm?.classList.add("hidden");
|
||||
});
|
||||
|
||||
// ---------- Re-setup ----------
|
||||
forgetBtn?.addEventListener("click", () => {
|
||||
// Clear SSID_Station locally so index.astro's pickView swaps to the wizard.
|
||||
// We don't send anything to the device — the wizard's Apply pushes the new SSID.
|
||||
const baseCfg = $current_network_configuration.get();
|
||||
$current_network_configuration.set({ ...baseCfg, SSID_Station: "" });
|
||||
});
|
||||
|
||||
// ---------- Error modal ----------
|
||||
$network_config_error.subscribe((err) => {
|
||||
if (!err || !errorModal) return;
|
||||
if (ovErrorText) {
|
||||
const ssidPart = err.failed_ssid ? ` (SSID: ${err.failed_ssid})` : "";
|
||||
ovErrorText.textContent = `${err.reason}${ssidPart}`;
|
||||
}
|
||||
errorModal.showModal();
|
||||
// Clear after surfacing so it can fire again
|
||||
$network_config_error.set(null);
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,78 +0,0 @@
|
|||
---
|
||||
// Import all page components
|
||||
import DashboardPage from "./DashboardPage.astro";
|
||||
import TestbenchPage from "./TestbenchPage.astro";
|
||||
import MonitoringPage from "./MonitoringPage.astro";
|
||||
import ControlPage from "./ControlPage.astro";
|
||||
import DcdcPage from "./DcdcPage.astro";
|
||||
import ModulationPage from "./ModulationPage.astro";
|
||||
import LogsPage from "./LogsPage.astro";
|
||||
import NetworkPage from "./NetworkPage.astro";
|
||||
|
||||
import SettingsPage from "./SettingsPage.astro";
|
||||
import SecurityPage from "./SecurityPage.astro";
|
||||
---
|
||||
|
||||
<div id="spa-content bg-base-100">
|
||||
<!-- All pages pre-rendered, dashboard visible by default -->
|
||||
<div id="page-dashboard" class="page-content">
|
||||
<DashboardPage />
|
||||
</div>
|
||||
<div id="page-testbench" class="page-content" style="display: none;">
|
||||
<TestbenchPage />
|
||||
</div>
|
||||
<div id="page-monitoring" class="page-content" style="display: none;">
|
||||
<MonitoringPage />
|
||||
</div>
|
||||
<div id="page-control" class="page-content" style="display: none;">
|
||||
<ControlPage />
|
||||
</div>
|
||||
<div id="page-dcdc" class="page-content" style="display: none;">
|
||||
<DcdcPage />
|
||||
</div>
|
||||
<div id="page-modulation" class="page-content" style="display: none;">
|
||||
<ModulationPage />
|
||||
</div>
|
||||
<div id="page-logs" class="page-content" style="display: none;">
|
||||
<LogsPage />
|
||||
</div>
|
||||
<div id="page-network" class="page-content" style="display: none;">
|
||||
<NetworkPage />
|
||||
</div>
|
||||
<div id="page-settings" class="page-content" style="display: none;">
|
||||
<SettingsPage />
|
||||
</div>
|
||||
<div id="page-security" class="page-content" style="display: none;">
|
||||
<SecurityPage />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Navigation handling
|
||||
window.navigateTo = function(path) {
|
||||
// Hide all pages first
|
||||
document.querySelectorAll('.page-content').forEach(page => {
|
||||
(page as HTMLElement).style.display = 'none';
|
||||
});
|
||||
|
||||
// Show the selected page
|
||||
const pageId = `page-${path.slice(1)}`; // Remove leading slash and add 'page-' prefix
|
||||
const selectedPage = document.getElementById(pageId) as HTMLElement;
|
||||
if (selectedPage) {
|
||||
// show navigated page
|
||||
selectedPage.style.display = 'block';
|
||||
} else {
|
||||
// If no matching page found, show dashboard
|
||||
const dashboard = document.getElementById('page-dashboard') as HTMLElement;
|
||||
if (dashboard) {
|
||||
dashboard.style.display = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Show dashboard by default
|
||||
const dashboard = document.getElementById('page-dashboard') as HTMLElement;
|
||||
if (dashboard) {
|
||||
dashboard.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,864 +0,0 @@
|
|||
---
|
||||
const pageTitle = "Security";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Web Access Security</h2>
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
<input id="security-slider" type="range" min="0" max="3" value="0" class="range range-error" step="1" />
|
||||
<div class="flex justify-between px-2.5 mt-2 text-xs">
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="0">
|
||||
<!-- None/0: Alert triangle icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-alert-triangle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" /></svg>
|
||||
</span>
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="1">
|
||||
<!-- Button icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-transition-bottom"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 17a1 1 0 0 1 1 1a4 4 0 0 1 -4 4h-12a4 4 0 0 1 -4 -4a1 1 0 0 1 2 0a2 2 0 0 0 2 2h12a2 2 0 0 0 1.995 -1.85l.005 -.15a1 1 0 0 1 1 -1m-9 1l-.082 -.004l-.119 -.016l-.111 -.03l-.111 -.044l-.098 -.052l-.096 -.067l-.09 -.08l-3 -3a1 1 0 0 1 1.414 -1.414l1.293 1.293v-4.586h-5a4 4 0 1 1 0 -8h12a4 4 0 1 1 0 8h-5v4.583l1.293 -1.29a1 1 0 0 1 1.32 -.083l.094 .083a1 1 0 0 1 0 1.414l-3 3l-.112 .097l-.11 .071l-.062 .031l-.081 .034l-.076 .024l-.149 .03z" /></svg>
|
||||
</span>
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="2">
|
||||
<!-- Pin icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M18 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M4 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M18 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 17h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /></svg>
|
||||
</span>
|
||||
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="3">
|
||||
<!-- Password icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-keyboard"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M2 6m0 2a2 2 0 0 1 2 -2h16a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2 -2z" /><path d="M6 10l0 .01" /><path d="M10 10l0 .01" /><path d="M14 10l0 .01" /><path d="M18 10l0 .01" /><path d="M6 14l0 .01" /><path d="M18 14l0 .01" /><path d="M10 14l4 .01" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<span id="security-mode-badge" class="badge text-xl px-6 py-3 font-bold">None</span>
|
||||
</div>
|
||||
<div class="flex justify-center mt-2">
|
||||
<span id="security-mode-explanation" class="text-center text-sm">No security, open access.</span>
|
||||
</div>
|
||||
<!-- Fixed height container for mode-specific content to ensure consistent Save button position -->
|
||||
<div class="w-full flex flex-col items-center" style="min-height: 200px;">
|
||||
<div id="none-warning" class="flex flex-col items-center mt-4" style="display:none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cloud-lock-open text-error"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 18a3.5 3.5 0 0 0 0 -7h-1c.397 -1.768 -.285 -3.593 -1.788 -4.787c-1.503 -1.193 -3.6 -1.575 -5.5 -1s-3.315 2.019 -3.712 3.787c-2.199 -.088 -4.155 1.326 -4.666 3.373c-.512 2.047 .564 4.154 2.566 5.027" /><path d="M8 15m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 15v-2a2 2 0 0 1 3.736 -1" /></svg>
|
||||
<span class="text-error font-bold text-base mt-2">NOT RECOMMENDED</span>
|
||||
</div>
|
||||
<div id="button-toggle-row" class="flex flex-col items-center mt-16" style="display:none;">
|
||||
<label class="flex items-center gap-3">
|
||||
<span>Allow permanent access to devices</span>
|
||||
<input id="button-permanent-toggle" type="checkbox" class="toggle toggle-error" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="security-input-fields" class="flex flex-col items-center mt-4 w-full" style="display:none;">
|
||||
<input id="security-input" type="password" maxlength="8" placeholder="Enter PIN/Password" class="input input-bordered mb-2 w-full" />
|
||||
<input id="security-confirm" type="password" maxlength="8" placeholder="Confirm PIN/Password" class="input input-bordered mb-2 w-full" />
|
||||
</div>
|
||||
<!-- Spacer to push Save button to bottom of container -->
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex justify-center mt-4">
|
||||
<button id="security-save" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="security-modals-buttons" class="flex flex-col gap-2 items-start mt-4">
|
||||
<button class="btn btn-accent" id="open-test-modal">Test Modal</button>
|
||||
<button class="btn btn-primary" id="open-pin-modal">PIN Login</button>
|
||||
<button class="btn btn-secondary" id="open-password-modal">Password Login</button>
|
||||
<button class="btn btn-info" id="open-updater-modal">Software Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Development Controls for Updates -->
|
||||
<div class="card bg-base-100 shadow mt-4">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Development Controls</h2>
|
||||
<p class="text-sm text-base-content/70 mb-4">Toggle individual firmware update availability for testing purposes.</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-alpha-core"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">Alpha-Core Update</span>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-filesystem"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">Filesystem (Web Interface)</span>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-wifi"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">WiFi Companion</span>
|
||||
</label>
|
||||
|
||||
<label class="cursor-pointer label justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary toggle-sm"
|
||||
checked
|
||||
id="toggle-laser-cpu"
|
||||
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
|
||||
/>
|
||||
<span class="label-text">Laser Core CPU</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>These controls simulate firmware update availability. In production, this information comes from the device via WebSocket.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DaisyUI Modal for Test (Hold Button) -->
|
||||
<dialog id="test-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box">
|
||||
<form id="test-modal-form" class="flex flex-col gap-4" method="dialog" onsubmit="return false;">
|
||||
<h3 class="font-bold text-lg text-center">Device Registration</h3>
|
||||
<label class="floating-label">
|
||||
<div class="input input-bordered flex items-center gap-2">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-edit opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/>
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/>
|
||||
<path d="M16 5l3 3"/>
|
||||
</svg>
|
||||
<input type="text" id="device-name" class="grow bg-transparent border-none outline-none" placeholder=" " required />
|
||||
<span>Device Name</span>
|
||||
</div>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-4 items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-devices">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M13 9a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1v-10z"/>
|
||||
<path d="M18 8v-3a1 1 0 0 0 -1 -1h-13a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h9"/>
|
||||
<path d="M16 9h2"/>
|
||||
</svg>
|
||||
<span id="device-type" class="text-sm text-gray-500">Unknown</span>
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-password-fingerprint">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 8c.788 1 1 2 1 3v1"/>
|
||||
<path d="M9 11c0 -1.578 1.343 -3 3 -3s3 1.422 3 3v2"/>
|
||||
<path d="M12 11v2"/>
|
||||
<path d="M6 12v-1.397c-.006 -1.999 1.136 -3.849 2.993 -4.85a6.385 6.385 0 0 1 6.007 -.005"/>
|
||||
<path d="M12 17v4"/>
|
||||
<path d="M10 20l4 -2"/>
|
||||
<path d="M10 18l4 2"/>
|
||||
<path d="M5 17v4"/>
|
||||
<path d="M3 20l4 -2"/>
|
||||
<path d="M3 18l4 2"/>
|
||||
<path d="M19 17v4"/>
|
||||
<path d="M17 20l4 -2"/>
|
||||
<path d="M17 18l4 2"/>
|
||||
</svg>
|
||||
<span class="text-sm text-gray-500">01234</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold mb-1 flex items-center gap-2">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-hourglass-empty">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 20v-2a6 6 0 1 1 12 0v2a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M6 4v2a6 6 0 1 0 12 0v-2a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1z"/>
|
||||
</svg>
|
||||
Access Expiration
|
||||
</span>
|
||||
<div class="w-full flex flex-col sm:flex-row gap-2">
|
||||
<div class="join join-vertical lg:join-horizontal w-full">
|
||||
<button type="button" id="exp-session" class="btn join-item btn-outline btn-active flex-1">Session</button>
|
||||
<button type="button" id="exp-lifetime" class="btn join-item btn-outline flex-1">Lifetime</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 mt-2">
|
||||
<span class="text-sm text-gray-500">Hold the Unlock <b>Button on the Alpha Laserdriver</b> for 3 seconds to authenticate.</span>
|
||||
<button type="button" id="hold-confirm" class="btn btn-error w-full transition-colors duration-300 text-lg" style="background-color: #dc2626;">
|
||||
<span id="hold-text" class="text-lg">Hold to Authenticate</span>
|
||||
</button>
|
||||
<div id="success-message" class="hidden flex flex-col items-center text-success mt-2">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="success-detail">Device added for this session.</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- PIN Modal -->
|
||||
<dialog id="pin-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box">
|
||||
<form id="pin-modal-form" class="flex flex-col gap-6" method="dialog" onsubmit="return false;">
|
||||
<h3 class="font-bold text-lg text-center">PIN Login</h3>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<span class="flex items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4"/></svg>
|
||||
</span>
|
||||
<input type="password" id="pin-input" class="grow" placeholder="Enter PIN" required maxlength="8" inputmode="numeric" pattern="[0-9]*" />
|
||||
<button type="button" id="toggle-keypad" tabindex="-1" class="ml-2">
|
||||
<svg id="keypad-open-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M18 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M4 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M18 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 17h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/></svg>
|
||||
<svg id="keypad-close-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad-off" style="display:none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-4v-4"/><path d="M17 3h4v4h-4z"/><path d="M10 6v-3h4v4h-3"/><path d="M3 10h4v4h-3"/><path d="M17 13v-3h4v4h-3"/><path d="M14 14h-4v-4"/><path d="M10 17h4v4h-4z"/><path d="M3 3l18 18"/></svg>
|
||||
</button>
|
||||
</label>
|
||||
<div id="pin-keypad-container" class="w-full flex justify-center items-center" style="display:none;min-height:0;">
|
||||
<div class="pin-keypad grid place-items-center w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md">
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">1</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">2</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">3</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">4</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">5</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">6</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">7</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">8</button>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">9</button>
|
||||
<div></div>
|
||||
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">0</button>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="pin-submit" class="btn btn-success w-full text-lg flex items-center justify-center gap-2">Login
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-login-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8v-2a2 2 0 0 1 2 -2h7a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-2"/><path d="M3 12h13l-3 -3"/><path d="M13 15l3 -3"/></svg>
|
||||
</button>
|
||||
<div id="pin-success-message" class="hidden flex flex-col items-center text-success mt-2">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
<span>Access granted!</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Password Modal -->
|
||||
<dialog id="password-modal" class="modal modal-bottom sm:modal-middle">
|
||||
<div class="modal-box">
|
||||
<form id="password-modal-form" class="flex flex-col gap-6" method="dialog" onsubmit="return false;">
|
||||
<h3 class="font-bold text-lg text-center">Password Login</h3>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<span class="flex items-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4"/></svg>
|
||||
</span>
|
||||
<input type="password" id="password-input" class="grow" placeholder="Enter Password" required maxlength="64" />
|
||||
</label>
|
||||
<button type="submit" id="password-submit" class="btn btn-success w-full text-lg flex items-center justify-center gap-2">Login
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-login-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8v-2a2 2 0 0 1 2 -2h7a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-2"/><path d="M3 12h13l-3 -3"/><path d="M13 15l3 -3"/></svg>
|
||||
</button>
|
||||
<div id="password-success-message" class="hidden flex flex-col items-center text-success mt-2">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
<span>Access granted!</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors for all toggles
|
||||
const allToggles = document.querySelectorAll('.toggle');
|
||||
allToggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
/*
|
||||
// Function to update mock update availability based on toggles
|
||||
window.updateAvailability = function() {
|
||||
// Get toggle states
|
||||
const alphaCoreToggle = document.getElementById('toggle-alpha-core') as HTMLInputElement;
|
||||
const filesystemToggle = document.getElementById('toggle-filesystem') as HTMLInputElement;
|
||||
const wifiToggle = document.getElementById('toggle-wifi') as HTMLInputElement;
|
||||
const laserCpuToggle = document.getElementById('toggle-laser-cpu') as HTMLInputElement;
|
||||
|
||||
// Update global mock data (this would be replaced by WebSocket in production)
|
||||
if (typeof window.mockUpdates !== 'undefined') {
|
||||
window.mockUpdates[0].updateAvailable = alphaCoreToggle?.checked || false;
|
||||
window.mockUpdates[1].updateAvailable = filesystemToggle?.checked || false;
|
||||
window.mockUpdates[2].updateAvailable = laserCpuToggle?.checked || false;
|
||||
window.mockUpdates[3].updateAvailable = wifiToggle?.checked || false;
|
||||
}
|
||||
|
||||
// Check if any updates are available
|
||||
const hasUpdates = (alphaCoreToggle?.checked || false) ||
|
||||
(filesystemToggle?.checked || false) ||
|
||||
(wifiToggle?.checked || false) ||
|
||||
(laserCpuToggle?.checked || false);
|
||||
|
||||
// Show/hide notification
|
||||
const notification = document.getElementById('update-notification');
|
||||
if (notification) {
|
||||
if (hasUpdates) {
|
||||
notification.classList.remove('hidden');
|
||||
} else {
|
||||
notification.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide sidebar update button
|
||||
const updateLink = document.getElementById('update-link');
|
||||
if (updateLink) {
|
||||
if (hasUpdates) {
|
||||
updateLink.style.display = '';
|
||||
} else {
|
||||
updateLink.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};*/
|
||||
/*
|
||||
// Add event listeners to all toggles
|
||||
const toggles = ['toggle-alpha-core', 'toggle-filesystem', 'toggle-wifi', 'toggle-laser-cpu'];
|
||||
toggles.forEach(toggleId => {
|
||||
const toggle = document.getElementById(toggleId);
|
||||
if (toggle) {
|
||||
toggle.addEventListener('change', updateAvailability);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize update availability on page load
|
||||
setTimeout(() => {
|
||||
if (typeof window.updateAvailability === 'function') {
|
||||
window.updateAvailability();
|
||||
}
|
||||
}, 100);*/
|
||||
|
||||
// Test Modal logic
|
||||
const openTestModalBtn = document.getElementById('open-test-modal');
|
||||
const testModal = document.getElementById('test-modal');
|
||||
const holdBtn = document.getElementById('hold-confirm') as HTMLButtonElement | null;
|
||||
const holdText = document.getElementById('hold-text');
|
||||
const successMsg = document.getElementById('success-message');
|
||||
let holdTimeout: number | undefined, holdStart: number, holdInterval: number | undefined;
|
||||
const holdDuration = 3000; // 3 seconds
|
||||
|
||||
if (openTestModalBtn && testModal) {
|
||||
openTestModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if ((testModal as HTMLDialogElement).open) {
|
||||
(testModal as HTMLDialogElement).close();
|
||||
}
|
||||
(testModal as HTMLDialogElement).showModal();
|
||||
|
||||
// Reset device name input without focusing
|
||||
const deviceNameInput = document.getElementById('device-name') as HTMLInputElement | null;
|
||||
if (deviceNameInput) {
|
||||
deviceNameInput.value = '';
|
||||
deviceNameInput.blur(); // Ensure no focus
|
||||
}
|
||||
|
||||
if (holdBtn) {
|
||||
holdBtn.disabled = false;
|
||||
holdBtn.classList.remove('btn-success');
|
||||
holdBtn.classList.add('btn-error');
|
||||
holdBtn.style.backgroundColor = '#dc2626';
|
||||
holdBtn.classList.remove('animate-stripes');
|
||||
holdBtn.classList.add('text-black');
|
||||
holdBtn.classList.remove('text-white');
|
||||
holdBtn.classList.remove('breathing-orange');
|
||||
}
|
||||
if (holdText) {
|
||||
holdText.textContent = 'Hold to Authenticate';
|
||||
holdText.classList.add('text-black');
|
||||
holdText.classList.remove('text-white');
|
||||
}
|
||||
if (successMsg) successMsg.classList.add('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error opening test modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (holdBtn) {
|
||||
function getHoldColor(progress: number): { color: string, textBlack: boolean } {
|
||||
let hue: number, sat: number, light: number;
|
||||
if (progress < 0.85) {
|
||||
hue = 20 + (25 * (progress / 0.85));
|
||||
sat = 100;
|
||||
light = 45 + (10 * (progress / 0.85));
|
||||
} else {
|
||||
hue = 45 + (75 * ((progress - 0.85) / 0.15));
|
||||
sat = 100;
|
||||
light = 55;
|
||||
}
|
||||
const textBlack = hue >= 40;
|
||||
return { color: `hsl(${hue}, ${sat}%, ${light}%)`, textBlack };
|
||||
}
|
||||
|
||||
const setBtnColor = (color: string, breathing = false, textBlack = false) => {
|
||||
holdBtn.style.backgroundColor = color;
|
||||
if (breathing) {
|
||||
holdBtn.classList.add('breathing-orange');
|
||||
} else {
|
||||
holdBtn.classList.remove('breathing-orange');
|
||||
}
|
||||
holdBtn.classList.add('text-black');
|
||||
holdBtn.classList.remove('text-white');
|
||||
if (holdText) {
|
||||
holdText.classList.add('text-black');
|
||||
holdText.classList.remove('text-white');
|
||||
}
|
||||
};
|
||||
|
||||
const updateProgress = () => {
|
||||
try {
|
||||
const elapsed = Date.now() - holdStart;
|
||||
const progress = Math.min(elapsed / holdDuration, 1);
|
||||
const { color, textBlack } = getHoldColor(progress);
|
||||
if (progress < 1) {
|
||||
setBtnColor(color, true, textBlack);
|
||||
} else {
|
||||
setBtnColor(getHoldColor(1).color, false, true);
|
||||
holdBtn.disabled = true;
|
||||
holdBtn.classList.remove('btn-error');
|
||||
holdBtn.classList.add('btn-success');
|
||||
if (holdText) holdText.textContent = 'Confirmed!';
|
||||
if (successMsg) {
|
||||
const detail = successMsg.querySelector('.success-detail') as HTMLElement | null;
|
||||
if (detail) {
|
||||
if (typeof expirationValue !== 'undefined' && expirationValue === 'lifetime') {
|
||||
detail.textContent = 'Device added permanently.';
|
||||
} else {
|
||||
detail.textContent = 'Device added for this session.';
|
||||
}
|
||||
}
|
||||
successMsg.classList.remove('hidden');
|
||||
}
|
||||
setTimeout(() => {
|
||||
(testModal as HTMLDialogElement).close();
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating progress:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Use both mouse and touch events for better mobile support
|
||||
const startHold = () => {
|
||||
try {
|
||||
if (holdBtn.disabled) return;
|
||||
holdStart = Date.now();
|
||||
setBtnColor('hsl(20, 100%, 45%)', true, false);
|
||||
holdInterval = window.setInterval(updateProgress, 50);
|
||||
holdTimeout = window.setTimeout(() => {
|
||||
updateProgress();
|
||||
}, holdDuration);
|
||||
if (holdText) holdText.textContent = 'Keep Holding...';
|
||||
} catch (error) {
|
||||
console.error('Error starting hold:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const endHold = () => {
|
||||
try {
|
||||
if (holdBtn.disabled) return;
|
||||
clearInterval(holdInterval);
|
||||
clearTimeout(holdTimeout);
|
||||
setBtnColor('#dc2626', false, false);
|
||||
if (holdText) holdText.textContent = 'Hold to Authenticate';
|
||||
} catch (error) {
|
||||
console.error('Error ending hold:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add both mouse and touch event listeners
|
||||
holdBtn.addEventListener('mousedown', startHold);
|
||||
holdBtn.addEventListener('touchstart', startHold, { passive: true });
|
||||
holdBtn.addEventListener('mouseup', endHold);
|
||||
holdBtn.addEventListener('touchend', endHold, { passive: true });
|
||||
holdBtn.addEventListener('mouseleave', endHold);
|
||||
holdBtn.addEventListener('touchcancel', endHold, { passive: true });
|
||||
}
|
||||
|
||||
// Expiration button logic
|
||||
const expSessionBtn = document.getElementById('exp-session');
|
||||
const expLifetimeBtn = document.getElementById('exp-lifetime');
|
||||
let expirationValue = 'session';
|
||||
if (expSessionBtn && expLifetimeBtn) {
|
||||
expSessionBtn.addEventListener('click', function() {
|
||||
try {
|
||||
expSessionBtn.classList.add('btn-active');
|
||||
expLifetimeBtn.classList.remove('btn-active');
|
||||
expirationValue = 'session';
|
||||
} catch (error) {
|
||||
console.error('Error setting session expiration:', error);
|
||||
}
|
||||
});
|
||||
expLifetimeBtn.addEventListener('click', function() {
|
||||
try {
|
||||
expLifetimeBtn.classList.add('btn-active');
|
||||
expSessionBtn.classList.remove('btn-active');
|
||||
expirationValue = 'lifetime';
|
||||
} catch (error) {
|
||||
console.error('Error setting lifetime expiration:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PIN Modal logic
|
||||
const openPinModalBtn = document.getElementById('open-pin-modal');
|
||||
const pinModal = document.getElementById('pin-modal');
|
||||
const pinForm = document.getElementById('pin-modal-form') as HTMLFormElement | null;
|
||||
const pinInput = document.getElementById('pin-input') as HTMLInputElement | null;
|
||||
const pinSuccessMsg = document.getElementById('pin-success-message');
|
||||
const isMobile = /android|iphone|ipad|ipod|opera mini|iemobile|mobile/i.test(navigator.userAgent);
|
||||
|
||||
if (openPinModalBtn && pinModal) {
|
||||
openPinModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if ((pinModal as HTMLDialogElement).open) {
|
||||
(pinModal as HTMLDialogElement).close();
|
||||
}
|
||||
(pinModal as HTMLDialogElement).showModal();
|
||||
if (pinInput) pinInput.value = '';
|
||||
if (pinSuccessMsg) pinSuccessMsg.classList.add('hidden');
|
||||
const pinKeypadContainer = document.getElementById('pin-keypad-container');
|
||||
const toggleKeypadBtn = document.getElementById('toggle-keypad');
|
||||
if (isMobile) {
|
||||
if (pinKeypadContainer) pinKeypadContainer.style.display = 'none';
|
||||
if (toggleKeypadBtn) toggleKeypadBtn.style.display = 'none';
|
||||
} else {
|
||||
if (toggleKeypadBtn) toggleKeypadBtn.style.display = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening PIN modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keypad logic
|
||||
const pinKeypadContainer = document.getElementById('pin-keypad-container');
|
||||
const toggleKeypadBtn = document.getElementById('toggle-keypad');
|
||||
const keypadOpenIcon = document.getElementById('keypad-open-icon');
|
||||
const keypadCloseIcon = document.getElementById('keypad-close-icon');
|
||||
let keypadOpen = false;
|
||||
if (toggleKeypadBtn && pinKeypadContainer && keypadOpenIcon && keypadCloseIcon) {
|
||||
toggleKeypadBtn.addEventListener('click', () => {
|
||||
try {
|
||||
keypadOpen = !keypadOpen;
|
||||
pinKeypadContainer.style.display = keypadOpen ? '' : 'none';
|
||||
keypadOpenIcon.style.display = keypadOpen ? 'none' : '';
|
||||
keypadCloseIcon.style.display = keypadOpen ? '' : 'none';
|
||||
} catch (error) {
|
||||
console.error('Error toggling keypad:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pinKeypadContainer && pinInput) {
|
||||
pinKeypadContainer.addEventListener('click', (e) => {
|
||||
try {
|
||||
const t = e.target as HTMLButtonElement | null;
|
||||
if (t && t.classList.contains('pin-key')) {
|
||||
if (pinInput.value.length < 8) pinInput.value += t.textContent ?? '';
|
||||
t.classList.add('btn-active', 'pulse-key');
|
||||
setTimeout(() => {
|
||||
t.classList.remove('btn-active', 'pulse-key');
|
||||
}, 120);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling keypad click:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pinForm && pinModal && pinSuccessMsg) {
|
||||
pinForm.addEventListener('submit', (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
pinSuccessMsg.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
(pinModal as HTMLDialogElement).close();
|
||||
}, 1200);
|
||||
} catch (error) {
|
||||
console.error('Error submitting PIN form:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Password Modal logic
|
||||
const openPasswordModalBtn = document.getElementById('open-password-modal');
|
||||
const passwordModal = document.getElementById('password-modal');
|
||||
const passwordForm = document.getElementById('password-modal-form') as HTMLFormElement | null;
|
||||
const passwordInput = document.getElementById('password-input') as HTMLInputElement | null;
|
||||
const passwordSuccessMsg = document.getElementById('password-success-message');
|
||||
if (openPasswordModalBtn && passwordModal) {
|
||||
openPasswordModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if ((passwordModal as HTMLDialogElement).open) {
|
||||
(passwordModal as HTMLDialogElement).close();
|
||||
}
|
||||
(passwordModal as HTMLDialogElement).showModal();
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
if (passwordSuccessMsg) passwordSuccessMsg.classList.add('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error opening password modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (passwordForm && passwordModal && passwordSuccessMsg) {
|
||||
passwordForm.addEventListener('submit', (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
passwordSuccessMsg.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
(passwordModal as HTMLDialogElement).close();
|
||||
}, 1200);
|
||||
} catch (error) {
|
||||
console.error('Error submitting password form:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Updater Modal logic
|
||||
const openUpdaterModalBtn = document.getElementById('open-updater-modal');
|
||||
if (openUpdaterModalBtn) {
|
||||
openUpdaterModalBtn.addEventListener('click', () => {
|
||||
try {
|
||||
if (typeof window.openUpdaterModal === 'function') {
|
||||
window.openUpdaterModal();
|
||||
} else {
|
||||
console.error('openUpdaterModal function not available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening updater modal:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Web Access Security slider logic
|
||||
const slider = document.getElementById('security-slider');
|
||||
const badge = document.getElementById('security-mode-badge');
|
||||
const explanation = document.getElementById('security-mode-explanation');
|
||||
const inputFields = document.getElementById('security-input-fields');
|
||||
const input = document.getElementById('security-input');
|
||||
const confirm = document.getElementById('security-confirm');
|
||||
const saveBtn = document.getElementById('security-save');
|
||||
const MODES = [
|
||||
{ label: 'None', color: 'badge-error', slider: 'range-error', explain: 'No security, open access.', showInput: false },
|
||||
{ label: 'Button', color: 'badge-info', slider: 'range-info', explain: 'Physical button press required to authenticate.', showInput: false },
|
||||
{ label: 'PIN', color: 'badge-warning', slider: 'range-warning', explain: '4-6 digit numeric code required for access.', showInput: true, inputType: 'pin' },
|
||||
{ label: 'Password', color: 'badge-success', slider: 'range-success', explain: 'Alphanumeric password required for access.', showInput: true, inputType: 'password' },
|
||||
];
|
||||
let lastMode = 0;
|
||||
|
||||
function updateSecurityUI(val: number) {
|
||||
try {
|
||||
const mode = MODES[val];
|
||||
if (badge) {
|
||||
badge.textContent = mode.label;
|
||||
badge.className = `badge text-xl px-6 py-3 font-bold ${mode.color}`;
|
||||
}
|
||||
if (explanation) {
|
||||
if (val === 1) {
|
||||
explanation.textContent = 'Physical button press required to authenticate.';
|
||||
} else {
|
||||
explanation.textContent = mode.explain;
|
||||
}
|
||||
}
|
||||
if (slider) slider.className = `range ${mode.slider}`;
|
||||
if (inputFields && input && confirm) {
|
||||
const inputEl = input as HTMLInputElement;
|
||||
const confirmEl = confirm as HTMLInputElement;
|
||||
if (mode.showInput) {
|
||||
inputFields.style.display = '';
|
||||
inputEl.type = mode.inputType === 'pin' ? 'password' : 'password';
|
||||
inputEl.maxLength = mode.inputType === 'pin' ? 8 : 64;
|
||||
inputEl.placeholder = mode.inputType === 'pin' ? 'Enter PIN' : 'Enter Password';
|
||||
confirmEl.type = mode.inputType === 'pin' ? 'password' : 'password';
|
||||
confirmEl.maxLength = mode.inputType === 'pin' ? 8 : 64;
|
||||
confirmEl.placeholder = mode.inputType === 'pin' ? 'Confirm PIN' : 'Confirm Password';
|
||||
} else {
|
||||
inputFields.style.display = 'none';
|
||||
inputEl.value = '';
|
||||
confirmEl.value = '';
|
||||
}
|
||||
}
|
||||
// Show/hide the none-warning icon and text
|
||||
const noneWarning = document.getElementById('none-warning');
|
||||
if (noneWarning) {
|
||||
noneWarning.style.display = (val === 0) ? '' : 'none';
|
||||
}
|
||||
// Show/hide the button-toggle-row for BUTTON mode
|
||||
const buttonToggleRow = document.getElementById('button-toggle-row');
|
||||
if (buttonToggleRow) {
|
||||
buttonToggleRow.style.display = (val === 1) ? '' : 'none';
|
||||
}
|
||||
// Remove Save button margin manipulation - no longer needed
|
||||
lastMode = val;
|
||||
} catch (error) {
|
||||
console.error('Error updating security UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (slider) {
|
||||
updateSecurityUI(Number((slider as HTMLInputElement).value));
|
||||
slider.addEventListener('input', function() {
|
||||
try {
|
||||
updateSecurityUI(Number((slider as HTMLInputElement).value));
|
||||
} catch (error) {
|
||||
console.error('Error handling slider input:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', function(e) {
|
||||
try {
|
||||
e.preventDefault();
|
||||
// TODO: Save logic
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add click listeners to icons to control slider
|
||||
const iconContainers = document.querySelectorAll('[data-slider-value]');
|
||||
iconContainers.forEach(icon => {
|
||||
icon.addEventListener('click', function(this: HTMLElement) {
|
||||
try {
|
||||
const value = this.getAttribute('data-slider-value');
|
||||
if (slider && value !== null) {
|
||||
(slider as HTMLInputElement).value = value;
|
||||
updateSecurityUI(Number(value));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling icon click:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes breathing {
|
||||
0% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
50% { filter: brightness(1.35) drop-shadow(0 0 1.5rem #fbbf24); }
|
||||
100% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
}
|
||||
.breathing-orange {
|
||||
animation: breathing 1.2s infinite;
|
||||
}
|
||||
.pin-keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
}
|
||||
.pin-keypad .pin-key {
|
||||
min-width: 3rem;
|
||||
min-height: 3rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.pin-keypad { max-width: 100%; }
|
||||
.pin-keypad .pin-key { font-size: 1rem; min-width: 2.25rem; min-height: 2.25rem; }
|
||||
}
|
||||
#pin-keypad-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#pin-modal-form {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.pulse-key {
|
||||
animation: pulse-btn 0.18s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
@keyframes pulse-btn {
|
||||
0% { filter: brightness(1.1); }
|
||||
50% { filter: brightness(1.5); }
|
||||
100% { filter: brightness(1.1); }
|
||||
}
|
||||
@keyframes security-fade {
|
||||
0% { opacity: 0; transform: translateY(10px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-security-fade {
|
||||
animation: security-fade 0.3s;
|
||||
}
|
||||
/* Fix hold button border to match background color */
|
||||
#hold-confirm {
|
||||
border: 2px solid transparent !important;
|
||||
}
|
||||
|
||||
/* Floating label styles */
|
||||
.floating-label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.floating-label > div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.floating-label span {
|
||||
position: absolute;
|
||||
left: 3rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
pointer-events: none;
|
||||
padding: 0 0.375rem;
|
||||
z-index: 1;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Use DaisyUI's standard label-text color class behavior */
|
||||
.floating-label span {
|
||||
@apply text-base-content/60 bg-base-100;
|
||||
}
|
||||
|
||||
.floating-label input:focus ~ span,
|
||||
.floating-label input:not(:placeholder-shown) ~ span {
|
||||
top: 0;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.75rem;
|
||||
left: 2.75rem;
|
||||
@apply text-primary font-medium;
|
||||
}
|
||||
|
||||
/* Ensure the input doesn't have autofocus */
|
||||
.floating-label input {
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.floating-label input:focus {
|
||||
border-color: hsl(var(--p));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,462 +0,0 @@
|
|||
---
|
||||
const pageTitle = "System Settings";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
</div>
|
||||
|
||||
<!-- About this Device -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-xl font-bold">About this Device</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Device Model</div>
|
||||
<div class="stat-value text-sm">Alpha GaN Driver</div>
|
||||
<div class="stat-desc text-xs">50V 25A REV 2.3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Serial Number</div>
|
||||
<div class="stat-value text-sm" id="serial-number">Alpha-0001</div>
|
||||
<div class="stat-desc text-xs">Lasertack GmbH</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Operating Hours</div>
|
||||
<div class="stat-value text-sm" id="operating-hours">1,247</div>
|
||||
<div class="stat-desc text-xs">Total Runtime</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Driver Core MCU</div>
|
||||
<div class="stat-value text-sm" id="driver-firmware-version">FW1.00</div>
|
||||
<div class="stat-desc text-xs">Build 05.07.2025</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Host MCU</div>
|
||||
<div class="stat-value text-sm" id="companion-firmware-version">FW1.00</div>
|
||||
<div class="stat-desc text-xs">Build 07.07.2025</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats shadow bg-base-200 rounded-lg border-0">
|
||||
<div class="stat bg-base-200 p-3">
|
||||
<div class="stat-title text-xs">Wireless MCU</div>
|
||||
<div class="stat-value text-sm" id="wireless-firmware-version">FW1.00</div>
|
||||
<div class="stat-desc text-xs">Build 01.06.2025</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Preferences -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">System Preferences</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Time & Locale</h3>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Timezone</span>
|
||||
</label>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20.884 10.554a9 9 0 1 0 -10.337 10.328" />
|
||||
<path d="M3.6 9h16.8" />
|
||||
<path d="M3.6 15h6.9" />
|
||||
<path d="M11.5 3a17 17 0 0 0 -1.502 14.954" />
|
||||
<path d="M12.5 3a17 17 0 0 1 2.52 7.603" />
|
||||
<path d="M18 18m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
|
||||
<path d="M18 16.5v1.5l.5 .5" />
|
||||
</svg>
|
||||
<select class="grow bg-transparent border-none outline-none" id="timezone">
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="Europe/Berlin" selected>Europe/Berlin</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Time Format</span>
|
||||
</label>
|
||||
<div class="join w-full">
|
||||
<button class="btn btn-outline join-item flex-1" id="time-12h" onclick="if('vibrate' in navigator) navigator.vibrate(15)">12 Hour</button>
|
||||
<button class="btn btn-active join-item flex-1" id="time-24h" onclick="if('vibrate' in navigator) navigator.vibrate(15)">24 Hour</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Current Time</span>
|
||||
</label>
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12,6 12,12 16,14"/>
|
||||
</svg>
|
||||
<input type="text" class="grow" readonly id="current-time" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Safety & Logging</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable System Logging</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-logging" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Auto-save Settings</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-autosave" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Watchdog Timer</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-watchdog" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Temperature Monitoring</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-temp-monitoring" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Configuration -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Device Configuration</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Hardware Settings</h3>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">CPU Frequency</span>
|
||||
</label>
|
||||
<select class="select select-bordered" id="cpu-frequency">
|
||||
<option value="240">240 MHz (Default)</option>
|
||||
<option value="160">160 MHz (Power Save)</option>
|
||||
<option value="80">80 MHz (Low Power)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Flash Frequency</span>
|
||||
</label>
|
||||
<select class="select select-bordered" id="flash-frequency">
|
||||
<option value="80" selected>80 MHz</option>
|
||||
<option value="40">40 MHz</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Brownout Detector</span>
|
||||
<input type="checkbox" class="toggle toggle-success" checked id="toggle-brownout" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Performance Settings</h3>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Task Watchdog Timeout</span>
|
||||
</label>
|
||||
<select class="select select-bordered" id="task-watchdog">
|
||||
<option value="5" selected>5 seconds</option>
|
||||
<option value="10">10 seconds</option>
|
||||
<option value="30">30 seconds</option>
|
||||
<option value="0">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Stack Size (KB)</span>
|
||||
</label>
|
||||
<input type="range" min="4" max="16" value="8" step="1" class="range range-primary" id="stack-size" />
|
||||
<div class="w-full flex justify-between text-xs px-2 mt-1">
|
||||
<span>4KB</span>
|
||||
<span>8KB</span>
|
||||
<span>12KB</span>
|
||||
<span>16KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="cursor-pointer label justify-start gap-4">
|
||||
<span class="label-text">Enable Debug Output</span>
|
||||
<input type="checkbox" class="toggle toggle-error" id="toggle-debug" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">System Actions</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
||||
<button class="btn btn-primary" id="save-settings" onclick="if('vibrate' in navigator) navigator.vibrate([100, 200, 100])">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
Save Settings
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline" id="export-config" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3.75H6.912a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.013 1.244h3.218a2.25 2.25 0 002.013-1.244l.256-.512a2.25 2.25 0 012.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15" />
|
||||
</svg>
|
||||
Export Config
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning" id="restart-system" onclick="if('vibrate' in navigator) navigator.vibrate([500, 200, 500, 200, 500])">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
Restart
|
||||
</button>
|
||||
|
||||
<button class="btn btn-error" id="factory-reset" onclick="if('vibrate' in navigator) navigator.vibrate([500, 200, 500, 200, 500])">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
Factory Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle color management function
|
||||
function updateToggleColor(toggle: HTMLInputElement) {
|
||||
if (toggle.checked) {
|
||||
toggle.classList.remove('toggle-error');
|
||||
toggle.classList.add('toggle-success');
|
||||
} else {
|
||||
toggle.classList.remove('toggle-success');
|
||||
toggle.classList.add('toggle-error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize toggle colors
|
||||
const toggles = document.querySelectorAll('.toggle');
|
||||
toggles.forEach(toggle => {
|
||||
updateToggleColor(toggle as HTMLInputElement);
|
||||
});
|
||||
|
||||
// Update current time
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (timeElement) {
|
||||
(timeElement as HTMLInputElement).value = now.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
// Update time every second
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
|
||||
// Time format buttons
|
||||
const time12hBtn = document.getElementById('time-12h');
|
||||
const time24hBtn = document.getElementById('time-24h');
|
||||
|
||||
if (time12hBtn && time24hBtn) {
|
||||
time12hBtn.addEventListener('click', function() {
|
||||
time12hBtn.classList.add('btn-active');
|
||||
time24hBtn.classList.remove('btn-active');
|
||||
});
|
||||
|
||||
time24hBtn.addEventListener('click', function() {
|
||||
time24hBtn.classList.add('btn-active');
|
||||
time12hBtn.classList.remove('btn-active');
|
||||
});
|
||||
}
|
||||
|
||||
// Stack size slider
|
||||
const stackSizeSlider = document.getElementById('stack-size');
|
||||
if (stackSizeSlider) {
|
||||
stackSizeSlider.addEventListener('input', function() {
|
||||
console.log('Stack size:', (this as HTMLInputElement).value + 'KB');
|
||||
});
|
||||
}
|
||||
|
||||
// Save settings
|
||||
const saveSettingsBtn = document.getElementById('save-settings');
|
||||
if (saveSettingsBtn) {
|
||||
saveSettingsBtn.addEventListener('click', function() {
|
||||
console.log('Saving settings...');
|
||||
// Collect all settings and send via WebSocket
|
||||
|
||||
// Show success feedback
|
||||
const originalText = saveSettingsBtn.textContent;
|
||||
saveSettingsBtn.textContent = 'Saved!';
|
||||
saveSettingsBtn.classList.add('btn-success');
|
||||
|
||||
setTimeout(() => {
|
||||
saveSettingsBtn.textContent = originalText;
|
||||
saveSettingsBtn.classList.remove('btn-success');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// Export config
|
||||
const exportConfigBtn = document.getElementById('export-config');
|
||||
if (exportConfigBtn) {
|
||||
exportConfigBtn.addEventListener('click', function() {
|
||||
// Generate and download config file
|
||||
const config = {
|
||||
system: {
|
||||
timezone: (document.getElementById('timezone') as HTMLSelectElement)?.value,
|
||||
logging: (document.getElementById('toggle-logging') as HTMLInputElement)?.checked,
|
||||
cpu_frequency: (document.getElementById('cpu-frequency') as HTMLSelectElement)?.value
|
||||
}
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `alpha-driver-config-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
// Restart system
|
||||
const restartBtn = document.getElementById('restart-system');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', function() {
|
||||
if (confirm('Are you sure you want to restart the system? This will temporarily interrupt operations.')) {
|
||||
console.log('Restarting system...');
|
||||
// Send restart command via WebSocket
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Factory reset
|
||||
const factoryResetBtn = document.getElementById('factory-reset');
|
||||
if (factoryResetBtn) {
|
||||
factoryResetBtn.addEventListener('click', function() {
|
||||
if (confirm('WARNING: This will reset all settings to factory defaults. This action cannot be undone.\n\nAre you sure you want to continue?')) {
|
||||
console.log('Performing factory reset...');
|
||||
// Send factory reset command via WebSocket
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update settings from WebSocket
|
||||
(window as any).updateSystemSettings = function(data: any) {
|
||||
if (data.firmware_version) {
|
||||
const firmwareEl = document.getElementById('firmware-version');
|
||||
if (firmwareEl) firmwareEl.textContent = data.firmware_version;
|
||||
}
|
||||
|
||||
if (data.mac_address) {
|
||||
const macEl = document.getElementById('mac-address');
|
||||
if (macEl) macEl.textContent = data.mac_address;
|
||||
}
|
||||
};
|
||||
|
||||
// Device Type detection
|
||||
const deviceTypeEl = document.getElementById('device-type');
|
||||
if (deviceTypeEl) {
|
||||
let type = 'Unknown';
|
||||
const ua = navigator.userAgent;
|
||||
if (/android/i.test(ua)) type = 'Android Phone';
|
||||
else if (/iPhone|iPad|iPod/i.test(ua)) type = 'iPhone';
|
||||
else if (/Windows/i.test(ua)) type = 'Windows Computer';
|
||||
else if (/Macintosh/i.test(ua)) type = 'Mac Computer';
|
||||
else if (/Linux/i.test(ua)) type = 'Linux Computer';
|
||||
deviceTypeEl.textContent = type;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes breathing {
|
||||
0% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
50% { filter: brightness(1.35) drop-shadow(0 0 1.5rem #fbbf24); }
|
||||
100% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
|
||||
}
|
||||
.breathing-orange {
|
||||
animation: breathing 1.2s infinite;
|
||||
}
|
||||
.pin-keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
}
|
||||
.pin-keypad .pin-key {
|
||||
min-width: 3rem;
|
||||
min-height: 3rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.pin-keypad { max-width: 100%; }
|
||||
.pin-keypad .pin-key { font-size: 1rem; min-width: 2.25rem; min-height: 2.25rem; }
|
||||
}
|
||||
#pin-keypad-container {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#pin-modal-form {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.pulse-key {
|
||||
animation: pulse-btn 0.18s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
@keyframes pulse-btn {
|
||||
0% { filter: brightness(1.1); }
|
||||
50% { filter: brightness(1.5); }
|
||||
100% { filter: brightness(1.1); }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
---
|
||||
import ConnectionStatusBadge from "./ConnectionStatusBadge.tsx";
|
||||
import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
|
||||
import { $availableUpdates } from "./AppProviderStore.tsx";
|
||||
interface NavigationItem {
|
||||
path: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
// Laser section items
|
||||
const laserNavigationItems: NavigationItem[] = [
|
||||
{
|
||||
path: "/dashboard",
|
||||
title: "Dashboard",
|
||||
icon: '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/testbench",
|
||||
title: "Testbench",
|
||||
icon: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="14" cy="8" r="1"/><circle cx="18" cy="8" r="1"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/control",
|
||||
title: "Control",
|
||||
icon: '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/modulation",
|
||||
title: "Laser",
|
||||
icon: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/dcdc",
|
||||
title: "Power",
|
||||
icon: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
|
||||
{
|
||||
path: "/logs",
|
||||
title: "Logs",
|
||||
icon: '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
];
|
||||
|
||||
// System section items
|
||||
const systemNavigationItems: NavigationItem[] = [
|
||||
{
|
||||
path: "/network",
|
||||
title: "Network",
|
||||
icon: '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
title: "Settings",
|
||||
icon: '<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/>',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
{
|
||||
path: "/security",
|
||||
title: "Security",
|
||||
icon: '<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.998 2l.118 .007l.059 .008l.061 .013l.111 .034a.993 .993 0 0 1 .217 .112l.104 .082l.255 .218a11 11 0 0 0 7.189 2.537l.342 -.01a1 1 0 0 1 1.005 .717a13 13 0 0 1 -9.208 16.25a1 1 0 0 1 -.502 0a13 13 0 0 1 -9.209 -16.25a1 1 0 0 1 1.005 -.717a11 11 0 0 0 7.531 -2.527l.263 -.225l.096 -.075a.993 .993 0 0 1 .217 -.112l.112 -.034a.97 .97 0 0 1 .119 -.021l.115 -.007zm.002 7a2 2 0 0 0 -1.995 1.85l-.005 .15l.005 .15a2 2 0 0 0 .995 1.581v1.769l.007 .117a1 1 0 0 0 1.993 -.117l.001 -1.768a2 2 0 0 0 -1.001 -3.732z" />',
|
||||
viewBox: "0 0 24 24",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<aside class="menu bg-base-200 w-70 min-h-full p-4 text-base-content">
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
<div class="text-xl font-bold">Lasertack</div>
|
||||
</div>
|
||||
|
||||
<ul class="menu menu-md text-lg">
|
||||
<li>
|
||||
<h2 class="menu-title text-base">Laser</h2>
|
||||
<ul>
|
||||
{
|
||||
laserNavigationItems.map((item) => (
|
||||
<li>
|
||||
<a href={item.path} class="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox={item.viewBox}
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5"
|
||||
set:html={item.icon}
|
||||
/>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h2 class="menu-title text-base">System</h2>
|
||||
<ul>
|
||||
{
|
||||
systemNavigationItems.map((item) => (
|
||||
<li>
|
||||
<a href={item.path} class="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox={item.viewBox}
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="w-5 h-5"
|
||||
set:html={item.icon}
|
||||
/>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
<li>
|
||||
<a href="/modal" class="cursor-pointer flex items-center gap-2" id="status-link">
|
||||
<span class="text-sm">Driver:</span>
|
||||
<ConnectionStatusBadge client:load />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/modal" class="cursor-pointer flex items-center gap-2" id="testbench-status-link">
|
||||
<span class="text-sm">Testbench:</span>
|
||||
<TestbenchConnectionStatusBadge client:load />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/update" class="cursor-pointer" id="update-link">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-refresh-alert"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
<path d="M12 9l0 3"></path>
|
||||
<path d="M12 15l.01 0"></path>
|
||||
</svg>
|
||||
<span id="update-status" class="text-neutral-500"
|
||||
>No updates available</span
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<script>
|
||||
// Type declaration for global functions
|
||||
declare global {
|
||||
interface Window {
|
||||
handleNavigation: (path: string) => void;
|
||||
openStatusModal?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Import the store
|
||||
import { $availableUpdates } from "./AppProviderStore.tsx";
|
||||
|
||||
// Function to update the UI based on available updates
|
||||
function updateUpdateStatus() {
|
||||
const updateStatus = document.getElementById("update-status");
|
||||
const updateLink = document.getElementById("update-link");
|
||||
|
||||
if (updateStatus && updateLink) {
|
||||
const updates = $availableUpdates.get();
|
||||
const hasUpdates = updates && updates.length > 0;
|
||||
|
||||
if (hasUpdates) {
|
||||
updateStatus.textContent = "Update Available";
|
||||
updateStatus.className = "text-success";
|
||||
updateLink.classList.add("btn-success", "text-success");
|
||||
} else {
|
||||
updateStatus.textContent = "No updates available";
|
||||
updateStatus.className = "text-neutral-500";
|
||||
updateLink.classList.remove("btn-success", "text-success");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warten bis das DOM geladen ist
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initial update
|
||||
updateUpdateStatus();
|
||||
|
||||
// Subscribe to store changes
|
||||
$availableUpdates.subscribe(() => {
|
||||
updateUpdateStatus();
|
||||
});
|
||||
// Alle Links in der Sidebar abfangen
|
||||
document.querySelectorAll('.menu a[href^="/"]').forEach((link) => {
|
||||
link.addEventListener(
|
||||
"click",
|
||||
function (this: HTMLAnchorElement, e: Event) {
|
||||
e.preventDefault();
|
||||
const path = this.getAttribute("href");
|
||||
if (path === "/modal") {
|
||||
if (typeof window.openStatusModal === "function") {
|
||||
window.openStatusModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path === "/update") {
|
||||
if (typeof window.openUpdaterModal === "function") {
|
||||
window.openUpdaterModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
window.handleNavigation &&
|
||||
typeof window.handleNavigation === "function" &&
|
||||
path
|
||||
) {
|
||||
window.handleNavigation(path);
|
||||
// Optional: URL in der Adresszeile aktualisieren
|
||||
//window.history.pushState({}, '', path);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
944
src/components/StationConfigModal.astro
Normal file
944
src/components/StationConfigModal.astro
Normal file
|
|
@ -0,0 +1,944 @@
|
|||
---
|
||||
import IconEye from "../icons/IconEye.astro";
|
||||
import IconEyeClosed from "../icons/IconEyeClosed.astro";
|
||||
---
|
||||
|
||||
<dialog id="station-config-modal" class="modal modal-middle">
|
||||
<div class="modal-box w-11/12 sm:w-6/12 max-w-5xl flex flex-col">
|
||||
<h1 class="text-lg font-bold">WiFi Station Configuration</h1>
|
||||
<div class="divider divider-primary"></div>
|
||||
|
||||
<div id="config-form-view" class="flex flex-col gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold mb-2 ml-1">Network SSID</span>
|
||||
</label>
|
||||
<input type="text" class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary" id="station-ssid" placeholder="Select or enter network name" />
|
||||
|
||||
<div id="selected-network-info" class="hidden mt-3"></div>
|
||||
|
||||
<div class="text-right mt-4 mb-1 hidden" id="refresh-btn-row">
|
||||
<button class="btn btn-xs btn-ghost gap-1" id="refresh-networks-btn">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<polyline points="1 20 1 14 7 14"></polyline>
|
||||
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"></path>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul id="network-scan-list" class="list bg-base-100 rounded-lg border border-base-300 max-h-64 overflow-y-scroll mt-2 p-1 space-y-0.5">
|
||||
<li class="flex flex-col items-center gap-4 py-8">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<span class="text-sm opacity-60">Scanning for networks...</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p id="station-ssid-error" class="mt-2 text-sm text-error hidden"></p>
|
||||
</div>
|
||||
|
||||
<div id="password-field-wrapper" class="form-control hidden">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold mb-2">Password</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="password" class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary pr-10" id="station-password" placeholder="WiFi password" />
|
||||
<button type="button" id="toggle-station-password-btn" class="absolute right-3 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs p-1 min-h-0 h-7 hover:bg-primary hover:text-primary-content">
|
||||
<span id="station-eye-icon-open">
|
||||
<IconEye size={20} class="opacity-70 hover:opacity-100" />
|
||||
</span>
|
||||
<span id="station-eye-icon-closed" class="hidden">
|
||||
<IconEyeClosed size={20} class="opacity-70 hover:opacity-100" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p id="station-password-error" class="mt-2 text-sm text-error hidden"></p>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<label>
|
||||
|
||||
<span class="label-text font-semibold select-none">Enable Static IP Configuration</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" id="static-ip-toggle-modal" />
|
||||
</label>
|
||||
|
||||
<div id="static-ip-fields" class="hidden mb-2 py-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">IP Address</span>
|
||||
</label>
|
||||
<label class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary">
|
||||
<input type="text" class="w-full border-none outline-none bg-transparent" id="static-ip-modal" placeholder="192.168.1.100" />
|
||||
</label>
|
||||
<p id="static-ip-error" class="mt-2 text-sm text-error hidden">IP Address is required and invalid</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Netmask</span>
|
||||
</label>
|
||||
<label class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary">
|
||||
<input type="text" class="w-full border-none outline-none bg-transparent" id="static-netmask-modal" placeholder="255.255.255.0" />
|
||||
</label>
|
||||
<p id="static-netmask-error" class="mt-2 text-sm text-error hidden">Netmask is required and invalid</p>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Gateway</span>
|
||||
</label>
|
||||
<label class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary">
|
||||
<input type="text" class="w-full border-none outline-none bg-transparent" id="static-gateway-modal" placeholder="192.168.1.1" />
|
||||
</label>
|
||||
<p id="static-gateway-error" class="mt-2 text-sm text-error hidden">Gateway is required and invalid</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Summary and Confirmation View -->
|
||||
<div id="confirmation-view" class="hidden flex-col gap-2">
|
||||
<h2 class="text-base font-semibold">Configuration Summary</h2>
|
||||
<div class="divider my-1"></div>
|
||||
|
||||
<div id="confirm-network-entry"></div>
|
||||
|
||||
<ul class="list bg-base-100 rounded-lg border border-base-300 mt-2 p-1 space-y-0.5">
|
||||
<li>
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-base-200 rounded-lg">
|
||||
<span class="flex-1 text-base font-semibold text-base-content">IP Configuration</span>
|
||||
<span class="text-base text-base-content/70" id="summary-ip-mode"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li id="summary-ip-address-row" class="hidden">
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-base-200 rounded-lg">
|
||||
<span class="flex-1 text-base font-semibold text-base-content">IP Address</span>
|
||||
<span class="text-base font-mono text-base-content/70" id="summary-ip-address"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li id="summary-netmask-row" class="hidden">
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-base-200 rounded-lg">
|
||||
<span class="flex-1 text-base font-semibold text-base-content">Netmask</span>
|
||||
<span class="text-base font-mono text-base-content/70" id="summary-netmask"></span>
|
||||
</div>
|
||||
</li>
|
||||
<li id="summary-gateway-row" class="hidden">
|
||||
<div class="flex items-center gap-3 px-4 py-3 bg-base-200 rounded-lg">
|
||||
<span class="flex-1 text-base font-semibold text-base-content">Gateway</span>
|
||||
<span class="text-base font-mono text-base-content/70" id="summary-gateway"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-warning py-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold">Warning</span>
|
||||
<ul class="list-disc list-inside text-sm space-y-0.5">
|
||||
<li>The device may not be reachable over WiFi for a while during the configuration process.</li>
|
||||
<li>
|
||||
The WiFi credentials must be valid to establish a working connection. Otherwise, the device will revert to the previous valid WiFi configuration, if there was one.
|
||||
</li>
|
||||
<li>The device remains reachable via Ethernet (if connected) or USB at any time.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ethernet-warning" class="hidden alert alert-info py-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold">Ethernet Detected</span>
|
||||
<ul class="list-disc list-inside text-sm space-y-0.5">
|
||||
<li>Ethernet and WiFi can be active simultaneously. The device will be reachable through both interfaces.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-auto">
|
||||
<form method="dialog">
|
||||
<button class="btn" id="cancel-btn">Cancel</button>
|
||||
</form>
|
||||
<button class="btn btn-outline hidden" id="back-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
Back
|
||||
</button>
|
||||
<button class="btn btn-primary" id="save-station-btn">Save Configuration</button>
|
||||
<button class="btn btn-primary hidden" id="wifi-station-apply-btn">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
import {
|
||||
$scanned_wifi_networks,
|
||||
scan_wifi_networks,
|
||||
clear_scanned_wifi_networks,
|
||||
type WiFiNetwork,
|
||||
type NetworkConfigNVS,
|
||||
ConnectionMode,
|
||||
AntennaSelection,
|
||||
$current_network_configuration,
|
||||
send_new_network_configuration_to_alpha,
|
||||
} from "../backend_components/NetworkManagerStore";
|
||||
|
||||
import {
|
||||
is_valid_ip_format,
|
||||
isValidNetmask,
|
||||
isPracticalNetmask,
|
||||
isInValidHostRange,
|
||||
isInSameSubnet,
|
||||
} from "../helper_scripts/IP_Subnetmask_validation";
|
||||
|
||||
const stationSSID = document.getElementById("station-ssid") as HTMLInputElement;
|
||||
const stationSsidError = document.getElementById("station-ssid-error");
|
||||
const stationPassword = document.getElementById("station-password") as HTMLInputElement;
|
||||
const stationPasswordError = document.getElementById("station-password-error");
|
||||
const networkInfo = document.getElementById("selected-network-info");
|
||||
const passwordFieldWrapper = document.getElementById("password-field-wrapper");
|
||||
const scanList = document.getElementById("network-scan-list");
|
||||
const refreshBtnRow = document.getElementById("refresh-btn-row");
|
||||
const toggleStationPasswordBtn = document.getElementById("toggle-station-password-btn") as HTMLButtonElement | null;
|
||||
const stationEyeOpen = document.getElementById("station-eye-icon-open");
|
||||
const stationEyeClosed = document.getElementById("station-eye-icon-closed");
|
||||
|
||||
toggleStationPasswordBtn?.addEventListener("click", () => {
|
||||
if (!stationPassword) return;
|
||||
const isVisible = stationPassword.type === "text";
|
||||
stationPassword.type = isVisible ? "password" : "text";
|
||||
stationEyeOpen?.classList.toggle("hidden", !isVisible);
|
||||
stationEyeClosed?.classList.toggle("hidden", isVisible);
|
||||
});
|
||||
|
||||
const staticIpToggle = document.getElementById("static-ip-toggle-modal") as HTMLInputElement;
|
||||
const staticIpFields = document.getElementById("static-ip-fields");
|
||||
const staticIpError = document.getElementById("static-ip-error");
|
||||
const staticNetmaskError = document.getElementById("static-netmask-error");
|
||||
const staticGatewayError = document.getElementById("static-gateway-error");
|
||||
const staticIP_address = document.getElementById("static-ip-modal") as HTMLInputElement;
|
||||
const staticIP_netmask = document.getElementById("static-netmask-modal") as HTMLInputElement;
|
||||
const staticIP_gateway = document.getElementById("static-gateway-modal") as HTMLInputElement;
|
||||
const staticIP_address_wrap = staticIP_address?.parentElement;
|
||||
const staticIP_netmask_wrap = staticIP_netmask?.parentElement;
|
||||
const staticIP_gateway_wrap = staticIP_gateway?.parentElement;
|
||||
const modal = document.getElementById("station-config-modal") as HTMLDialogElement;
|
||||
const confirmationView = document.getElementById("confirmation-view");
|
||||
const configFormView = document.getElementById("config-form-view");
|
||||
const saveStationBtn = document.getElementById("save-station-btn");
|
||||
const applyBtn = document.getElementById("wifi-station-apply-btn");
|
||||
const backBtn = document.getElementById("back-btn");
|
||||
const cancelBtn = document.getElementById("cancel-btn");
|
||||
const confirmNetworkEntry = document.getElementById("confirm-network-entry");
|
||||
const summaryIPMode = document.getElementById("summary-ip-mode");
|
||||
const summaryIPAddressRow = document.getElementById("summary-ip-address-row");
|
||||
const summaryNetmaskRow = document.getElementById("summary-netmask-row");
|
||||
const summaryGatewayRow = document.getElementById("summary-gateway-row");
|
||||
const summaryIPAddress = document.getElementById("summary-ip-address");
|
||||
const summaryNetmask = document.getElementById("summary-netmask");
|
||||
const summaryGateway = document.getElementById("summary-gateway");
|
||||
const ethernetWarning = document.getElementById("ethernet-warning");
|
||||
|
||||
stationSSID.addEventListener("input", () => {
|
||||
filterText = stationSSID.value.trim();
|
||||
if (!isScanning && !scanTimedOut) {
|
||||
applyFilter();
|
||||
}
|
||||
});
|
||||
|
||||
function getSignalStrengthInfo(dbm: number) {
|
||||
if (dbm >= -50) {
|
||||
return { quality: "Excellent", color: "text-success", bars: 4 };
|
||||
} else if (dbm >= -60) {
|
||||
return { quality: "Good", color: "text-success", bars: 3 };
|
||||
} else if (dbm >= -70) {
|
||||
return { quality: "Fair", color: "text-warning", bars: 2 };
|
||||
} else {
|
||||
return { quality: "Weak", color: "text-error", bars: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
function wifiIconHtml(bars: number): string {
|
||||
const svgBase =
|
||||
'xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
||||
const pNone = '<path stroke="none" d="M0 0h24v24H0z" fill="none"/>';
|
||||
const pDot = '<path d="M12 18l.01 0"/>';
|
||||
const pArc1 = '<path d="M9.172 15.172a4 4 0 0 1 5.656 0"/>';
|
||||
const pArc2 = '<path d="M6.343 12.343a8 8 0 0 1 11.314 0"/>';
|
||||
const pArc3 = '<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0"/>';
|
||||
const fgPaths = bars >= 4 ? pDot + pArc1 + pArc2 + pArc3 : bars === 3 ? pDot + pArc1 + pArc2 : bars === 2 ? pDot + pArc1 : pDot;
|
||||
return (
|
||||
`<div class="relative flex-shrink-0" style="width:24px;height:24px">` +
|
||||
`<svg ${svgBase} class="absolute inset-0 opacity-20">${pNone}${pDot}${pArc1}${pArc2}${pArc3}</svg>` +
|
||||
`<svg ${svgBase} class="absolute inset-0 text-primary">${pNone}${fgPaths}</svg>` +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function rssiBadgeClass(bars: number): string {
|
||||
return bars >= 3 ? "badge-success" : bars === 2 ? "badge-warning" : "badge-error";
|
||||
}
|
||||
|
||||
function securityBadgeClass(secType: string): string {
|
||||
if (secType === "Open") return "badge-success";
|
||||
return "badge-info";
|
||||
}
|
||||
|
||||
function lockIconHtml(): string {
|
||||
return (
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-60 flex-shrink-0">` +
|
||||
`<path stroke="none" d="M0 0h24v24H0z" fill="none"/>` +
|
||||
`<path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"/>` +
|
||||
`<path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"/>` +
|
||||
`<path d="M8 11v-4a4 4 0 1 1 8 0v4"/>` +
|
||||
`</svg>`
|
||||
);
|
||||
}
|
||||
|
||||
let selectedNetworkForConfirm: { ssid: string; signal: number; security: string; channel: string } | null = null;
|
||||
let isScanning = false;
|
||||
let scanTimedOut = false;
|
||||
let scanTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
let filterText = "";
|
||||
|
||||
const SCAN_TIMEOUT_MS = 15000;
|
||||
|
||||
function renderNetworkList(networks: readonly WiFiNetwork[]) {
|
||||
const networkList = document.getElementById("network-scan-list");
|
||||
if (!networkList) return;
|
||||
|
||||
if (isScanning && networks.length === 0) {
|
||||
refreshBtnRow?.classList.add("hidden");
|
||||
networkList.innerHTML = `
|
||||
<li class="flex flex-col items-center gap-4 py-8">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
<span class="text-sm opacity-60">Scanning for networks...</span>
|
||||
</li>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (scanTimedOut && networks.length === 0) {
|
||||
refreshBtnRow?.classList.add("hidden");
|
||||
networkList.innerHTML = `
|
||||
<li class="flex flex-col items-center gap-4 py-8">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-error opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
</svg>
|
||||
<span class="text-sm text-error font-semibold">Scan timed out — no response from device.</span>
|
||||
<button class="btn btn-outline btn-error btn-sm" id="retry-btn">Retry</button>
|
||||
</li>
|
||||
`;
|
||||
document.getElementById("retry-btn")?.addEventListener("click", () => {
|
||||
performScan();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isScanning && networks.length === 0) {
|
||||
refreshBtnRow?.classList.add("hidden");
|
||||
networkList.innerHTML = `
|
||||
<li class="flex flex-col items-center gap-4 py-8">
|
||||
<span class="text-sm opacity-60">No networks found. Try scanning again.</span>
|
||||
<button class="btn btn-outline btn-primary" id="rescan-btn">Rescan</button>
|
||||
</li>
|
||||
`;
|
||||
document.getElementById("rescan-btn")?.addEventListener("click", () => {
|
||||
performScan();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
refreshBtnRow?.classList.remove("hidden");
|
||||
networkList.innerHTML = networks
|
||||
.map((network) => {
|
||||
const signalInfo = getSignalStrengthInfo(network.RSSI);
|
||||
return `
|
||||
<li class="flex items-center gap-3 px-4 py-4 bg-base-200 rounded-lg border border-transparent hover:bg-primary/10 hover:border-primary/30 active:bg-primary/20 transition-colors duration-150 cursor-pointer network-item" data-ssid="${network.SSID}" data-signal="${network.RSSI}" data-security="${network.SecurityType}" data-channel="${network.Channel}">
|
||||
<div class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span class="text-xs text-base-content/40 shrink-0">SSID</span>
|
||||
<span class="text-base font-bold text-primary truncate">${network.SSID}</span>
|
||||
</div>
|
||||
<div class="hidden sm:block w-20 shrink-0 text-sm text-base-content/50 text-center">Channel ${network.Channel}</div>
|
||||
<div class="w-5 shrink-0 flex items-center justify-center">${network.SecurityType !== "Open" ? lockIconHtml() : ""}</div>
|
||||
<div class="shrink-0">${wifiIconHtml(signalInfo.bars)}</div>
|
||||
<div class="hidden sm:block w-44 shrink-0 ml-2"><span class="badge badge-sm ${rssiBadgeClass(signalInfo.bars)}">${network.RSSI} dBm · ${signalInfo.quality}</span></div>
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
document.querySelectorAll(".network-item").forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
const ssid = item.getAttribute("data-ssid") || "";
|
||||
const signal = parseInt(item.getAttribute("data-signal") || "0");
|
||||
const security = item.getAttribute("data-security") || "";
|
||||
const channel = item.getAttribute("data-channel") || "";
|
||||
|
||||
if (stationSSID) stationSSID.value = ssid;
|
||||
selectedNetworkForConfirm = { ssid, signal, security, channel };
|
||||
|
||||
if (networkInfo) {
|
||||
const signalInfo = getSignalStrengthInfo(signal);
|
||||
networkInfo.innerHTML = `
|
||||
<div class="flex items-center gap-3 px-4 py-4 bg-base-200 rounded-lg border border-primary/30">
|
||||
<div class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span class="text-xs text-base-content/40 shrink-0">SSID</span>
|
||||
<span class="text-base font-bold text-primary truncate">${ssid}</span>
|
||||
</div>
|
||||
<div class="hidden sm:block w-20 shrink-0 text-sm text-base-content/50 text-center">Channel ${channel}</div>
|
||||
<div class="w-5 shrink-0 flex items-center justify-center">${security !== "Open" ? lockIconHtml() : ""}</div>
|
||||
<div class="shrink-0">${wifiIconHtml(signalInfo.bars)}</div>
|
||||
<div class="hidden sm:block w-44 shrink-0 ml-2"><span class="badge badge-sm ${rssiBadgeClass(signalInfo.bars)}">${signal} dBm · ${signalInfo.quality}</span></div>
|
||||
<button class="btn btn-sm btn-circle btn-ghost hover:bg-primary hover:text-primary-content border-0 shrink-0" id="clear-network-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
networkInfo.classList.remove("hidden");
|
||||
refreshBtnRow?.classList.add("hidden");
|
||||
scanList?.classList.add("hidden");
|
||||
if (security !== "Open") {
|
||||
passwordFieldWrapper?.classList.remove("hidden");
|
||||
} else {
|
||||
passwordFieldWrapper?.classList.add("hidden");
|
||||
if (stationPassword) {
|
||||
stationPassword.value = "";
|
||||
stationPassword.classList.remove("input-error");
|
||||
}
|
||||
stationPasswordError?.classList.add("hidden");
|
||||
}
|
||||
document.getElementById("clear-network-btn")?.addEventListener("click", () => {
|
||||
if (stationSSID) stationSSID.value = "";
|
||||
selectedNetworkForConfirm = null;
|
||||
filterText = "";
|
||||
networkInfo.classList.add("hidden");
|
||||
networkInfo.innerHTML = "";
|
||||
passwordFieldWrapper?.classList.add("hidden");
|
||||
if (stationPassword) {
|
||||
stationPassword.value = "";
|
||||
stationPassword.classList.remove("input-error");
|
||||
}
|
||||
stationPasswordError?.classList.add("hidden");
|
||||
scanList?.classList.remove("hidden");
|
||||
applyFilter();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("refresh-networks-btn")?.addEventListener("click", () => {
|
||||
console.log("refresh-button clicked: sending scan command of wifi networks!");
|
||||
performScan();
|
||||
});
|
||||
|
||||
function applyFilter() {
|
||||
const allNetworks = $scanned_wifi_networks.get();
|
||||
if (!filterText) {
|
||||
renderNetworkList(allNetworks);
|
||||
return;
|
||||
}
|
||||
const q = filterText.toLowerCase();
|
||||
const filtered = allNetworks.filter((n) => n.SSID.toLowerCase().includes(q));
|
||||
if (filtered.length === 0) {
|
||||
refreshBtnRow?.classList.add("hidden");
|
||||
const networkList = document.getElementById("network-scan-list");
|
||||
if (networkList) {
|
||||
networkList.innerHTML = `
|
||||
<li class="flex flex-col items-center gap-4 py-8">
|
||||
<span class="text-sm opacity-60">No network with name "<span class="font-medium">${filterText}</span>" found.</span>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
renderNetworkList(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
function performScan() {
|
||||
if (scanTimeoutHandle) {
|
||||
clearTimeout(scanTimeoutHandle);
|
||||
scanTimeoutHandle = null;
|
||||
}
|
||||
isScanning = true;
|
||||
scanTimedOut = false;
|
||||
if (stationSSID) stationSSID.disabled = true;
|
||||
renderNetworkList([]);
|
||||
scan_wifi_networks();
|
||||
scanTimeoutHandle = setTimeout(() => {
|
||||
scanTimeoutHandle = null;
|
||||
if (isScanning) {
|
||||
isScanning = false;
|
||||
scanTimedOut = true;
|
||||
if (stationSSID) stationSSID.disabled = false;
|
||||
renderNetworkList($scanned_wifi_networks.get());
|
||||
}
|
||||
}, SCAN_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
$scanned_wifi_networks.subscribe((networks) => {
|
||||
if (isScanning && networks.length === 0) {
|
||||
renderNetworkList([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isScanning && networks.length > 0) {
|
||||
isScanning = false;
|
||||
scanTimedOut = false;
|
||||
if (stationSSID) stationSSID.disabled = false;
|
||||
if (scanTimeoutHandle) {
|
||||
clearTimeout(scanTimeoutHandle);
|
||||
scanTimeoutHandle = null;
|
||||
}
|
||||
}
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
if (modal) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "open" && modal.open) {
|
||||
// Full reset on every open so stale state never leaks between sessions
|
||||
if (stationSSID) {
|
||||
stationSSID.value = "";
|
||||
stationSSID.classList.remove("input-error");
|
||||
}
|
||||
if (stationPassword) {
|
||||
stationPassword.value = "";
|
||||
stationPassword.classList.remove("input-error");
|
||||
}
|
||||
stationPasswordError?.classList.add("hidden");
|
||||
stationSsidError?.classList.add("hidden");
|
||||
filterText = "";
|
||||
passwordFieldWrapper?.classList.add("hidden");
|
||||
networkInfo?.classList.add("hidden");
|
||||
if (networkInfo) networkInfo.innerHTML = "";
|
||||
scanList?.classList.remove("hidden");
|
||||
selectedNetworkForConfirm = null;
|
||||
if (stationPassword) stationPassword.type = "password";
|
||||
stationEyeOpen?.classList.remove("hidden");
|
||||
stationEyeClosed?.classList.add("hidden");
|
||||
performScan();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(modal, { attributes: true, attributeFilter: ["open"] });
|
||||
}
|
||||
|
||||
modal?.addEventListener("close", () => {
|
||||
// reset the forms
|
||||
showFormView();
|
||||
pendingConfig = null;
|
||||
|
||||
if (stationSSID) {
|
||||
stationSSID.value = "";
|
||||
stationSSID.classList.remove("input-error");
|
||||
}
|
||||
if (stationPassword) {
|
||||
stationPassword.value = "";
|
||||
stationPassword.classList.remove("input-error");
|
||||
}
|
||||
if (staticIpToggle) staticIpToggle.checked = false;
|
||||
if (staticIpFields) staticIpFields.classList.add("hidden");
|
||||
passwordFieldWrapper?.classList.add("hidden");
|
||||
refreshBtnRow?.classList.add("hidden");
|
||||
if (stationSsidError) stationSsidError.classList.add("hidden");
|
||||
if (stationPasswordError) stationPasswordError.classList.add("hidden");
|
||||
// static configuration fields
|
||||
staticIP_address_wrap?.classList.remove("input-error");
|
||||
staticIP_address!.value = "";
|
||||
staticIP_netmask_wrap?.classList.remove("input-error");
|
||||
staticIP_netmask!.value = "";
|
||||
staticIP_gateway_wrap?.classList.remove("input-error");
|
||||
staticIP_gateway!.value = "";
|
||||
staticIpError?.classList.add("hidden");
|
||||
staticNetmaskError?.classList.add("hidden");
|
||||
staticGatewayError?.classList.add("hidden");
|
||||
|
||||
isScanning = false;
|
||||
scanTimedOut = false;
|
||||
filterText = "";
|
||||
if (scanTimeoutHandle) {
|
||||
clearTimeout(scanTimeoutHandle);
|
||||
scanTimeoutHandle = null;
|
||||
}
|
||||
networkInfo?.classList.add("hidden");
|
||||
if (networkInfo) networkInfo.innerHTML = "";
|
||||
scanList?.classList.remove("hidden");
|
||||
selectedNetworkForConfirm = null;
|
||||
if (stationPassword) stationPassword.type = "password";
|
||||
stationEyeOpen?.classList.remove("hidden");
|
||||
stationEyeClosed?.classList.add("hidden");
|
||||
clear_scanned_wifi_networks();
|
||||
renderNetworkList([]);
|
||||
});
|
||||
|
||||
staticIpToggle?.addEventListener("change", () => {
|
||||
if (staticIpToggle.checked) {
|
||||
staticIpFields?.classList.remove("hidden");
|
||||
// Pre-fill from saved static config, or fall back to current DHCP lease values
|
||||
const cfg = $current_network_configuration.get();
|
||||
if (!staticIP_address?.value) {
|
||||
staticIP_address!.value =
|
||||
cfg.Static_Cfg_IP_Address_Station ||
|
||||
cfg.IP_Address_Station ||
|
||||
"";
|
||||
}
|
||||
if (!staticIP_netmask?.value) {
|
||||
staticIP_netmask!.value =
|
||||
cfg.Static_Cfg_Netmask_Station ||
|
||||
cfg.Netmask_Station ||
|
||||
"";
|
||||
}
|
||||
if (!staticIP_gateway?.value) {
|
||||
staticIP_gateway!.value =
|
||||
cfg.Static_Cfg_Gateway_IP_Address_Station ||
|
||||
cfg.Gateway_IP_Address_Station ||
|
||||
"";
|
||||
}
|
||||
} else {
|
||||
staticIpFields?.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
function showConfirmationView(config: NetworkConfigNVS) {
|
||||
if (!confirmationView || !configFormView || !summaryIPMode) return;
|
||||
|
||||
// Hide form view and show confirmation view
|
||||
configFormView.classList.add("hidden");
|
||||
confirmationView.classList.remove("hidden");
|
||||
confirmationView.classList.add("flex");
|
||||
|
||||
// Update button visibility
|
||||
saveStationBtn?.classList.add("hidden");
|
||||
applyBtn?.classList.remove("hidden");
|
||||
backBtn?.classList.remove("hidden");
|
||||
|
||||
// Populate network entry card
|
||||
if (confirmNetworkEntry) {
|
||||
if (selectedNetworkForConfirm) {
|
||||
const { ssid, signal, security, channel } = selectedNetworkForConfirm;
|
||||
const si = getSignalStrengthInfo(signal);
|
||||
confirmNetworkEntry.innerHTML = `
|
||||
<div class="flex items-center gap-3 px-4 py-4 bg-base-200 rounded-lg border border-primary/30">
|
||||
<div class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span class="text-xs text-base-content/40 shrink-0">SSID</span>
|
||||
<span class="text-base font-bold text-primary truncate">${ssid}</span>
|
||||
</div>
|
||||
<div class="hidden sm:block w-20 shrink-0 text-sm text-base-content/50 text-center">Channel ${channel}</div>
|
||||
<div class="w-5 shrink-0 flex items-center justify-center">${security !== "Open" ? lockIconHtml() : ""}</div>
|
||||
<div class="shrink-0">${wifiIconHtml(si.bars)}</div>
|
||||
<div class="hidden sm:block w-44 shrink-0 ml-2"><span class="badge badge-sm ${rssiBadgeClass(si.bars)}">${signal} dBm · ${si.quality}</span></div>
|
||||
</div>`;
|
||||
} else {
|
||||
confirmNetworkEntry.innerHTML = `
|
||||
<div class="flex items-center gap-3 px-4 py-4 bg-base-200 rounded-lg border border-primary/30">
|
||||
<div class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span class="text-xs text-base-content/40 shrink-0">SSID</span>
|
||||
<span class="text-base font-bold text-primary truncate">${config.SSID_Station}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
summaryIPMode.textContent = config.DHCP_Mode_Station ? "DHCP (Automatic)" : "Static IP";
|
||||
|
||||
if (
|
||||
!config.DHCP_Mode_Station &&
|
||||
summaryIPAddressRow &&
|
||||
summaryNetmaskRow &&
|
||||
summaryGatewayRow &&
|
||||
summaryIPAddress &&
|
||||
summaryNetmask &&
|
||||
summaryGateway
|
||||
) {
|
||||
summaryIPAddressRow.classList.remove("hidden");
|
||||
summaryNetmaskRow.classList.remove("hidden");
|
||||
summaryGatewayRow.classList.remove("hidden");
|
||||
summaryIPAddress.textContent = config.Static_Cfg_IP_Address_Station;
|
||||
summaryNetmask.textContent = config.Static_Cfg_Netmask_Station;
|
||||
summaryGateway.textContent = config.Static_Cfg_Gateway_IP_Address_Station;
|
||||
} else {
|
||||
if (summaryIPAddressRow) summaryIPAddressRow.classList.add("hidden");
|
||||
if (summaryNetmaskRow) summaryNetmaskRow.classList.add("hidden");
|
||||
if (summaryGatewayRow) summaryGatewayRow.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (ethernetWarning) {
|
||||
ethernetWarning.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function showFormView() {
|
||||
if (!confirmationView || !configFormView) return;
|
||||
|
||||
// Hide confirmation view and show form view
|
||||
confirmationView.classList.add("hidden");
|
||||
confirmationView.classList.remove("flex");
|
||||
configFormView.classList.remove("hidden");
|
||||
|
||||
// Update button visibility
|
||||
saveStationBtn?.classList.remove("hidden");
|
||||
applyBtn?.classList.add("hidden");
|
||||
backBtn?.classList.add("hidden");
|
||||
}
|
||||
|
||||
let pendingConfig: NetworkConfigNVS | null = null;
|
||||
|
||||
saveStationBtn?.addEventListener("click", () => {
|
||||
const _currCfg = $current_network_configuration.get();
|
||||
const _newStaDHCP = !staticIpToggle?.checked; // toggle checked = static IP, unchecked = DHCP
|
||||
const _staMode = _currCfg.DHCP_Mode_Ethernet
|
||||
? (_newStaDHCP ? ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP : ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC)
|
||||
: (_newStaDHCP ? ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP : ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC);
|
||||
const _cfg = $current_network_configuration.get();
|
||||
let temp_station_network_config: NetworkConfigNVS = {
|
||||
Connection_Mode: _staMode,
|
||||
Antenna_Power: _cfg.Antenna_Power,
|
||||
Antenna_Selection: _cfg.Antenna_Selection,
|
||||
DHCP_Mode_Station: _cfg.DHCP_Mode_Station,
|
||||
DHCP_Mode_Ethernet: _cfg.DHCP_Mode_Ethernet,
|
||||
AP_Channel: _cfg.AP_Channel,
|
||||
WiFi_Enabled: _cfg.WiFi_Enabled,
|
||||
IP_Address_Ethernet: _cfg.IP_Address_Ethernet,
|
||||
IP_Address_Station: _cfg.IP_Address_Station,
|
||||
Gateway_IP_Address_Ethernet: _cfg.Gateway_IP_Address_Ethernet,
|
||||
Gateway_IP_Address_Station: _cfg.Gateway_IP_Address_Station,
|
||||
Netmask_Ethernet: _cfg.Netmask_Ethernet,
|
||||
Netmask_Station: _cfg.Netmask_Station,
|
||||
Static_Cfg_IP_Address_Ethernet: _cfg.Static_Cfg_IP_Address_Ethernet,
|
||||
Static_Cfg_IP_Address_Station: _cfg.Static_Cfg_IP_Address_Station,
|
||||
Static_Cfg_Gateway_IP_Address_Station: "",
|
||||
Static_Cfg_Gateway_IP_Address_Ethernet: _cfg.Static_Cfg_Gateway_IP_Address_Ethernet,
|
||||
Static_Cfg_Netmask_Station: "",
|
||||
Static_Cfg_Netmask_Ethernet: _cfg.Static_Cfg_Netmask_Ethernet,
|
||||
SSID_Station: "",
|
||||
Password_Station: "",
|
||||
SSID_AP: _cfg.SSID_AP,
|
||||
Password_AP: _cfg.Password_AP,
|
||||
MDNS_Hostname: _cfg.MDNS_Hostname,
|
||||
};
|
||||
|
||||
// reset the error messages
|
||||
stationSsidError?.classList.add("hidden");
|
||||
stationPasswordError?.classList.add("hidden");
|
||||
staticIpError?.classList.add("hidden");
|
||||
staticNetmaskError?.classList.add("hidden");
|
||||
staticGatewayError?.classList.add("hidden");
|
||||
// first validate the configuration!
|
||||
if (!stationSSID.value) {
|
||||
stationSSID.classList.add("input-error");
|
||||
if (stationSsidError) { stationSsidError.textContent = "Select a network first."; stationSsidError.classList.remove("hidden"); }
|
||||
return;
|
||||
} else if (stationSSID.value.length > 31) {
|
||||
stationSSID.classList.add("input-error");
|
||||
if (stationSsidError) { stationSsidError.textContent = "SSID must be shorter than 32 characters."; stationSsidError.classList.remove("hidden"); }
|
||||
return;
|
||||
} else {
|
||||
stationSSID.classList.remove("input-error");
|
||||
stationSsidError?.classList.add("hidden");
|
||||
}
|
||||
const passwordRequired = passwordFieldWrapper && !passwordFieldWrapper.classList.contains("hidden");
|
||||
if (passwordRequired && !stationPassword.value) {
|
||||
stationPassword?.classList.add("input-error");
|
||||
if (stationPasswordError) { stationPasswordError.textContent = "Password is required for secured networks."; stationPasswordError.classList.remove("hidden"); }
|
||||
return;
|
||||
} else if (stationPassword.value.length > 31) {
|
||||
stationPassword?.classList.add("input-error");
|
||||
if (stationPasswordError) { stationPasswordError.textContent = "Password must be shorter than 32 characters."; stationPasswordError.classList.remove("hidden"); }
|
||||
return;
|
||||
} else {
|
||||
stationPassword?.classList.remove("input-error");
|
||||
stationPasswordError?.classList.add("hidden");
|
||||
}
|
||||
if (staticIpToggle.checked) {
|
||||
// validate static IP configuration
|
||||
let isValid = true;
|
||||
|
||||
// FIRST: Validate netmask format and structure (must be checked before IP/Gateway)
|
||||
if (!staticIP_netmask?.value || !is_valid_ip_format(staticIP_netmask?.value)) {
|
||||
staticIP_netmask_wrap?.classList.add("input-error");
|
||||
if (staticNetmaskError) {
|
||||
staticNetmaskError.classList.remove("hidden");
|
||||
staticNetmaskError.textContent = "Invalid Netmask format";
|
||||
}
|
||||
isValid = false;
|
||||
// Scroll to error field to make it visible
|
||||
staticIP_netmask.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
} else if (!isValidNetmask(staticIP_netmask?.value)) {
|
||||
staticIP_netmask_wrap?.classList.add("input-error");
|
||||
if (staticNetmaskError) {
|
||||
staticNetmaskError.classList.remove("hidden");
|
||||
staticNetmaskError.textContent = "Invalid Netmask";
|
||||
}
|
||||
isValid = false;
|
||||
// Scroll to error field to make it visible
|
||||
staticIP_netmask.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
} else if (!isPracticalNetmask(staticIP_netmask?.value)) {
|
||||
staticIP_netmask_wrap?.classList.add("input-error");
|
||||
if (staticNetmaskError) {
|
||||
staticNetmaskError.classList.remove("hidden");
|
||||
staticNetmaskError.textContent = "Netmask too restrictive (needs space for IP and Gateway)";
|
||||
}
|
||||
isValid = false;
|
||||
// Scroll to error field to make it visible
|
||||
staticIP_netmask.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
} else {
|
||||
staticNetmaskError?.classList.add("hidden");
|
||||
staticIP_netmask_wrap?.classList.remove("input-error");
|
||||
}
|
||||
|
||||
// Validate IP address format
|
||||
if (!staticIP_address.value || !is_valid_ip_format(staticIP_address.value)) {
|
||||
staticIP_address_wrap?.classList.add("input-error");
|
||||
if (staticIpError) {
|
||||
staticIpError.classList.remove("hidden");
|
||||
staticIpError.textContent = "Invalid IP address format";
|
||||
}
|
||||
isValid = false;
|
||||
// Scroll to error field to make it visible
|
||||
staticIP_address.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
} else {
|
||||
staticIpError?.classList.add("hidden");
|
||||
staticIP_address_wrap?.classList.remove("input-error");
|
||||
}
|
||||
|
||||
// Validate gateway format
|
||||
if (!staticIP_gateway.value || !is_valid_ip_format(staticIP_gateway.value)) {
|
||||
staticIP_gateway_wrap?.classList.add("input-error");
|
||||
if (staticGatewayError) {
|
||||
staticGatewayError?.classList.remove("hidden");
|
||||
staticGatewayError!.textContent = "Invalid Gateway IP format";
|
||||
}
|
||||
isValid = false;
|
||||
// Scroll to error field to make it visible
|
||||
staticIP_gateway.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
} else {
|
||||
staticGatewayError?.classList.add("hidden");
|
||||
staticIP_gateway_wrap?.classList.remove("input-error");
|
||||
}
|
||||
|
||||
// If all formats are valid, check IP range and subnet consistency
|
||||
if (isValid) {
|
||||
const ip = staticIP_address.value;
|
||||
const netmask = staticIP_netmask.value;
|
||||
const gateway = staticIP_gateway.value;
|
||||
|
||||
// Check if IP is in valid host range (not network or broadcast address)
|
||||
if (!isInValidHostRange(ip, netmask)) {
|
||||
staticIP_address_wrap?.classList.add("input-error");
|
||||
staticIpError?.classList.remove("hidden");
|
||||
staticIpError!.textContent = "IP address cannot be network or broadcast address";
|
||||
isValid = false;
|
||||
staticIP_address.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
|
||||
// Check if gateway is in the same subnet as the IP
|
||||
if (!isInSameSubnet(ip, gateway, netmask)) {
|
||||
// Show error on both IP and Gateway fields for better UX
|
||||
staticIP_address_wrap?.classList.add("input-error");
|
||||
staticIpError?.classList.remove("hidden");
|
||||
staticIpError!.textContent = "IP and Gateway must be in the same subnet";
|
||||
|
||||
staticIP_gateway_wrap?.classList.add("input-error");
|
||||
staticGatewayError?.classList.remove("hidden");
|
||||
staticGatewayError!.textContent = "IP and Gateway must be in the same subnet";
|
||||
isValid = false;
|
||||
// Scroll to first error field
|
||||
staticIP_address.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
|
||||
// Check if gateway is in valid host range
|
||||
if (isInSameSubnet(ip, gateway, netmask) && !isInValidHostRange(gateway, netmask)) {
|
||||
staticIP_gateway_wrap?.classList.add("input-error");
|
||||
staticGatewayError?.classList.remove("hidden");
|
||||
staticGatewayError!.textContent = "Gateway cannot be network or broadcast address";
|
||||
isValid = false;
|
||||
staticIP_gateway.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
// check if gateway is same as host IP
|
||||
if (ip === gateway) {
|
||||
staticIP_gateway_wrap?.classList.add("input-error");
|
||||
staticGatewayError?.classList.remove("hidden");
|
||||
staticGatewayError!.textContent = "Gateway cannot be the same as host IP";
|
||||
isValid = false;
|
||||
staticIP_gateway.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
} else {
|
||||
// Static IP configuration is valid --> set network config!
|
||||
temp_station_network_config.Static_Cfg_IP_Address_Station = staticIP_address.value;
|
||||
temp_station_network_config.Static_Cfg_Netmask_Station = staticIP_netmask.value;
|
||||
temp_station_network_config.Static_Cfg_Gateway_IP_Address_Station = staticIP_gateway.value;
|
||||
temp_station_network_config.SSID_Station = stationSSID.value;
|
||||
temp_station_network_config.Password_Station = stationPassword.value;
|
||||
temp_station_network_config.DHCP_Mode_Station = false;
|
||||
temp_station_network_config.WiFi_Enabled = true;
|
||||
}
|
||||
} else {
|
||||
// enable DHCP mode
|
||||
temp_station_network_config.Static_Cfg_IP_Address_Station = "";
|
||||
temp_station_network_config.Static_Cfg_Netmask_Station = "";
|
||||
temp_station_network_config.Static_Cfg_Gateway_IP_Address_Station = "";
|
||||
temp_station_network_config.SSID_Station = stationSSID.value;
|
||||
temp_station_network_config.Password_Station = stationPassword.value;
|
||||
temp_station_network_config.DHCP_Mode_Station = true;
|
||||
temp_station_network_config.WiFi_Enabled = true;
|
||||
}
|
||||
|
||||
// Store the config and show confirmation view
|
||||
pendingConfig = temp_station_network_config;
|
||||
showConfirmationView(temp_station_network_config);
|
||||
});
|
||||
|
||||
// Confirm and apply button handler
|
||||
applyBtn?.addEventListener("click", () => {
|
||||
if (!pendingConfig) return;
|
||||
|
||||
// send configuration to Alpha!
|
||||
if (send_new_network_configuration_to_alpha(pendingConfig)) {
|
||||
window.dispatchEvent(new CustomEvent("network-config-applying"));
|
||||
modal?.close();
|
||||
pendingConfig = null;
|
||||
} else {
|
||||
// Show error or keep modal open
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Back button handler
|
||||
backBtn?.addEventListener("click", () => {
|
||||
showFormView();
|
||||
pendingConfig = null;
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { $testbenchWebsocketConnectionStatus } from './TestbenchStore';
|
||||
|
||||
export default function TestbenchConnectionStatusBadge() {
|
||||
const status = useStore($testbenchWebsocketConnectionStatus);
|
||||
|
||||
let colorClass = 'bg-gray-400 text-white';
|
||||
if (status.startsWith('Connected')) colorClass = 'bg-green-500 text-white';
|
||||
else if (status.startsWith('Connecting')) colorClass = 'bg-yellow-400 text-black';
|
||||
else if (
|
||||
status.startsWith('Error') ||
|
||||
status.startsWith('Failed') ||
|
||||
status.startsWith('Disconnected')
|
||||
)
|
||||
colorClass = 'bg-red-500 text-white';
|
||||
|
||||
// Passe die Breite ggf. an den längsten Status an!
|
||||
const maxText = 'Connecting ...';
|
||||
|
||||
return (
|
||||
<span
|
||||
class={`badge ${colorClass} font-mono relative inline-block transition-all duration-200`}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px', padding: 0 }}
|
||||
>
|
||||
{/* Unsichtbarer Platzhalter für feste Breite und Position */}
|
||||
<span
|
||||
class="opacity-0 select-none pointer-events-none"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{maxText}
|
||||
</span>
|
||||
{/* Sichtbarer Status absolut darüber */}
|
||||
<span
|
||||
class="absolute left-0 top-0 w-full h-full flex items-center"
|
||||
style={{ justifyContent: 'left', paddingLeft: '0.75rem' /* ggf. anpassen */ }}
|
||||
>
|
||||
{status}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,692 +0,0 @@
|
|||
---
|
||||
import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
|
||||
const pageTitle = "Test Bench Dashboard";
|
||||
---
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-bold">{pageTitle}</h1>
|
||||
|
||||
<!-- Board Connection Status -->
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title">Board Connection Status</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="badge badge-lg" id="board-status-badge">
|
||||
<span class="mr-2">●</span>
|
||||
<span id="board-status-text">Disconnected</span>
|
||||
</div>
|
||||
<div class="badge badge-lg badge-neutral" id="vcc-display">
|
||||
VCC: 12V
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">Testbench:</span>
|
||||
<TestbenchConnectionStatusBadge client:load />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<!-- Board Power Relay -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">Power Control</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">Board Relay:</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle border-red-600 bg-red-500 checked:border-orange-500 checked:bg-orange-400"
|
||||
id="power-relay-toggle"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">GND Sense:</span>
|
||||
<div class="badge badge-lg" id="gnd-sense-status">
|
||||
<span class="mr-2">●</span>
|
||||
<span id="gnd-sense-text">No Board Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UART Routing -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">UART Routing</h2>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
class="join-item btn flex-1"
|
||||
type="radio"
|
||||
name="uart_routing"
|
||||
aria-label="ST"
|
||||
id="uart-st"
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
class="join-item btn flex-1"
|
||||
type="radio"
|
||||
name="uart_routing"
|
||||
aria-label="P4"
|
||||
id="uart-p4"
|
||||
/>
|
||||
<input
|
||||
class="join-item btn flex-1"
|
||||
type="radio"
|
||||
name="uart_routing"
|
||||
aria-label="C6"
|
||||
id="uart-c6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootloader Control -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">Bootloader Control</h2>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
class="btn btn-error btn-sm w-full"
|
||||
id="p4-bootloader-btn"
|
||||
>
|
||||
P4 Bootloader OFF
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error btn-sm w-full"
|
||||
id="c6-bootloader-btn"
|
||||
>
|
||||
C6 Bootloader OFF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enable Control -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg p-4">
|
||||
<h2 class="card-title text-lg mb-3">Enable Control</h2>
|
||||
<div class="space-y-2">
|
||||
<button class="btn btn-error btn-sm w-full" id="p4-enable-btn">
|
||||
P4 Disabled
|
||||
</button>
|
||||
<button class="btn btn-error btn-sm w-full" id="c6-enable-btn">
|
||||
C6 Disabled
|
||||
</button>
|
||||
<button class="btn btn-error btn-sm w-full" id="st-enable-btn">
|
||||
ST Disabled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UART Console -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-lg mb-3">UART Console</h2>
|
||||
|
||||
<!-- Console Output -->
|
||||
<div
|
||||
class="mockup-code w-full h-64 overflow-y-auto mb-4"
|
||||
id="uart-console"
|
||||
>
|
||||
<pre
|
||||
data-prefix=">"
|
||||
class="text-info"><code>UART Console Ready</code></pre>
|
||||
<pre
|
||||
data-prefix="RX"
|
||||
class="text-success"><code>Waiting for data...</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- Input Controls -->
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter UART message"
|
||||
class="input input-bordered flex-1"
|
||||
id="uart-input"
|
||||
/>
|
||||
<button class="btn btn-primary" id="send-uart">Send</button>
|
||||
<button class="btn btn-outline btn-sm" id="clear-console"
|
||||
>Clear</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Number Conversion Utility -->
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body bg-base-200 rounded-lg">
|
||||
<h2 class="card-title text-lg mb-3">Number Conversion Utility</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Decimal Input -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Decimal</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="255"
|
||||
class="input input-bordered"
|
||||
id="decimal-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Hex Output -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Hexadecimal</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="0xFF"
|
||||
class="input input-bordered"
|
||||
id="hex-output"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Binary Output -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Binary</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="11111111"
|
||||
class="input input-bordered"
|
||||
id="binary-output"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button class="btn btn-outline btn-sm" id="copy-hex"
|
||||
>Copy Hex</button
|
||||
>
|
||||
<button class="btn btn-outline btn-sm" id="copy-binary"
|
||||
>Copy Binary</button
|
||||
>
|
||||
<button class="btn btn-outline btn-sm" id="clear-converter"
|
||||
>Clear</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import {
|
||||
$testbenchStates,
|
||||
sendCommandToBackend,
|
||||
TESTBENCH_COMMANDS,
|
||||
} from "./TestbenchStore.tsx";
|
||||
import { $testbenchWebsocketConnectionStatus } from "./TestbenchStore.tsx";
|
||||
|
||||
// Extend Window interface for custom properties
|
||||
declare global {
|
||||
interface Window {
|
||||
updateTestBenchStatus: (data: any) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// State management
|
||||
let currentUartRouting = "st"; // 'st', 'p4', 'c6' - default to ST
|
||||
let p4BootloaderActive = false;
|
||||
let c6BootloaderActive = false;
|
||||
let p4Enabled = false;
|
||||
let c6Enabled = false;
|
||||
let stEnabled = false;
|
||||
let boardConnected = false;
|
||||
let powerRelayEnabled = false;
|
||||
let gndSense = false;
|
||||
let vccVoltage = 12; // Default VCC voltage
|
||||
|
||||
// Update board connection status
|
||||
// Global function to update WebSocket connection status (for HydroBuddy address)
|
||||
function updateWebSocketStatus(connected: boolean) {
|
||||
const badge = document.getElementById("board-status-badge");
|
||||
const text = document.getElementById("board-status-text");
|
||||
if (badge && text) {
|
||||
if (connected) {
|
||||
badge.className = "badge badge-lg badge-success";
|
||||
text.textContent = "Connected";
|
||||
} else {
|
||||
badge.className = "badge badge-lg badge-error";
|
||||
text.textContent = "Disconnected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global function to update board connection status (for GND Sense)
|
||||
function updateBoardConnectionStatus(connected: boolean) {
|
||||
boardConnected = connected;
|
||||
const badge = document.getElementById("board-connection-badge");
|
||||
const text = document.getElementById("board-connection-text");
|
||||
if (badge && text) {
|
||||
if (connected) {
|
||||
badge.className = "badge badge-lg badge-success";
|
||||
text.textContent = "Board Connected";
|
||||
} else {
|
||||
badge.className = "badge badge-lg badge-error";
|
||||
text.textContent = "Board Disconnected";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update VCC voltage display
|
||||
function updateVccDisplay(voltage: number) {
|
||||
vccVoltage = voltage;
|
||||
const vccDisplay = document.getElementById("vcc-display");
|
||||
if (vccDisplay) {
|
||||
vccDisplay.textContent = `VCC: ${voltage}V`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update GND Sense status display (based on boardConnected status)
|
||||
function updateGndSenseStatus(boardConnected: boolean) {
|
||||
gndSense = !boardConnected; // GND Sense is LOW when board is connected
|
||||
// Update both GND Sense display and Board Connection display
|
||||
const gndBadge = document.getElementById("gnd-sense-status");
|
||||
const gndText = document.getElementById("gnd-sense-text");
|
||||
if (gndBadge && gndText) {
|
||||
if (boardConnected) {
|
||||
gndBadge.className = "badge badge-lg badge-success";
|
||||
gndText.textContent = "Connected";
|
||||
} else {
|
||||
gndBadge.className = "badge badge-lg badge-error";
|
||||
gndText.textContent = "Not Connected";
|
||||
}
|
||||
}
|
||||
// Also update the board connection status
|
||||
updateBoardConnectionStatus(boardConnected);
|
||||
}
|
||||
|
||||
// Toggle power relay
|
||||
function togglePowerRelay() {
|
||||
if ($testbenchStates.get().powerRelayEnabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_POWER_RELAY_OFF);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_POWER_RELAY_ON);
|
||||
}
|
||||
}
|
||||
|
||||
// Update UART routing display
|
||||
function updateUartRoutingDisplay() {
|
||||
const buttons = document.querySelectorAll('input[name="uart_routing"]');
|
||||
const currentState = $testbenchStates.get();
|
||||
|
||||
// Update button selection
|
||||
buttons.forEach((button) => {
|
||||
const inputButton = button as HTMLInputElement;
|
||||
if (inputButton) {
|
||||
const label = inputButton.getAttribute("aria-label")?.toLowerCase();
|
||||
if (label) {
|
||||
inputButton.checked = label === currentState.uartRouting;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set UART routing
|
||||
function setUartRouting(target: string) {
|
||||
if (target === "st") {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_ST);
|
||||
} else if (target === "c6") {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_C6);
|
||||
} else if (target === "p4") {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_P4);
|
||||
}
|
||||
console.log(`Clicked UART routing button: ${target}`);
|
||||
}
|
||||
|
||||
// Update bootloader status
|
||||
function updateBootloaderStatus(target: string, active: boolean) {
|
||||
const button = document.getElementById(`${target}-bootloader-btn`);
|
||||
if (!button) return;
|
||||
if (active) {
|
||||
button.textContent = `${target.toUpperCase()} Bootloader ON`;
|
||||
button.className = "btn btn-success btn-sm w-full";
|
||||
} else {
|
||||
button.textContent = `${target.toUpperCase()} Bootloader OFF`;
|
||||
button.className = "btn btn-error btn-sm w-full";
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle bootloader
|
||||
function toggleBootloader(target: string) {
|
||||
if (target === "p4") {
|
||||
if ($testbenchStates.get().p4BootloaderActive) {
|
||||
// Turn off P4 bootloader
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_BOOTLOADER_OFF);
|
||||
console.log("Turned off P4 bootloader");
|
||||
} else {
|
||||
// Turn on P4 bootloader, turn off C6 bootloader and enable P4
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_BOOTLOADER_ON);
|
||||
console.log("Turned on P4 bootloader");
|
||||
}
|
||||
} else if (target === "c6") {
|
||||
if ($testbenchStates.get().c6BootloaderActive) {
|
||||
// Turn off C6 bootloader
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_BOOTLOADER_OFF);
|
||||
console.log("Turned off C6 bootloader");
|
||||
} else {
|
||||
// Turn on C6 bootloader, turn off P4 bootloader and enable C6
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_BOOTLOADER_ON);
|
||||
console.log("Turned on C6 bootloader");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update enable status based on testbench states
|
||||
function updateEnableStatus(target: string, enabled: boolean) {
|
||||
const button = document.getElementById(`${target}-enable-btn`);
|
||||
if (!button) return;
|
||||
|
||||
// Get current testbench states
|
||||
const states = $testbenchStates.get();
|
||||
|
||||
// Check if target is actually enabled in the store
|
||||
const isActuallyEnabled = states[
|
||||
`${target}Enabled` as keyof typeof states
|
||||
] as boolean;
|
||||
|
||||
if (isActuallyEnabled) {
|
||||
button.textContent = `${target.toUpperCase()} Enabled`;
|
||||
button.className = "btn btn-success btn-sm w-full";
|
||||
} else {
|
||||
button.textContent = `${target.toUpperCase()} Disabled`;
|
||||
button.className = "btn btn-error btn-sm w-full";
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle enable
|
||||
function toggleEnable(target: string) {
|
||||
const currentState = $testbenchStates.get();
|
||||
switch (target) {
|
||||
case "p4":
|
||||
if (!currentState.c6BootloaderActive) {
|
||||
// Only allow if not in bootloader mode
|
||||
if (currentState.p4Enabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_DISABLED);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_ENABLED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "c6":
|
||||
if (!currentState.p4BootloaderActive) {
|
||||
// Only allow if not in bootloader mode
|
||||
if (currentState.c6Enabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_DISABLED);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_ENABLED);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "st":
|
||||
if (currentState.stEnabled) {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_ST_DISABLED);
|
||||
} else {
|
||||
sendCommandToBackend(TESTBENCH_COMMANDS.SET_ST_ENABLED);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update UI based on current state
|
||||
const state = $testbenchStates.get();
|
||||
updateEnableStatus("p4", state.p4Enabled);
|
||||
updateEnableStatus("c6", state.c6Enabled);
|
||||
updateEnableStatus("st", state.stEnabled);
|
||||
|
||||
console.log(
|
||||
`P4 Enable: ${state.p4Enabled}, C6 Enable: ${state.c6Enabled}, ST Enable: ${state.stEnabled}`
|
||||
);
|
||||
}
|
||||
|
||||
// Add message to console (global function)
|
||||
function addConsoleMessage(
|
||||
prefix: string,
|
||||
message: string,
|
||||
className = ""
|
||||
) {
|
||||
const uartConsole = document.getElementById("uart-console");
|
||||
if (uartConsole) {
|
||||
const pre = document.createElement("pre");
|
||||
pre.setAttribute("data-prefix", prefix);
|
||||
if (className) pre.className = className;
|
||||
|
||||
const code = document.createElement("code");
|
||||
code.textContent = message;
|
||||
pre.appendChild(code);
|
||||
|
||||
uartConsole.appendChild(pre);
|
||||
uartConsole.scrollTop = uartConsole.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// UART Console functionality
|
||||
const uartConsole = document.getElementById("uart-console");
|
||||
const uartInput = document.getElementById("uart-input");
|
||||
const sendButton = document.getElementById("send-uart");
|
||||
const clearButton = document.getElementById("clear-console");
|
||||
|
||||
// Number conversion functionality
|
||||
const decimalInput = document.getElementById("decimal-input");
|
||||
const hexOutput = document.getElementById("hex-output");
|
||||
const binaryOutput = document.getElementById("binary-output");
|
||||
const copyHexBtn = document.getElementById("copy-hex");
|
||||
const copyBinaryBtn = document.getElementById("copy-binary");
|
||||
const clearConverterBtn = document.getElementById("clear-converter");
|
||||
|
||||
// Send UART message
|
||||
function sendUartMessage() {
|
||||
const input = uartInput as HTMLInputElement;
|
||||
|
||||
if (!input.value.trim()) return;
|
||||
|
||||
const message = input.value.trim();
|
||||
|
||||
// Display sent message
|
||||
addConsoleMessage("TX", message, "text-warning");
|
||||
|
||||
// TODO: Send via WebSocket as text
|
||||
console.log("Sending UART text:", message);
|
||||
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
// Clear console
|
||||
function clearConsole() {
|
||||
if (uartConsole) {
|
||||
uartConsole.innerHTML =
|
||||
'<pre data-prefix=">" class="text-info"><code>Console cleared</code></pre>';
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for UART console
|
||||
if (sendButton) {
|
||||
sendButton.addEventListener("click", sendUartMessage);
|
||||
}
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", clearConsole);
|
||||
}
|
||||
|
||||
if (uartInput) {
|
||||
uartInput.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
sendUartMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners for number conversion
|
||||
if (decimalInput) {
|
||||
decimalInput.addEventListener("input", updateConversion);
|
||||
}
|
||||
|
||||
if (copyHexBtn) {
|
||||
copyHexBtn.addEventListener("click", () => {
|
||||
const hexOut = hexOutput as HTMLInputElement;
|
||||
if (hexOut.value) copyToClipboard(hexOut.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (copyBinaryBtn) {
|
||||
copyBinaryBtn.addEventListener("click", () => {
|
||||
const binOut = binaryOutput as HTMLInputElement;
|
||||
if (binOut.value) copyToClipboard(binOut.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (clearConverterBtn) {
|
||||
clearConverterBtn.addEventListener("click", clearConverter);
|
||||
}
|
||||
|
||||
// Number conversion functions
|
||||
function updateConversion() {
|
||||
const decInput = decimalInput as HTMLInputElement;
|
||||
const hexOut = hexOutput as HTMLInputElement;
|
||||
const binOut = binaryOutput as HTMLInputElement;
|
||||
|
||||
const decimal = parseInt(decInput.value);
|
||||
|
||||
if (isNaN(decimal) || decInput.value === "") {
|
||||
hexOut.value = "";
|
||||
binOut.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to hex (with 0x prefix)
|
||||
hexOut.value =
|
||||
"0x" + decimal.toString(16).toUpperCase().padStart(2, "0");
|
||||
|
||||
// Convert to binary (8-bit)
|
||||
binOut.value = decimal.toString(2).padStart(8, "0");
|
||||
}
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
console.log("Copied to clipboard:", text);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy:", err);
|
||||
});
|
||||
}
|
||||
|
||||
function clearConverter() {
|
||||
const decInput = decimalInput as HTMLInputElement;
|
||||
const hexOut = hexOutput as HTMLInputElement;
|
||||
const binOut = binaryOutput as HTMLInputElement;
|
||||
|
||||
decInput.value = "";
|
||||
hexOut.value = "";
|
||||
binOut.value = "";
|
||||
}
|
||||
|
||||
// Simulate incoming UART data (remove this when connecting to real WebSocket)
|
||||
setTimeout(() => {
|
||||
addConsoleMessage("RX", "Hello from device!", "text-success");
|
||||
}, 2000);
|
||||
|
||||
// UART routing buttons
|
||||
document
|
||||
.querySelectorAll('input[name="uart_routing"]')
|
||||
.forEach((tab) => {
|
||||
const inputTab = tab as HTMLInputElement;
|
||||
inputTab.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
const target = this.getAttribute("aria-label")?.toLowerCase();
|
||||
if (target) {
|
||||
setUartRouting(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const p4BootloaderBtn = document.getElementById("p4-bootloader-btn");
|
||||
const c6BootloaderBtn = document.getElementById("c6-bootloader-btn");
|
||||
const p4EnableBtn = document.getElementById("p4-enable-btn");
|
||||
const c6EnableBtn = document.getElementById("c6-enable-btn");
|
||||
const stEnableBtn = document.getElementById("st-enable-btn");
|
||||
const powerRelayToggle = document.getElementById("power-relay-toggle");
|
||||
|
||||
// Bootloader buttons
|
||||
if (p4BootloaderBtn) {
|
||||
p4BootloaderBtn.addEventListener("click", () =>
|
||||
toggleBootloader("p4")
|
||||
);
|
||||
}
|
||||
if (c6BootloaderBtn) {
|
||||
c6BootloaderBtn.addEventListener("click", () =>
|
||||
toggleBootloader("c6")
|
||||
);
|
||||
}
|
||||
|
||||
// Enable buttons
|
||||
if (p4EnableBtn) {
|
||||
p4EnableBtn.addEventListener("click", () => toggleEnable("p4"));
|
||||
}
|
||||
if (c6EnableBtn) {
|
||||
c6EnableBtn.addEventListener("click", () => toggleEnable("c6"));
|
||||
}
|
||||
if (stEnableBtn) {
|
||||
stEnableBtn.addEventListener("click", () => toggleEnable("st"));
|
||||
}
|
||||
|
||||
// Power relay toggle
|
||||
if (powerRelayToggle) {
|
||||
powerRelayToggle.addEventListener("change", togglePowerRelay);
|
||||
}
|
||||
|
||||
// Initialize display
|
||||
updateWebSocketStatus(false);
|
||||
updateVccDisplay(12);
|
||||
updateUartRoutingDisplay();
|
||||
updateBootloaderStatus("p4", false);
|
||||
updateBootloaderStatus("c6", false);
|
||||
updateEnableStatus("p4", false);
|
||||
updateEnableStatus("c6", false);
|
||||
updateEnableStatus("st", false);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Subscribe to testbench state changes for automatic UI updates
|
||||
$testbenchStates.subscribe((newStates) => {
|
||||
// Update enable status buttons based on store state
|
||||
updateEnableStatus("p4", newStates.p4Enabled);
|
||||
updateEnableStatus("c6", newStates.c6Enabled);
|
||||
updateEnableStatus("st", newStates.stEnabled);
|
||||
|
||||
// Update bootloader status
|
||||
updateBootloaderStatus("p4", newStates.p4BootloaderActive);
|
||||
updateBootloaderStatus("c6", newStates.c6BootloaderActive);
|
||||
|
||||
// Update UART routing
|
||||
currentUartRouting = newStates.uartRouting;
|
||||
updateUartRoutingDisplay();
|
||||
|
||||
// Update power relay
|
||||
powerRelayEnabled = newStates.powerRelayEnabled;
|
||||
const toggle = document.getElementById(
|
||||
"power-relay-toggle"
|
||||
) as HTMLInputElement;
|
||||
if (toggle) {
|
||||
toggle.checked = powerRelayEnabled;
|
||||
}
|
||||
|
||||
updateGndSenseStatus(newStates.boardConnected);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import { atom } from 'nanostores';
|
||||
import pkg from "websocket";
|
||||
const { w3cwebsocket } = pkg;
|
||||
|
||||
export const $testbenchWebsocket = atom<WebSocket | null>(null);
|
||||
export const $testbenchWebsocketConnectionStatus = atom<string>('Disconnected');
|
||||
|
||||
|
||||
export function setTestbenchWebsocket(websocket: WebSocket) {
|
||||
$testbenchWebsocket.set(websocket);
|
||||
}
|
||||
|
||||
export function setTestbenchWebsocketConnectionStatus(status: string) {
|
||||
$testbenchWebsocketConnectionStatus.set(status);
|
||||
}
|
||||
|
||||
export const TESTBENCH_COMMANDS = {
|
||||
SET_UART_ROUTING_ST: 0,
|
||||
SET_UART_ROUTING_C6: 1,
|
||||
SET_UART_ROUTING_P4: 2,
|
||||
SET_P4_BOOTLOADER_ON: 3,
|
||||
SET_P4_BOOTLOADER_OFF: 4,
|
||||
SET_C6_BOOTLOADER_ON: 5,
|
||||
SET_C6_BOOTLOADER_OFF: 6,
|
||||
SET_P4_ENABLED: 7,
|
||||
SET_P4_DISABLED: 8,
|
||||
SET_C6_ENABLED: 9,
|
||||
SET_C6_DISABLED: 10,
|
||||
SET_ST_ENABLED: 11,
|
||||
SET_ST_DISABLED: 12,
|
||||
SET_POWER_RELAY_ON: 13,
|
||||
SET_POWER_RELAY_OFF: 14,
|
||||
};
|
||||
|
||||
|
||||
// GPIO Pin Mapping Constants
|
||||
export const GPIO_PINS = {
|
||||
// Communication
|
||||
SDA: 1,
|
||||
SCL: 2,
|
||||
DTR: 3,
|
||||
RTS: 14,
|
||||
FTDI_UART_TX: 17,
|
||||
FTDI_UART_RX: 18,
|
||||
|
||||
// Switches
|
||||
SW_A: 4,
|
||||
SW_B: 5,
|
||||
SW_C: 6,
|
||||
SW_D: 7,
|
||||
// Route switches
|
||||
SW_ST_C6: 12,
|
||||
SW_C6_P4: 13,
|
||||
|
||||
// Power & Sensing
|
||||
BOARD_POWER: 15,
|
||||
GND_SENSE: 16,
|
||||
|
||||
// P4 Bootloader Pins
|
||||
P4_35: 47,
|
||||
P4_36: 21,
|
||||
P4_ENABLE: 9,
|
||||
|
||||
// STM32
|
||||
STM32_RESET: 38,
|
||||
|
||||
// C6 Control
|
||||
C6_ENABLE: 10,
|
||||
C6_BOOT: 8,
|
||||
} as const;
|
||||
|
||||
// GPIO State Store
|
||||
export const $gpioStates = atom<Record<number, boolean>>({
|
||||
[GPIO_PINS.SW_A]: false,
|
||||
[GPIO_PINS.SW_B]: false,
|
||||
[GPIO_PINS.SW_C]: false,
|
||||
[GPIO_PINS.SW_D]: false,
|
||||
[GPIO_PINS.BOARD_POWER]: false,
|
||||
[GPIO_PINS.GND_SENSE]: true,
|
||||
[GPIO_PINS.P4_35]: true,
|
||||
[GPIO_PINS.P4_36]: false,
|
||||
[GPIO_PINS.STM32_RESET]: false,
|
||||
[GPIO_PINS.C6_ENABLE]: false,
|
||||
[GPIO_PINS.C6_BOOT]: true,
|
||||
});
|
||||
|
||||
// Testbench State Type
|
||||
export type TestbenchState = {
|
||||
boardConnected: boolean;
|
||||
vccVoltage: number;
|
||||
powerRelayEnabled: boolean;
|
||||
uartRouting: 'st' | 'p4' | 'c6';
|
||||
p4BootloaderActive: boolean;
|
||||
c6BootloaderActive: boolean;
|
||||
p4Enabled: boolean;
|
||||
c6Enabled: boolean;
|
||||
stEnabled: boolean;
|
||||
gndSense: boolean;
|
||||
};
|
||||
|
||||
// Testbench Control States
|
||||
export const $testbenchStates = atom<TestbenchState>({
|
||||
// Board Connection
|
||||
boardConnected: false,
|
||||
vccVoltage: 12,
|
||||
|
||||
// Power Control
|
||||
powerRelayEnabled: false,
|
||||
|
||||
// UART Routing
|
||||
uartRouting: 'p4',
|
||||
|
||||
// Bootloader States
|
||||
p4BootloaderActive: false,
|
||||
c6BootloaderActive: false,
|
||||
|
||||
// Enable States
|
||||
p4Enabled: false,
|
||||
c6Enabled: false,
|
||||
stEnabled: false,
|
||||
gndSense: true,
|
||||
});
|
||||
|
||||
export function getGpioState(pin: number): boolean {
|
||||
return $gpioStates.get()[pin] || false;
|
||||
}
|
||||
|
||||
export function sendCommandToBackend(command: number) {
|
||||
const websocket = $testbenchWebsocket.get();
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
message_type: "testbench_command",
|
||||
command: command
|
||||
};
|
||||
|
||||
try {
|
||||
websocket.send(JSON.stringify(message));
|
||||
console.log("Testbench command sent:", command);
|
||||
} catch (error) {
|
||||
console.error("Failed to send Testbench command:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import { useEffect } from 'preact/hooks';
|
||||
import { $gpioStates, $testbenchWebsocket, $testbenchWebsocketConnectionStatus, GPIO_PINS,
|
||||
setTestbenchWebsocketConnectionStatus, $testbenchStates } from './TestbenchStore';
|
||||
|
||||
// Heartbeat configuration
|
||||
const HEARTBEAT_INTERVAL = 1500; // Send ping every 1 second
|
||||
const HEARTBEAT_TIMEOUT = 700; // Wait 1 second for pong response
|
||||
const MAX_MISSED_HEARTBEATS = 5; // Close connection after 5 missed pongs
|
||||
|
||||
// Heartbeat state
|
||||
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let missedHeartbeats = 0;
|
||||
|
||||
// Reconnect state
|
||||
let isReconnecting = false;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Restart detection state
|
||||
let disconnectDetected = false;
|
||||
|
||||
|
||||
function onMessage(message: MessageEvent) {
|
||||
//console.log("WebSocket message received:", message.data.toString());
|
||||
// Verarbeite JSON Nachrichten
|
||||
const dataFromServer = JSON.parse(message.data.toString());
|
||||
if (dataFromServer.message_type === "pong") {
|
||||
// reset missed heartbeats and clear pending timeout
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
console.log("Heartbeat pong received");
|
||||
}
|
||||
else if (dataFromServer.message_type === "initial_testbench_state_response" || dataFromServer.message_type === "testbench_state_response") {
|
||||
console.log("Testbench state response received");
|
||||
console.log("Testbench state:", dataFromServer);
|
||||
if (dataFromServer.gpio_states) {
|
||||
|
||||
$gpioStates.set({
|
||||
[GPIO_PINS.SW_A]: dataFromServer.gpio_states["4"],
|
||||
[GPIO_PINS.SW_B]: dataFromServer.gpio_states["5"],
|
||||
[GPIO_PINS.SW_C]: dataFromServer.gpio_states["6"],
|
||||
[GPIO_PINS.SW_D]: dataFromServer.gpio_states["7"],
|
||||
[GPIO_PINS.BOARD_POWER]: dataFromServer.gpio_states["15"],
|
||||
[GPIO_PINS.GND_SENSE]: dataFromServer.gpio_states["16"],
|
||||
[GPIO_PINS.P4_35]: dataFromServer.gpio_states["47"],
|
||||
[GPIO_PINS.P4_36]: dataFromServer.gpio_states["21"],
|
||||
[GPIO_PINS.STM32_RESET]: dataFromServer.gpio_states["38"],
|
||||
[GPIO_PINS.C6_ENABLE]: dataFromServer.gpio_states["10"],
|
||||
[GPIO_PINS.C6_BOOT]: dataFromServer.gpio_states["8"],
|
||||
});
|
||||
$testbenchStates.set({
|
||||
boardConnected: dataFromServer.testbenchStates.boardConnected,
|
||||
vccVoltage: dataFromServer.testbenchStates.vccVoltage,
|
||||
powerRelayEnabled: dataFromServer.testbenchStates.powerRelayEnabled,
|
||||
uartRouting: dataFromServer.testbenchStates.uartRouting,
|
||||
p4BootloaderActive: dataFromServer.testbenchStates.p4BootloaderActive,
|
||||
c6BootloaderActive: dataFromServer.testbenchStates.c6BootloaderActive,
|
||||
p4Enabled: dataFromServer.testbenchStates.p4Enabled,
|
||||
c6Enabled: dataFromServer.testbenchStates.c6Enabled,
|
||||
stEnabled: dataFromServer.testbenchStates.stEnabled,
|
||||
gndSense: dataFromServer.testbenchStates.gndSense,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const connectWebSocket = () => {
|
||||
// Clear any existing reconnect attempts
|
||||
clearReconnect();
|
||||
|
||||
const testbenchWSaddress = "ws://testbench.local/ws";
|
||||
console.log(`Verbinde mit: ${testbenchWSaddress}`);
|
||||
if ($testbenchWebsocket.get() !== null) {
|
||||
($testbenchWebsocket.get() as WebSocket).close(1000);
|
||||
$testbenchWebsocket.set(null);
|
||||
}
|
||||
$testbenchWebsocket.set(new WebSocket(testbenchWSaddress));
|
||||
if ($testbenchWebsocket.get() !== null) {
|
||||
($testbenchWebsocket.get() as WebSocket).onopen = onOpen;
|
||||
|
||||
($testbenchWebsocket.get() as WebSocket).onmessage = onMessage;
|
||||
|
||||
($testbenchWebsocket.get() as WebSocket).onerror = onError;
|
||||
|
||||
($testbenchWebsocket.get() as WebSocket).onclose = onClose;
|
||||
}
|
||||
}
|
||||
|
||||
function clearReconnect() {
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
isReconnecting = false;
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
setTestbenchWebsocketConnectionStatus("Connected");
|
||||
console.log("WebSocket erfolgreich verbunden");
|
||||
missedHeartbeats = 0;
|
||||
|
||||
// Start heartbeat mechanism
|
||||
startHeartbeat();
|
||||
// Sende initiale Statusanfrage
|
||||
if ($testbenchWebsocket.get() !== null) {
|
||||
const initial_state_request = JSON.stringify({ message_type: "initial_testbench_state_request" });
|
||||
try {
|
||||
($testbenchWebsocket.get() as WebSocket).send(initial_state_request);
|
||||
} catch (error) {
|
||||
console.error("Failed to send initial testbench state request:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClose(event: CloseEvent) {
|
||||
$testbenchWebsocketConnectionStatus.set("Disconnected");
|
||||
console.log("Testbench WebSocket closed with code:", event.code);
|
||||
console.log("Testbench WebSocket close reason:", event.reason);
|
||||
console.log("Testbench WebSocket clean close:", event.wasClean);
|
||||
clearHeartbeat();
|
||||
if (event.code === 1000) {
|
||||
console.log("Normal closure");
|
||||
}
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
clearHeartbeat(); // Clear any existing heartbeat
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
sendHeartbeat();
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
function clearHeartbeat() {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
}
|
||||
|
||||
|
||||
function sendHeartbeat() {
|
||||
if ($testbenchWebsocket.get() && ($testbenchWebsocket.get() as WebSocket).readyState === ($testbenchWebsocket.get() as WebSocket).OPEN) {
|
||||
// Send ping message
|
||||
const pingMessage = JSON.stringify({ message_type: "ping" });
|
||||
try {
|
||||
($testbenchWebsocket.get() as WebSocket).send(pingMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to send heartbeat ping:", error);
|
||||
missedHeartbeats++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout for pong response
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
missedHeartbeats++;
|
||||
if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
|
||||
console.error("Too many missed heartbeats, closing connection");
|
||||
|
||||
// Wichtig: Erst Heartbeat stoppen
|
||||
clearHeartbeat();
|
||||
|
||||
// Dann WebSocket schließen und null setzen
|
||||
|
||||
if ($testbenchWebsocket.get()) {
|
||||
($testbenchWebsocket.get() as WebSocket).close();
|
||||
setTestbenchWebsocketConnectionStatus("Disconnected");
|
||||
$testbenchWebsocket.set(null);
|
||||
}
|
||||
}
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error: Event) {
|
||||
$testbenchWebsocketConnectionStatus.set("Disconnected");
|
||||
console.log("WebSocket error:", error);
|
||||
clearHeartbeat();
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (isReconnecting) {
|
||||
console.log("Reconnect already scheduled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
isReconnecting = true;
|
||||
console.log("Scheduling reconnect in 1 second");
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
isReconnecting = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export const TestbenchWebSocketProvider = () => {
|
||||
|
||||
// Auto-connect WebSocket at first render
|
||||
useEffect(() => {
|
||||
console.log("Component mounted, starting WebSocket connection...");
|
||||
connectWebSocket();
|
||||
// Cleanup beim Unmounting
|
||||
return () => {
|
||||
clearHeartbeat();
|
||||
clearReconnect();
|
||||
($testbenchWebsocket.get() as WebSocket).close();
|
||||
$testbenchWebsocket.set(null);
|
||||
$testbenchWebsocketConnectionStatus.set("Disconnected");
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs } from '@tabler/icons-preact';
|
||||
import {
|
||||
$availableUpdates,
|
||||
$updateState,
|
||||
$updateErrorMessages,
|
||||
$p4FirmwareProgress,
|
||||
$p4WebinterfaceProgress,
|
||||
$stm32FirmwareProgress,
|
||||
$c6FirmwareProgress,
|
||||
stateSetters,
|
||||
start_firmware_update_p4_fw,
|
||||
start_firmware_update_p4_web,
|
||||
start_firmware_update_stm32_fw,
|
||||
start_firmware_update_c6_fw,
|
||||
type UpdateInfo,
|
||||
request_available_updates
|
||||
} from './AppProviderStore';
|
||||
|
||||
interface UpdateErrorPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Function to get the appropriate icon for each update type
|
||||
const getUpdateIcon = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
return (
|
||||
<IconCpu2 strokeWidth={2} />
|
||||
);
|
||||
case 'p4 firmware':
|
||||
return (
|
||||
<IconCpu strokeWidth={2} />
|
||||
);
|
||||
case 'c6':
|
||||
return (
|
||||
<IconWifi strokeWidth={2} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<IconFileTypeJs strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get progress for a specific update type
|
||||
const getProgressForUpdate = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 firmware': return $p4FirmwareProgress.get();
|
||||
case 'p4 webinterface': return $p4WebinterfaceProgress.get();
|
||||
case 'stm32': return $stm32FirmwareProgress.get();
|
||||
case 'c6': return $c6FirmwareProgress.get();
|
||||
default: return [0, 0];
|
||||
}
|
||||
};
|
||||
|
||||
// Map core types to their target indices
|
||||
const getTargetIndex = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 webinterface': return 0; // P4 webinterface (first)
|
||||
case 'p4 firmware': return 1; // P4 firmware (second)
|
||||
case 'stm32': return 2; // STM32 firmware
|
||||
case 'c6': return 3; // C6 firmware (last)
|
||||
default: return -1;
|
||||
}
|
||||
};
|
||||
|
||||
export default function UpdateErrorPopup({ isOpen, onClose }: UpdateErrorPopupProps) {
|
||||
const availableUpdates = useStore($availableUpdates);
|
||||
const updateState = useStore($updateState);
|
||||
const updateErrorMessages = useStore($updateErrorMessages);
|
||||
|
||||
// Only show if update state is error
|
||||
if (!isOpen || updateState !== 'error') return null;
|
||||
|
||||
const handleClose = () => {
|
||||
// Don't reset update state here
|
||||
onClose();
|
||||
// reset update status:
|
||||
stateSetters.resetUpdateStatus();
|
||||
// query available updates again!
|
||||
request_available_updates();
|
||||
};
|
||||
|
||||
const retryUpdate = () => {
|
||||
// Reset state and start update again
|
||||
stateSetters.setUpdateState('updating');
|
||||
stateSetters.setUpdateErrorMessages({});
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
stateSetters.setCurrentUpdateTargetIndex(0);
|
||||
stateSetters.setP4FirmwareProgress(0, 0);
|
||||
stateSetters.setP4WebinterfaceProgress(0, 0);
|
||||
stateSetters.setStm32FirmwareProgress(0, 0);
|
||||
stateSetters.setC6FirmwareProgress(0, 0);
|
||||
|
||||
// Start the update process again
|
||||
const updatesToProcess = availableUpdates.filter(update => update.updateAvailable);
|
||||
if (updatesToProcess.length > 0) {
|
||||
const firstUpdate = updatesToProcess[0];
|
||||
switch (firstUpdate.core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
start_firmware_update_stm32_fw();
|
||||
break;
|
||||
case 'p4 webinterface':
|
||||
start_firmware_update_p4_web();
|
||||
break;
|
||||
case 'p4 firmware':
|
||||
start_firmware_update_p4_fw();
|
||||
break;
|
||||
case 'c6':
|
||||
start_firmware_update_c6_fw();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// close the popup
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-alert-circle text-error mb-4">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 8v4" />
|
||||
<path d="M12 16h.01" />
|
||||
</svg>
|
||||
<h3 class="font-bold text-lg text-error mb-2">Update Failed</h3>
|
||||
<p class="text-center mb-6">One or more firmware updates failed to complete successfully.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
{availableUpdates.map((update, index) => {
|
||||
const [currentProgress, status] = getProgressForUpdate(update.core);
|
||||
const updateTargetIndex = getTargetIndex(update.core);
|
||||
const errorMessage = updateErrorMessages[updateTargetIndex] || '';
|
||||
const isFailed = status === -1;
|
||||
|
||||
return (
|
||||
<div key={index} class={`flex items-center justify-between p-3 rounded-lg ${isFailed ? 'bg-error/10' : 'bg-success/10'}`}>
|
||||
<div class="flex items-center gap-3">
|
||||
{getUpdateIcon(update.core)}
|
||||
<div class="flex-1">
|
||||
<span class="font-medium">{update.core}</span>
|
||||
{isFailed && errorMessage && (
|
||||
<div class="text-sm text-error mt-1">{errorMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{isFailed ? (
|
||||
<>
|
||||
<span class="text-sm text-error">Failed</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x text-error">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M18 6l-12 12" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span class="text-sm">v{update.newVersion}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-outline" onClick={handleClose}>Close</button>
|
||||
<button class="btn btn-primary" onClick={retryUpdate}>Retry Update</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={handleClose}></div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
import { useStore } from '@nanostores/preact';
|
||||
import { useEffect, useState, useRef } from 'preact/hooks';
|
||||
import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs, IconRefresh } from '@tabler/icons-preact';
|
||||
import {
|
||||
$availableUpdates,
|
||||
$updateState,
|
||||
stateSetters,
|
||||
type UpdateInfo
|
||||
} from './AppProviderStore';
|
||||
|
||||
interface UpdateSuccessPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Function to get the appropriate icon for each update type
|
||||
const getUpdateIcon = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
return (
|
||||
<IconCpu2 strokeWidth={2} />
|
||||
);
|
||||
case 'p4 firmware':
|
||||
return (
|
||||
<IconCpu strokeWidth={2} />
|
||||
);
|
||||
case 'c6':
|
||||
return (
|
||||
<IconWifi strokeWidth={2} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<IconFileTypeJs strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function UpdateSuccessPopup({ isOpen, onClose }: UpdateSuccessPopupProps) {
|
||||
const availableUpdates = useStore($availableUpdates);
|
||||
const updateState = useStore($updateState);
|
||||
const [showRestarting, setShowRestarting] = useState(false);
|
||||
|
||||
// Only show if explicitly opened via isOpen prop (don't auto-close based on updateState)
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Handle restarting state - just show animation, no timer
|
||||
useEffect(() => {
|
||||
console.log("🎉 UpdateSuccessPopup useEffect triggered:", {
|
||||
updateState,
|
||||
isOpen,
|
||||
showRestarting
|
||||
});
|
||||
|
||||
if (updateState === 'restarting') {
|
||||
console.log("🔄 RESTARTING state detected - showing restart animation");
|
||||
setShowRestarting(true);
|
||||
} else {
|
||||
console.log("🎉 Non-restarting state - hiding restart animation");
|
||||
setShowRestarting(false);
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
const handleClose = () => {
|
||||
// Don't reset update state here if we're restarting
|
||||
if (updateState !== 'restarting') {
|
||||
onClose();
|
||||
}
|
||||
// reset update status:
|
||||
stateSetters.resetUpdateStatus();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<div class="flex flex-col items-center justify-center py-8">
|
||||
{/* Always show success checkmark first */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success mb-4">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
<h3 class="font-bold text-lg text-success mb-2">Update Completed Successfully!</h3>
|
||||
<p class="text-center mb-6">All listed components have been updated successfully.</p>
|
||||
|
||||
{/* Show restarting section below if in restarting state */}
|
||||
{updateState === 'restarting' && (
|
||||
<div class="border-t border-base-300 pt-6 mt-2 w-full">
|
||||
<div class="flex flex-col items-center">
|
||||
<IconRefresh
|
||||
size={48}
|
||||
class="text-primary animate-spin mb-3"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<h4 class="font-semibold text-md text-primary mb-2">Restarting Device...</h4>
|
||||
<p class="text-center text-sm text-base-content/70">
|
||||
The device is restarting to apply the firmware update. Please wait for reconnection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show update list in both success and restarting state */}
|
||||
{(updateState === 'success' || updateState === 'restarting') && (
|
||||
<div class="space-y-2 mb-6">
|
||||
{availableUpdates.map((update, index) => (
|
||||
<div key={index} class="flex items-center justify-between p-3 bg-success/10 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
{getUpdateIcon(update.core)}
|
||||
<span class="font-medium">{update.core}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm">v{update.newVersion}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Button only in success state */}
|
||||
{updateState === 'success' && (
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-primary" onClick={handleClose}>OK</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="modal-backdrop" onClick={handleClose}></div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,472 +0,0 @@
|
|||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { marked } from 'marked';
|
||||
import { useStore } from '@nanostores/preact';
|
||||
import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs } from '@tabler/icons-preact';
|
||||
import {
|
||||
$availableUpdates,
|
||||
$updateChangelog,
|
||||
$updateInProgress,
|
||||
$p4FirmwareProgress,
|
||||
$p4WebinterfaceProgress,
|
||||
$stm32FirmwareProgress,
|
||||
$c6FirmwareProgress,
|
||||
$currentUpdateTargetIndex,
|
||||
$updateErrorMessages,
|
||||
$updateState,
|
||||
$updatedTargets,
|
||||
stateSetters,
|
||||
request_update_progress,
|
||||
start_firmware_update_p4_fw,
|
||||
start_firmware_update_p4_web,
|
||||
start_firmware_update_stm32_fw,
|
||||
start_firmware_update_c6_fw,
|
||||
type UpdateInfo,
|
||||
UpdateStatus,
|
||||
TargetUpdateStatus
|
||||
} from './AppProviderStore';
|
||||
|
||||
// Configure marked with basic options
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
});
|
||||
|
||||
interface UpdaterModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Function to check if any updates are available
|
||||
const hasAvailableUpdates = (updates: UpdateInfo[]) => updates.some(update => update.updateAvailable);
|
||||
|
||||
// Function to get the appropriate icon for each update type
|
||||
const getUpdateIcon = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
return (
|
||||
<IconCpu2 strokeWidth={2} />
|
||||
);
|
||||
case 'p4 firmware':
|
||||
return (
|
||||
<IconCpu strokeWidth={2} />
|
||||
);
|
||||
case 'c6':
|
||||
return (
|
||||
<IconWifi strokeWidth={2} />
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<IconFileTypeJs strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function UpdaterModal({ isOpen, onClose }: UpdaterModalProps) {
|
||||
const updateState = useStore($updateState);
|
||||
const updateInProgress = useStore($updateInProgress);
|
||||
const [currentUpdateIndex, setCurrentUpdateIndex] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
// Auto-close modal when updates are completed or failed
|
||||
useEffect(() => {
|
||||
console.log("🚀 UpdaterModal useEffect triggered:", {
|
||||
updateState,
|
||||
isOpen
|
||||
});
|
||||
|
||||
if (updateState === 'success' || updateState === 'error' || updateState === 'restarting') {
|
||||
if (isOpen) {
|
||||
console.log(`🚪 Closing updater modal due to state: '${updateState}'`);
|
||||
onClose();
|
||||
} else {
|
||||
console.log(`🚪 Updater modal already closed for state: '${updateState}'`);
|
||||
}
|
||||
} else {
|
||||
console.log("🚀 Updater modal stays open - state:", updateState);
|
||||
}
|
||||
}, [updateState, onClose]); // Removed updateInProgress to prevent loop
|
||||
|
||||
// Use useStore for reactive updates
|
||||
const availableUpdates = useStore($availableUpdates);
|
||||
const updateChangelog = useStore($updateChangelog);
|
||||
const currentUpdateTargetIndex = useStore($currentUpdateTargetIndex);
|
||||
const updateErrorMessages = useStore($updateErrorMessages);
|
||||
|
||||
// Enhanced: Use updated_targets from server response via global state
|
||||
const updatedTargetsFromServer = useStore($updatedTargets);
|
||||
|
||||
// Use reactive stores for all progress values
|
||||
const p4FirmwareProgress = useStore($p4FirmwareProgress);
|
||||
const p4WebinterfaceProgress = useStore($p4WebinterfaceProgress);
|
||||
const stm32FirmwareProgress = useStore($stm32FirmwareProgress);
|
||||
const c6FirmwareProgress = useStore($c6FirmwareProgress);
|
||||
|
||||
// Helper function to get progress for a specific update type - now using reactive stores
|
||||
const getProgressForUpdate = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 firmware': return p4FirmwareProgress;
|
||||
case 'p4 webinterface': return p4WebinterfaceProgress;
|
||||
case 'stm32': return stm32FirmwareProgress;
|
||||
case 'c6': return c6FirmwareProgress;
|
||||
default: return [0, 0];
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced: Check if a target is completed using updated_targets array from server
|
||||
const isTargetCompletedFromServer = (core: string) => {
|
||||
const targetIndex = getTargetIndex(core);
|
||||
return targetIndex >= 0 && targetIndex < updatedTargetsFromServer.length
|
||||
? updatedTargetsFromServer[targetIndex]
|
||||
: false;
|
||||
};
|
||||
|
||||
// Map core types to their target indices
|
||||
const getTargetIndex = (core: string) => {
|
||||
const coreLower = core.toLowerCase();
|
||||
console.log(`🔍 getTargetIndex called with: "${core}" -> "${coreLower}"`);
|
||||
|
||||
switch (coreLower) {
|
||||
case 'p4 webinterface':
|
||||
console.log(`✅ P4 webinterface -> index 1`);
|
||||
return 1; // P4 webinterface (second)
|
||||
case 'p4 firmware':
|
||||
console.log(`✅ P4 firmware -> index 2`);
|
||||
return 2; // P4 firmware (third)
|
||||
case 'stm32':
|
||||
console.log(`✅ STM32 -> index 0`);
|
||||
return 0; // STM32 firmware (first)
|
||||
case 'c6':
|
||||
console.log(`✅ C6 -> index 3`);
|
||||
return 3; // C6 firmware (last)
|
||||
default:
|
||||
console.warn(`❌ Unknown core type: "${core}" -> "${coreLower}" -> index -1`);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
const startUpdate = async () => {
|
||||
stateSetters.setUpdateState('updating');
|
||||
|
||||
const updatesToProcess = availableUpdates.filter(update => update.updateAvailable);
|
||||
|
||||
for (let i = 0; i < updatesToProcess.length; i++) {
|
||||
setCurrentUpdateIndex(i);
|
||||
const currentUpdate = updatesToProcess[i];
|
||||
|
||||
const updateTargetIndex = getTargetIndex(currentUpdate.core);
|
||||
|
||||
// Start the appropriate update based on the core type
|
||||
switch (currentUpdate.core.toLowerCase()) {
|
||||
case 'stm32':
|
||||
start_firmware_update_stm32_fw();
|
||||
break;
|
||||
case 'p4 webinterface':
|
||||
start_firmware_update_p4_web();
|
||||
break;
|
||||
case 'p4 firmware':
|
||||
start_firmware_update_p4_fw();
|
||||
break;
|
||||
case 'c6':
|
||||
start_firmware_update_c6_fw();
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown update type: ${currentUpdate.core}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for the update to complete by monitoring the progress
|
||||
if (currentUpdate.hasProgressBar) {
|
||||
// For updates with progress bar, wait for completion
|
||||
let updateCompleted = false;
|
||||
let updateFailed = false;
|
||||
|
||||
while (!updateCompleted && !updateFailed) {
|
||||
// Get progress for this specific update type - need to use .get() here for current values in async function
|
||||
const [currentProgress, status] = (() => {
|
||||
switch (currentUpdate.core.toLowerCase()) {
|
||||
case 'p4 firmware': return $p4FirmwareProgress.get();
|
||||
case 'p4 webinterface': return $p4WebinterfaceProgress.get();
|
||||
case 'stm32': return $stm32FirmwareProgress.get();
|
||||
case 'c6': return $c6FirmwareProgress.get();
|
||||
default: return [0, 0];
|
||||
}
|
||||
})();
|
||||
|
||||
setProgress(currentProgress);
|
||||
// Check error status first
|
||||
if (status === UpdateStatus.ERROR) {
|
||||
updateFailed = true;
|
||||
console.error(`Update failed for ${currentUpdate.core}`);
|
||||
stateSetters.setUpdateState('error');
|
||||
return;
|
||||
} else if (status === UpdateStatus.SUCCESS) {
|
||||
updateCompleted = true;
|
||||
break;
|
||||
}
|
||||
// Fallback checks
|
||||
if (currentProgress === 100) {
|
||||
updateCompleted = true;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
} else {
|
||||
// For updates without progress bar, show indeterminate progress for 3 seconds
|
||||
setProgress(0);
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
// Only show success if we reached here (all updates completed)
|
||||
stateSetters.setUpdateState('success');
|
||||
|
||||
// Clear all error messages on successful completion
|
||||
stateSetters.setUpdateErrorMessages({});
|
||||
};
|
||||
|
||||
const resetModal = () => {
|
||||
stateSetters.setUpdateState('initial');
|
||||
setCurrentUpdateIndex(0);
|
||||
setProgress(0);
|
||||
|
||||
stateSetters.setUpdateErrorMessages({});
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
stateSetters.setCurrentUpdateTargetIndex(0);
|
||||
stateSetters.setP4FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setP4WebinterfaceProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setStm32FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setC6FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
|
||||
stateSetters.setUpdaterModalOpen(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-4xl">
|
||||
{updateState === 'initial' && (
|
||||
<>
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-mobiledata">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M16 12v-8" />
|
||||
<path d="M8 20v-8" />
|
||||
<path d="M13 7l3 -3l3 3" />
|
||||
<path d="M5 17l3 3l3 -3" />
|
||||
</svg>
|
||||
Over The Air Update Available
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Available Updates */}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Available Updates:</h4>
|
||||
<div class="space-y-3">
|
||||
{availableUpdates
|
||||
.filter(update => update.updateAvailable)
|
||||
.sort((a, b) => {
|
||||
const indexA = getTargetIndex(a.core);
|
||||
const indexB = getTargetIndex(b.core);
|
||||
console.log(`🔄 Sorting: "${a.core}" (${indexA}) vs "${b.core}" (${indexB}) -> ${indexA - indexB}`);
|
||||
return indexA - indexB;
|
||||
})
|
||||
.map((update, index) => (
|
||||
<div key={index} class="flex items-center justify-between p-3 bg-base-200 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
{getUpdateIcon(update.core)}
|
||||
<div>
|
||||
<div class="font-medium">{update.core}</div>
|
||||
<div class="text-sm opacity-70">
|
||||
{update.currentVersion} → {update.newVersion}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="badge badge-success">Update</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changelog */}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-exchange">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19 3a3 3 0 0 1 1 5.83v4.17a6 6 0 0 1 -6 6h-.585l1.292 1.293a1 1 0 0 1 .083 1.32l-.083 .094a1 1 0 0 1 -1.414 0l-2.959 -2.959a1 1 0 0 1 -.238 -.32l-.038 -.091l-.028 -.094a.98 .98 0 0 1 .187 -.866l.076 -.084l3 -3a1 1 0 0 1 1.414 1.414l-1.293 1.293h.586a4 4 0 0 0 3.995 -3.8l.005 -.2v-4.171a3 3 0 0 1 -2 -2.829l.005 -.176a3 3 0 0 1 2.995 -2.824m-8.293 -.707l3 3a.98 .98 0 0 1 .263 .95l-.01 .031l-.003 .018l-.008 .018l-.007 .027l-.016 .035l-.01 .032l-.007 .01l-.005 .014a1 1 0 0 1 -.232 .316l-2.965 2.963a1 1 0 0 1 -1.32 .083l-.094 -.083a1 1 0 0 1 0 -1.414l1.291 -1.293h-.584a4 4 0 0 0 -3.995 3.8l-.005 .2v4.171a3.001 3.001 0 1 1 -4 2.829l.005 -.176a3 3 0 0 1 1.995 -2.654v-4.17a6 6 0 0 1 6 -6h.585l-1.292 -1.293a1 1 0 0 1 -.083 -1.32l.083 -.094a1 1 0 0 1 1.414 0" />
|
||||
</svg>
|
||||
Changelog
|
||||
</h4>
|
||||
<div class="bg-base-200 rounded-lg p-4 h-80 overflow-y-auto">
|
||||
{updateChangelog ? (
|
||||
<div class="prose prose-sm max-w-none [&>h2]:text-xl [&>h2]:font-semibold [&>h2]:mb-3 [&>h2]:mt-4 [&>h3]:text-lg [&>h3]:font-medium [&>h3]:mb-2 [&>h3]:mt-3 [&>ul]:list-disc [&>ul]:ml-6 [&>ul]:mb-3 [&>li]:mb-1 [&>p]:mb-3 [&>strong]:font-semibold"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(updateChangelog) as string }} />
|
||||
) : (
|
||||
<div class="text-center text-gray-500">
|
||||
<p>No changelog available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!hasAvailableUpdates(availableUpdates) && (
|
||||
<div class="alert alert-info mt-4">
|
||||
<span>No updates available.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="alert alert-warning mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>The device will restart automatically after the update. This process takes approximately 2-3 minutes.</span>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-outline" onClick={resetModal}>Cancel</button>
|
||||
<button class="btn btn-primary" onClick={startUpdate} disabled={!hasAvailableUpdates(availableUpdates)}>Start Update</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{updateState === 'updating' && (
|
||||
<>
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-mobiledata">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M16 12v-8" />
|
||||
<path d="M8 20v-8" />
|
||||
<path d="M13 7l3 -3l3 3" />
|
||||
<path d="M5 17l3 3l3 -3" />
|
||||
</svg>
|
||||
Updating Over The Air...
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
{availableUpdates
|
||||
.filter(update => update.updateAvailable)
|
||||
.sort((a, b) => {
|
||||
const indexA = getTargetIndex(a.core);
|
||||
const indexB = getTargetIndex(b.core);
|
||||
console.log(`🔄 Sorting (updating): "${a.core}" (${indexA}) vs "${b.core}" (${indexB}) -> ${indexA - indexB}`);
|
||||
return indexA - indexB;
|
||||
})
|
||||
.map((update, index) => {
|
||||
// Enhanced: Get real progress and status for each target using updated_targets
|
||||
const [currentProgress, status] = getProgressForUpdate(update.core);
|
||||
const targetIndex = getTargetIndex(update.core);
|
||||
const isCompletedFromServer = isTargetCompletedFromServer(update.core);
|
||||
const isCurrentTarget = targetIndex === currentUpdateTargetIndex;
|
||||
|
||||
// Determine the actual state of this target
|
||||
let targetState: 'completed' | 'current' | 'waiting' | 'error';
|
||||
let displayProgress = 0;
|
||||
let statusText = '';
|
||||
|
||||
if (status === UpdateStatus.ERROR) {
|
||||
targetState = 'error';
|
||||
statusText = 'Failed';
|
||||
} else if (isCompletedFromServer || status === UpdateStatus.SUCCESS || currentProgress === 100) {
|
||||
// Target is marked as completed in updated_targets array from server OR has success status
|
||||
targetState = 'completed';
|
||||
statusText = 'Complete';
|
||||
displayProgress = 100;
|
||||
} else if (status === UpdateStatus.STARTED || status === UpdateStatus.IN_PROGRESS) {
|
||||
// Show progress for any target that has started or is in progress, regardless of currentUpdateTargetIndex
|
||||
targetState = 'current';
|
||||
displayProgress = currentProgress;
|
||||
statusText = update.hasProgressBar ? `${currentProgress}%` : 'Updating...';
|
||||
} else {
|
||||
targetState = 'waiting';
|
||||
statusText = 'Waiting';
|
||||
displayProgress = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} class="flex items-center gap-4 p-4 bg-base-200 rounded-lg">
|
||||
{getUpdateIcon(update.core)}
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="font-medium">{update.core}</span>
|
||||
<span class="text-sm">
|
||||
{targetState === 'completed' ? (
|
||||
<span class="flex items-center gap-1 text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M9 12l2 2l4 -4" />
|
||||
</svg>
|
||||
{statusText}
|
||||
</span>
|
||||
) : targetState === 'error' ? (
|
||||
<span class="flex items-center gap-1 text-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
{statusText}
|
||||
</span>
|
||||
) : targetState === 'current' ? (
|
||||
<span class="flex items-center gap-1 text-primary">
|
||||
{statusText}
|
||||
</span>
|
||||
) : (
|
||||
<span class="flex items-center gap-1 text-base-content/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-clock-pause">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M20.942 13.018a9 9 0 1 0 -7.909 7.922" />
|
||||
<path d="M12 7v5l2 2" />
|
||||
<path d="M17 17v5" />
|
||||
<path d="M21 17v5" />
|
||||
</svg>
|
||||
{statusText}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{update.hasProgressBar ? (
|
||||
<progress
|
||||
class={`progress w-full ${
|
||||
targetState === 'completed' ? 'progress-success' :
|
||||
targetState === 'current' ? 'progress-primary' :
|
||||
targetState === 'error' ? 'progress-error' :
|
||||
'progress-base-300'
|
||||
}`}
|
||||
value={displayProgress}
|
||||
max="100"
|
||||
></progress>
|
||||
) : (
|
||||
<progress
|
||||
class={`progress w-full ${
|
||||
targetState === 'completed' ? 'progress-success' :
|
||||
targetState === 'current' ? 'progress-primary' :
|
||||
targetState === 'error' ? 'progress-error' :
|
||||
'progress-base-300'
|
||||
}`}
|
||||
value={targetState === 'current' ? undefined : displayProgress}
|
||||
max="100"
|
||||
></progress>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Please do not power off the device during the update process.</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(updateState === 'initial') && (
|
||||
<div class="modal-backdrop" onClick={resetModal}></div>
|
||||
)}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
3
src/components/WebSocketAtom.ts
Normal file
3
src/components/WebSocketAtom.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { atom } from "nanostores";
|
||||
|
||||
export const $websocket = atom<WebSocket | null>(null);
|
||||
|
|
@ -1,692 +1,255 @@
|
|||
import { useCallback, useEffect } from 'preact/hooks';
|
||||
import { $isUpdaterModalOpen, $isSuccessPopupOpen, $isErrorPopupOpen, $websocket, $wsConnectionStatus, $updateErrorMessages, $p4FirmwareProgress, $p4WebinterfaceProgress, $stm32FirmwareProgress, $c6FirmwareProgress, $updateState, $updatedTargets, $availableUpdates, type UpdateInfo, UpdateStatus, TargetUpdateStatus, TotalUpdateStatus } from './AppProviderStore';
|
||||
import { stateSetters, request_available_updates } from './AppProviderStore';
|
||||
import UpdaterModal from './UpdaterModal';
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { $websocket } from "./WebSocketAtom";
|
||||
import { stateSetters } from "./AppProviderStore";
|
||||
import {
|
||||
$scanned_wifi_networks,
|
||||
$current_network_configuration,
|
||||
$network_status_display,
|
||||
$network_config_error,
|
||||
request_network_config,
|
||||
type WiFiNetwork,
|
||||
authmodeToSecurity,
|
||||
ConnectionMode,
|
||||
} from "./NetworkManagerStore";
|
||||
|
||||
// Helper function to get target index for update types
|
||||
const getTargetIndex = (core: string) => {
|
||||
switch (core.toLowerCase()) {
|
||||
case 'p4 webinterface': return 1; // P4 webinterface (first)
|
||||
case 'p4 firmware': return 2; // P4 firmware (second)
|
||||
case 'stm32': return 0; // STM32 firmware
|
||||
case 'c6': return 3; // C6 firmware (last)
|
||||
default: return -1;
|
||||
}
|
||||
};
|
||||
export { $websocket } from "./WebSocketAtom";
|
||||
|
||||
// Heartbeat configuration
|
||||
const HEARTBEAT_INTERVAL = 1500; // Send ping every 1 second
|
||||
const HEARTBEAT_TIMEOUT = 700; // Wait 1 second for pong response
|
||||
const HEARTBEAT_INTERVAL = 1500; // Send ping every 1.5 seconds
|
||||
const HEARTBEAT_TIMEOUT = 1000; // Wait 1 second for pong response
|
||||
const MAX_MISSED_HEARTBEATS = 5; // Close connection after 5 missed pongs
|
||||
|
||||
// Heartbeat state
|
||||
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let missedHeartbeats = 0;
|
||||
let _lastPingSentAt = 0;
|
||||
|
||||
// Reconnect state
|
||||
let isReconnecting = false;
|
||||
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let reload_site_after_webinterface_update = false;
|
||||
|
||||
// Restart detection state
|
||||
let disconnectDetected = false;
|
||||
let heartbeatsAfterReconnect = 0;
|
||||
const HEARTBEATS_NEEDED_AFTER_RESTART = 3;
|
||||
|
||||
function clearHeartbeat() {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
heartbeatInterval = null;
|
||||
}
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
}
|
||||
|
||||
function clearReconnect() {
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
isReconnecting = false;
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout);
|
||||
reconnectTimeout = null;
|
||||
}
|
||||
isReconnecting = false;
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (isReconnecting) {
|
||||
////console.log("Reconnect already scheduled, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
isReconnecting = true;
|
||||
////console.log("Scheduling reconnect in 1 second");
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
isReconnecting = false;
|
||||
}, 1000);
|
||||
if (isReconnecting) {
|
||||
return;
|
||||
}
|
||||
isReconnecting = true;
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
connectWebSocket();
|
||||
isReconnecting = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
// Send ping message
|
||||
const pingMessage = JSON.stringify({ message_type: "ping" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(pingMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to send heartbeat ping:", error);
|
||||
missedHeartbeats++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout for pong response
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
missedHeartbeats++;
|
||||
if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
|
||||
console.error("Too many missed heartbeats, closing connection");
|
||||
|
||||
// Check if we're in restarting state - this indicates device restart
|
||||
const currentUpdateState = $updateState.get();
|
||||
if (currentUpdateState === 'restarting') {
|
||||
//console.log("🔄 Disconnect detected during restart - marking for restart detection");
|
||||
disconnectDetected = true;
|
||||
heartbeatsAfterReconnect = 0;
|
||||
}
|
||||
|
||||
// Wichtig: Erst Heartbeat stoppen
|
||||
clearHeartbeat();
|
||||
|
||||
// Dann WebSocket schließen und null setzen
|
||||
|
||||
if ($websocket.get()) {
|
||||
($websocket.get() as WebSocket).close();
|
||||
stateSetters.setWebSocketConnectionStatus("Disconnected");
|
||||
$websocket.set(null);
|
||||
}
|
||||
}
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
const ws = $websocket.get();
|
||||
if (ws && (ws as WebSocket).readyState === (ws as WebSocket).OPEN) {
|
||||
// Send ping message via WebSocket
|
||||
const pingMessage = JSON.stringify({ message_type: "ping" });
|
||||
try {
|
||||
(ws as WebSocket).send(pingMessage);
|
||||
_lastPingSentAt = performance.now();
|
||||
} catch (error) {
|
||||
missedHeartbeats++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout for pong response
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
heartbeatTimeout = setTimeout(() => {
|
||||
missedHeartbeats++;
|
||||
if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
|
||||
// First stop the heartbeat, then close + null the socket
|
||||
clearHeartbeat();
|
||||
const cur = $websocket.get();
|
||||
if (cur) {
|
||||
(cur as WebSocket).close();
|
||||
stateSetters.setWebSocketConnectionStatus("Disconnected");
|
||||
$websocket.set(null);
|
||||
}
|
||||
}
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
function startHeartbeat() {
|
||||
clearHeartbeat(); // Clear any existing heartbeat
|
||||
|
||||
heartbeatInterval = setInterval(() => {
|
||||
sendHeartbeat();
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
clearHeartbeat(); // Clear any existing heartbeat
|
||||
heartbeatInterval = setInterval(() => {
|
||||
sendHeartbeat();
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
stateSetters.setWebSocketConnectionStatus("Connected");
|
||||
//console.log("WebSocket erfolgreich verbunden");
|
||||
missedHeartbeats = 0;
|
||||
stateSetters.setWebSocketConnectionStatus("Connected");
|
||||
missedHeartbeats = 0;
|
||||
|
||||
// If we reconnected after detecting a restart, start counting heartbeats
|
||||
if (disconnectDetected) {
|
||||
//console.log("🔄 Reconnected after restart - starting heartbeat counter");
|
||||
heartbeatsAfterReconnect = 0;
|
||||
}
|
||||
// Start heartbeat mechanism
|
||||
startHeartbeat();
|
||||
|
||||
// Start heartbeat mechanism
|
||||
startHeartbeat();
|
||||
// Sende initiale Statusanfrage
|
||||
if ($websocket.get() !== null) {
|
||||
const initial_state_request = JSON.stringify({ message_type: "initial_state_request" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(initial_state_request);
|
||||
} catch (error) {
|
||||
console.error("Failed to send initial state request:", error);
|
||||
}
|
||||
|
||||
// Request update status immediately to catch any missed completion messages
|
||||
setTimeout(() => {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
const update_status_request = JSON.stringify({ message_type: "request_update_status" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(update_status_request);
|
||||
//console.log("Requested update status on reconnection");
|
||||
} catch (error) {
|
||||
console.error("Failed to send update status request:", error);
|
||||
}
|
||||
}
|
||||
}, 500); // Reduced from 1000ms to 500ms for faster response
|
||||
|
||||
// send request for available updates
|
||||
setTimeout(() => {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
request_available_updates();
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
// Ask the device for its current network config so the UI knows whether to show
|
||||
// the first-time wizard or the configured overview.
|
||||
setTimeout(() => {
|
||||
request_network_config();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function onMessage(message: MessageEvent) {
|
||||
////console.log("WebSocket message received:", message.data.toString());
|
||||
// Verarbeite JSON Nachrichten
|
||||
const dataFromServer = JSON.parse(message.data.toString());
|
||||
if (dataFromServer.message_type === "pong") {
|
||||
// reset missed heartbeats and clear pending timeout
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
missedHeartbeats = 0;
|
||||
//console.log("Heartbeat pong received");
|
||||
// Verarbeite JSON Nachrichten
|
||||
const dataFromServer = JSON.parse(message.data.toString());
|
||||
|
||||
// Count heartbeats after reconnect for restart detection
|
||||
if (disconnectDetected) {
|
||||
heartbeatsAfterReconnect++;
|
||||
//console.log(`🔄 Heartbeat ${heartbeatsAfterReconnect}/${HEARTBEATS_NEEDED_AFTER_RESTART} after restart`);
|
||||
|
||||
if (heartbeatsAfterReconnect >= HEARTBEATS_NEEDED_AFTER_RESTART) {
|
||||
//console.log("🎉 Device restart completed - closing success popup and resetting state");
|
||||
disconnectDetected = false;
|
||||
disconnectDetected = false;
|
||||
heartbeatsAfterReconnect = 0;
|
||||
|
||||
// Close success popup and reset state
|
||||
stateSetters.setSuccessPopupOpen(false);
|
||||
stateSetters.setUpdateState('initial');
|
||||
if (reload_site_after_webinterface_update) {
|
||||
// Hard reload ohne Cache
|
||||
//window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dataFromServer.message_type === "pong") {
|
||||
// reset missed heartbeats and clear pending timeout
|
||||
if (heartbeatTimeout) {
|
||||
clearTimeout(heartbeatTimeout);
|
||||
heartbeatTimeout = null;
|
||||
}
|
||||
else if (dataFromServer.message_type === "state_update" || dataFromServer.message_type === "initial_state_response") {
|
||||
|
||||
// If this is the initial state response, also request update status
|
||||
if (dataFromServer.message_type === "initial_state_response") {
|
||||
//console.log("📊 Initial state received, requesting update status...");
|
||||
setTimeout(() => {
|
||||
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
|
||||
const update_status_request = JSON.stringify({ message_type: "request_update_status" });
|
||||
try {
|
||||
($websocket.get() as WebSocket).send(update_status_request);
|
||||
//console.log("Requested update status after initial state");
|
||||
} catch (error) {
|
||||
console.error("Failed to send update status request:", error);
|
||||
}
|
||||
}
|
||||
}, 200); // Short delay to ensure connection is stable
|
||||
}
|
||||
|
||||
if (dataFromServer.free_internal_heap) {
|
||||
stateSetters.setFreeInternalHeap(dataFromServer.free_internal_heap);
|
||||
}
|
||||
// DCDC State aktualisieren
|
||||
if (dataFromServer.dcdcStateMachine) {
|
||||
stateSetters.setDCDCState(dataFromServer.dcdcStateMachine.dcdc_state);
|
||||
stateSetters.setPOWERGOOD(dataFromServer.dcdcStateMachine.POWERGOOD);
|
||||
stateSetters.setDcdcErrorStatus(dataFromServer.dcdcStateMachine.error_flag);
|
||||
stateSetters.setREF_value(dataFromServer.dcdcStateMachine.ref_value);
|
||||
}
|
||||
|
||||
// Laser State aktualisieren
|
||||
if (dataFromServer.laserStateMachine) {
|
||||
// Laser state update
|
||||
stateSetters.setLaserState(dataFromServer.laserStateMachine.laser_state);
|
||||
stateSetters.setModulationInputMethod(dataFromServer.laserStateMachine.laser_state);
|
||||
stateSetters.setREF_calibrated(dataFromServer.laserStateMachine.is_REF_calibrated);
|
||||
stateSetters.setHAS_DUTY_LOOKUP_TABLE(dataFromServer.laserStateMachine.has_duty_lookup_table);
|
||||
stateSetters.setLaserErrorStatus(dataFromServer.laserStateMachine.error_flag);
|
||||
stateSetters.setLASERGOOD(dataFromServer.laserStateMachine.LASERGOOD);
|
||||
stateSetters.setLaserSetTemp(dataFromServer.laserStateMachine.set_temp);
|
||||
stateSetters.setLaserMaxTemp(dataFromServer.laserStateMachine.max_temp);
|
||||
stateSetters.setInterlock(dataFromServer.laserStateMachine.interlock);
|
||||
stateSetters.setModulationInput(dataFromServer.laserStateMachine.modulationInput);
|
||||
stateSetters.setSetCurrent(dataFromServer.laserStateMachine.set_current);
|
||||
stateSetters.setMaxCurrent(dataFromServer.laserStateMachine.max_current);
|
||||
stateSetters.setThresholdCurrent(dataFromServer.laserStateMachine.threshold_current);
|
||||
stateSetters.setCurrentReading(dataFromServer.laserStateMachine.current_reading);
|
||||
stateSetters.setLaserTemp(dataFromServer.laserStateMachine.laser_temp);
|
||||
}
|
||||
|
||||
// Driver State aktualisieren
|
||||
if (dataFromServer.driverStateMachine) {
|
||||
stateSetters.setDriverTemp(dataFromServer.driverStateMachine.driver_temp);
|
||||
stateSetters.setDriverMaxTemp(dataFromServer.driverStateMachine.max_temp);
|
||||
stateSetters.setVccVoltage(dataFromServer.driverStateMachine.vcc_voltage);
|
||||
stateSetters.setPdVoltage(dataFromServer.driverStateMachine.pd_voltage);
|
||||
}
|
||||
} else if (dataFromServer.message_type === "calibration_table") {
|
||||
// Log raw data to see the actual format of values
|
||||
//console.log("Raw calibration message:", message.data.toString().substring(0, 500) + "...");
|
||||
// Parse values and format to always show with decimal places
|
||||
const parsedValues = dataFromServer.values ?
|
||||
dataFromServer.values.map((val: any) => {
|
||||
// Convert to number and keep as float
|
||||
const numVal = Number(val);
|
||||
|
||||
return numVal;
|
||||
}) : [];
|
||||
|
||||
// Erstelle einen neuen Array mit korrekten Indizes
|
||||
// Fülle Indizes 0-499 mit Nullen und füge dann die tatsächlichen Werte ein
|
||||
const minDuty = dataFromServer.min_duty || 500;
|
||||
const completedValues = [];
|
||||
|
||||
// Fülle Nullen für Indizes von 0 bis (minDuty-1)
|
||||
for (let i = 0; i < minDuty; i++) {
|
||||
completedValues.push(0);
|
||||
}
|
||||
for (let i = 0; i < parsedValues.length; i++) {
|
||||
completedValues.push(parsedValues[i]);
|
||||
}
|
||||
|
||||
// Store values in state - use the completed values with correct indices
|
||||
stateSetters.setCalibrationTableData(
|
||||
completedValues,
|
||||
minDuty,
|
||||
dataFromServer.max_duty || 3500, (dataFromServer.points_count || parsedValues.length) + minDuty
|
||||
);
|
||||
} else if (dataFromServer.message_type === "update_information") {
|
||||
//console.log("Update information received");
|
||||
//console.log("Raw update_information data:", dataFromServer);
|
||||
// Update Information
|
||||
if (dataFromServer.update_available) {
|
||||
const infos: UpdateInfo[] = [];
|
||||
if (dataFromServer.update_available.p4_firmware) {
|
||||
infos.push({
|
||||
core: 'p4 firmware',
|
||||
currentVersion: dataFromServer.update_available.p4_firmware.current_version,
|
||||
newVersion: dataFromServer.update_available.p4_firmware.new_version,
|
||||
icon: 'p4 firmware',
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("P4 Firmware update available");
|
||||
}
|
||||
if (dataFromServer.update_available.p4_webinterface) {
|
||||
infos.push({
|
||||
core: "p4 webinterface",
|
||||
currentVersion: dataFromServer.update_available.p4_webinterface.current_version,
|
||||
newVersion: dataFromServer.update_available.p4_webinterface.new_version,
|
||||
icon: "p4 webinterface",
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("P4 Webinterface update available");
|
||||
}
|
||||
if (dataFromServer.update_available.stm32_firmware) {
|
||||
infos.push({
|
||||
core: "STM32",
|
||||
currentVersion: dataFromServer.update_available.stm32_firmware.current_version,
|
||||
newVersion: dataFromServer.update_available.stm32_firmware.new_version,
|
||||
icon: "stm32",
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("STM32 Firmware update available");
|
||||
}
|
||||
if (dataFromServer.update_available.c6_firmware) {
|
||||
infos.push({
|
||||
core: "C6",
|
||||
currentVersion: dataFromServer.update_available.c6_firmware.current_version,
|
||||
newVersion: dataFromServer.update_available.c6_firmware.new_version,
|
||||
icon: "c6",
|
||||
hasProgressBar: true,
|
||||
updateAvailable: true,
|
||||
});
|
||||
//console.log("C6 Firmware update available");
|
||||
}
|
||||
stateSetters.setAvailableUpdates(infos);
|
||||
stateSetters.setUpdateChangelog(dataFromServer.changelog);
|
||||
}
|
||||
// Alte einzelne Progress-Message-Handler wurden durch complete_update_status ersetzt
|
||||
|
||||
} else if (dataFromServer.message_type === "complete_update_status") {
|
||||
//console.log("📊 Complete update status received:", dataFromServer);
|
||||
|
||||
// Check if we're currently in restarting state - if so, ignore status updates
|
||||
const currentUpdateState = $updateState.get() as any;
|
||||
if (currentUpdateState === 'restarting') {
|
||||
//console.log("🔄 Ignoring complete update status during RESTARTING state - countdown in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the complete status information for all targets
|
||||
if (dataFromServer.targets) {
|
||||
const targets = dataFromServer.targets;
|
||||
|
||||
// STM32 Status
|
||||
if (targets.stm32) {
|
||||
const status = targets.stm32.status;
|
||||
const progress = targets.stm32.progress || 0;
|
||||
const updateAvailable = targets.stm32.update_available || false;
|
||||
|
||||
// Convert string status to TargetUpdateStatus enum
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setStm32FirmwareProgress(progress, statusEnum);
|
||||
//console.log(`📊 STM32: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
|
||||
// P4 Webinterface Status
|
||||
if (targets.p4_webinterface) {
|
||||
const status = targets.p4_webinterface.status;
|
||||
const progress = targets.p4_webinterface.progress || 0;
|
||||
const updateAvailable = targets.p4_webinterface.update_available || false;
|
||||
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setP4WebinterfaceProgress(progress, statusEnum);
|
||||
//console.log(`📊 P4 Webinterface: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
|
||||
// P4 Firmware Status
|
||||
if (targets.p4_firmware) {
|
||||
const status = targets.p4_firmware.status;
|
||||
const progress = targets.p4_firmware.progress || 0;
|
||||
const updateAvailable = targets.p4_firmware.update_available || false;
|
||||
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setP4FirmwareProgress(progress, statusEnum);
|
||||
//console.log(`📊 P4 Firmware: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
|
||||
// C6 Status
|
||||
if (targets.c6) {
|
||||
const status = targets.c6.status;
|
||||
const progress = targets.c6.progress || 0;
|
||||
const updateAvailable = targets.c6.update_available || false;
|
||||
|
||||
let statusEnum = TargetUpdateStatus.NO_UPDATE;
|
||||
switch (status) {
|
||||
case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
|
||||
case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
|
||||
case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
|
||||
case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
|
||||
default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
|
||||
}
|
||||
|
||||
stateSetters.setC6FirmwareProgress(progress, statusEnum);
|
||||
//console.log(`📊 C6: status=${status}, progress=${progress}, available=${updateAvailable}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's an ongoing update
|
||||
if (dataFromServer.update_in_progress) {
|
||||
//console.log("🔄 Update in progress detected, setting total_update_status to updating");
|
||||
stateSetters.setUpdateInProgress(true);
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
|
||||
// Use the total_update_status from backend if available
|
||||
if (dataFromServer.total_update_status !== undefined) {
|
||||
const totalStatus = dataFromServer.total_update_status;
|
||||
//console.log("📊 Backend total_update_status:", totalStatus);
|
||||
|
||||
switch (totalStatus) {
|
||||
case 0: // INITIAL
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
|
||||
break;
|
||||
case 1: // STARTED
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.STARTED);
|
||||
break;
|
||||
case 2: // UPDATING
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
|
||||
break;
|
||||
case 3: // SUCCESS
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
|
||||
break;
|
||||
case 4: // ERROR
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.ERROR);
|
||||
break;
|
||||
case 5: // RESTARTING
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.RESTARTING);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unknown total_update_status:", totalStatus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the updater modal, if it is not already open and no success/error popups are showing
|
||||
const isModalOpen = $isUpdaterModalOpen.get();
|
||||
const isSuccessPopupOpen = $isSuccessPopupOpen.get();
|
||||
const isErrorPopupOpen = $isErrorPopupOpen.get();
|
||||
/*console.log("🎯 Modal decision matrix:", {
|
||||
isModalOpen,
|
||||
totalUpdateStatus: currentUpdateState,
|
||||
isSuccessPopupOpen,
|
||||
isErrorPopupOpen,
|
||||
shouldOpen: !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen && currentUpdateState !== 'success' && currentUpdateState !== 'restarting'
|
||||
});*/
|
||||
|
||||
// Don't open updater modal if success/error popups are open or we're in success/restarting state
|
||||
const shouldOpenModal = !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen &&
|
||||
currentUpdateState !== 'success' && currentUpdateState !== 'restarting';
|
||||
if (shouldOpenModal) {
|
||||
//console.log("🚀 Opening updater modal - conditions met");
|
||||
stateSetters.setUpdaterModalOpen(true);
|
||||
} else {
|
||||
//console.log("🚫 Skipping updater modal - conditions not met");
|
||||
}
|
||||
} else {
|
||||
//console.log("✅ No update in progress, checking if all available updates are completed");
|
||||
|
||||
// Check if all targets with update_available: true are completed
|
||||
let allAvailableUpdatesCompleted = true;
|
||||
let hasAnyAvailableUpdates = false;
|
||||
|
||||
if (dataFromServer.targets.stm32 && dataFromServer.targets.stm32.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.stm32.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
if (dataFromServer.targets.p4_webinterface && dataFromServer.targets.p4_webinterface.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.p4_webinterface.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
if (dataFromServer.targets.p4_firmware && dataFromServer.targets.p4_firmware.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.p4_firmware.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
if (dataFromServer.targets.c6 && dataFromServer.targets.c6.update_available) {
|
||||
hasAnyAvailableUpdates = true;
|
||||
if (dataFromServer.targets.c6.status !== 'completed') {
|
||||
allAvailableUpdatesCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAnyAvailableUpdates && allAvailableUpdatesCompleted) {
|
||||
//console.log("🎉 All available updates completed successfully, setting total_update_status to success");
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
|
||||
} else if (!hasAnyAvailableUpdates) {
|
||||
//console.log("ℹ️ No updates available, resetting total_update_status to initial");
|
||||
stateSetters.setUpdateInProgress(false);
|
||||
if (currentUpdateState !== 'success' && currentUpdateState !== 'error' && currentUpdateState !== 'restarting') {
|
||||
stateSetters.setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
|
||||
}
|
||||
} else {
|
||||
//console.log("⏳ Some available updates still in progress, keeping current total_update_status");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (dataFromServer.message_type === "response_update_status") {
|
||||
console.log("📊 Update status response received:", dataFromServer);
|
||||
|
||||
// Check if we're currently in restarting state - if so, ignore status updates
|
||||
const currentUpdateState = $updateState.get() as any;
|
||||
if (currentUpdateState === 'restarting') {
|
||||
//console.log("🔄 Ignoring update status during RESTARTING state - countdown in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Always process the complete status information, regardless of update_in_progress
|
||||
//console.log("🔄 Processing complete update status for all targets");
|
||||
|
||||
// Set progress and status for each update type if provided
|
||||
if (dataFromServer.p4_firmware_progress !== undefined) {
|
||||
const status = dataFromServer.p4_firmware_status || 0;
|
||||
stateSetters.setP4FirmwareProgress(dataFromServer.p4_firmware_progress, status);
|
||||
//console.log(`📊 P4 Firmware: progress=${dataFromServer.p4_firmware_progress}, status=${status}`);
|
||||
}
|
||||
if (dataFromServer.p4_webinterface_progress !== undefined) {
|
||||
const status = dataFromServer.p4_webinterface_status || 0;
|
||||
stateSetters.setP4WebinterfaceProgress(dataFromServer.p4_webinterface_progress, status);
|
||||
//console.log(`📊 P4 Webinterface: progress=${dataFromServer.p4_webinterface_progress}, status=${status}`);
|
||||
}
|
||||
if (dataFromServer.stm32_firmware_progress !== undefined) {
|
||||
const status = dataFromServer.stm32_firmware_status || 0;
|
||||
stateSetters.setStm32FirmwareProgress(dataFromServer.stm32_firmware_progress, status);
|
||||
//console.log(`📊 STM32 Firmware: progress=${dataFromServer.stm32_firmware_progress}, status=${status}`);
|
||||
}
|
||||
if (dataFromServer.c6_firmware_progress !== undefined) {
|
||||
const status = dataFromServer.c6_firmware_status || 0;
|
||||
stateSetters.setC6FirmwareProgress(dataFromServer.c6_firmware_progress, status);
|
||||
//console.log(`📊 C6 Firmware: progress=${dataFromServer.c6_firmware_progress}, status=${status}`);
|
||||
}
|
||||
|
||||
// Store updated_targets array from server for UpdaterModal
|
||||
if (dataFromServer.updated_targets && Array.isArray(dataFromServer.updated_targets)) {
|
||||
stateSetters.setUpdatedTargets(dataFromServer.updated_targets);
|
||||
//console.log("🎯 Updated targets:", dataFromServer.updated_targets);
|
||||
}
|
||||
|
||||
// Set current target if provided
|
||||
if (dataFromServer.current_target !== undefined) {
|
||||
stateSetters.setCurrentUpdateTargetIndex(dataFromServer.current_target);
|
||||
//console.log("🎯 Current target:", dataFromServer.current_target);
|
||||
}
|
||||
|
||||
// Check if there's an ongoing update
|
||||
if (dataFromServer.update_in_progress) {
|
||||
//console.log("🔄 Update in progress detected, setting state to updating");
|
||||
stateSetters.setUpdateInProgress(true);
|
||||
stateSetters.setUpdateState("updating");
|
||||
|
||||
// Open the updater modal, if it is not already open and no success/error popups are showing
|
||||
const isModalOpen = $isUpdaterModalOpen.get();
|
||||
const isSuccessPopupOpen = $isSuccessPopupOpen.get();
|
||||
const isErrorPopupOpen = $isErrorPopupOpen.get();
|
||||
/*console.log("🎯 Modal decision matrix:", {
|
||||
isModalOpen,
|
||||
updateState: currentUpdateState,
|
||||
isSuccessPopupOpen,
|
||||
isErrorPopupOpen,
|
||||
shouldOpen: !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen && currentUpdateState !== 'success' && currentUpdateState !== 'restarting'
|
||||
});*/
|
||||
|
||||
// Don't open updater modal if success/error popups are open or we're in success/restarting state
|
||||
const shouldOpenModal = !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen &&
|
||||
currentUpdateState !== 'success' && currentUpdateState !== 'restarting';
|
||||
if (shouldOpenModal) {
|
||||
//console.log("🚀 Opening updater modal - conditions met");
|
||||
stateSetters.setUpdaterModalOpen(true);
|
||||
} else {
|
||||
//console.log("🚫 Skipping updater modal - conditions not met");
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Unbekannter Nachrichtentyp – korrektes Feld protokollieren
|
||||
console.warn(`[${new Date().toISOString()}] Unbekannter Nachrichtentyp: ${dataFromServer.message_type ?? 'undefined'}`);
|
||||
console.debug('Payload:', dataFromServer);
|
||||
missedHeartbeats = 0;
|
||||
if ((window as any).__wsDebug && _lastPingSentAt > 0)
|
||||
console.log(`[WS] pong RTT=${(performance.now() - _lastPingSentAt).toFixed(1)}ms`);
|
||||
} else if (dataFromServer.message_type === "wifi_networks_response") {
|
||||
const networks: WiFiNetwork[] = (dataFromServer.networks ?? []).map((n: any) => ({
|
||||
SSID: n.ssid ?? "",
|
||||
RSSI: n.rssi ?? 0,
|
||||
Channel: n.channel ?? 0,
|
||||
SecurityType: authmodeToSecurity(n.authmode ?? 0),
|
||||
}));
|
||||
$scanned_wifi_networks.set(networks);
|
||||
} else if (dataFromServer.message_type === "network_status_update") {
|
||||
$network_status_display.set({
|
||||
Connection_Mode: dataFromServer.Connection_Mode ?? ConnectionMode.CONNECTION_MODE_UNKNOWN,
|
||||
AP_Channel: dataFromServer.AP_Channel ?? 0,
|
||||
IP_Address_Station: dataFromServer.IP_Address_Station ?? "",
|
||||
Gateway_IP_Address_Station: dataFromServer.Gateway_IP_Address_Station ?? "",
|
||||
Netmask_Station: dataFromServer.Netmask_Station ?? "",
|
||||
MDNS_Hostname: dataFromServer.MDNS_Hostname ?? "",
|
||||
MAC_Address_WiFi: dataFromServer.MAC_Address_WiFi ?? "",
|
||||
rssi: dataFromServer.rssi ?? null,
|
||||
});
|
||||
} else if (dataFromServer.message_type === "network_config_response") {
|
||||
console.log("[WS] network_config_response received:", dataFromServer);
|
||||
$current_network_configuration.set({
|
||||
Connection_Mode: dataFromServer.Connection_Mode ?? ConnectionMode.CONNECTION_MODE_UNKNOWN,
|
||||
DHCP_Mode_Station: dataFromServer.DHCP_Mode_Station ?? true,
|
||||
AP_Channel: dataFromServer.AP_Channel ?? 0,
|
||||
IP_Address_Station: dataFromServer.IP_Address_Station ?? "",
|
||||
Gateway_IP_Address_Station: dataFromServer.Gateway_IP_Address_Station ?? "",
|
||||
Netmask_Station: dataFromServer.Netmask_Station ?? "",
|
||||
Static_Cfg_IP_Address_Station: dataFromServer.Static_Cfg_IP_Address_Station ?? "",
|
||||
Static_Cfg_Gateway_IP_Address_Station: dataFromServer.Static_Cfg_Gateway_IP_Address_Station ?? "",
|
||||
Static_Cfg_Netmask_Station: dataFromServer.Static_Cfg_Netmask_Station ?? "",
|
||||
SSID_Station: dataFromServer.SSID_Station ?? "",
|
||||
Password_Station: dataFromServer.Password_Station ?? "",
|
||||
SSID_AP: dataFromServer.SSID_AP ?? "",
|
||||
Password_AP: dataFromServer.Password_AP ?? "",
|
||||
MDNS_Hostname: dataFromServer.MDNS_Hostname ?? "hydrobuddy",
|
||||
});
|
||||
// Update MAC in status display — config response is the first message that carries it
|
||||
if (dataFromServer.MAC_Address_WiFi) {
|
||||
$network_status_display.set({
|
||||
...$network_status_display.get(),
|
||||
MAC_Address_WiFi: dataFromServer.MAC_Address_WiFi || $network_status_display.get().MAC_Address_WiFi,
|
||||
});
|
||||
}
|
||||
console.log("[WS] $current_network_configuration updated:", $current_network_configuration.get());
|
||||
} else if (dataFromServer.message_type === "network_config_failed") {
|
||||
const failedSsid: string = dataFromServer.failed_ssid ?? "";
|
||||
const reason: string = dataFromServer.reason ?? "unknown";
|
||||
const connectionMode: ConnectionMode = dataFromServer.connection_mode ?? ConnectionMode.CONNECTION_MODE_UNKNOWN;
|
||||
console.warn(`[WS] network_config_failed: ssid='${failedSsid}' reason='${reason}'`);
|
||||
$network_config_error.set({ reason, failed_ssid: failedSsid, connection_mode: connectionMode });
|
||||
} else if (dataFromServer.message_type === "error_popup") {
|
||||
console.warn(`[error_popup] ${dataFromServer.error ?? dataFromServer.message ?? "(no detail)"}`);
|
||||
} else {
|
||||
// Unbekannter Nachrichtentyp - korrektes Feld protokollieren
|
||||
console.warn(`[${new Date().toISOString()}] Unbekannter Nachrichtentyp: ${dataFromServer.message_type ?? "undefined"}`);
|
||||
console.debug("Payload:", dataFromServer);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error: Event) {
|
||||
$wsConnectionStatus.set("Disconnected");
|
||||
//console.log("WebSocket error:", error);
|
||||
clearHeartbeat();
|
||||
scheduleReconnect();
|
||||
stateSetters.setWebSocketConnectionStatus("Disconnected");
|
||||
console.log("WebSocket error:", error);
|
||||
clearHeartbeat();
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
function onClose(event: CloseEvent) {
|
||||
$wsConnectionStatus.set("Disconnected");
|
||||
//console.log("WebSocket closed with code:", event.code);
|
||||
//console.log("Close reason:", event.reason);
|
||||
//console.log("Clean close:", event.wasClean);
|
||||
clearHeartbeat();
|
||||
if (event.code === 1000) {
|
||||
//console.log("Normal closure");
|
||||
}
|
||||
scheduleReconnect();
|
||||
stateSetters.setWebSocketConnectionStatus("Disconnected");
|
||||
console.log("WebSocket closed with code:", event.code);
|
||||
console.log("Close reason:", event.reason);
|
||||
console.log("Clean close:", event.wasClean);
|
||||
clearHeartbeat();
|
||||
if (event.code === 1000) {
|
||||
console.log("Normal closure");
|
||||
}
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
function getWsAddress(): string {
|
||||
// Derive WS address from the page's origin so it works with any hostname or IP.
|
||||
const h = window.location.hostname;
|
||||
if (h === "localhost" || h === "127.0.0.1") {
|
||||
return (import.meta.env.PUBLIC_DEV_WS_HOST as string | undefined) ?? "ws://hydrobuddy.local/ws";
|
||||
}
|
||||
return `ws://${h}/ws`;
|
||||
}
|
||||
|
||||
const connectWebSocket = () => {
|
||||
// Clear any existing reconnect attempts
|
||||
clearReconnect();
|
||||
clearHeartbeat();
|
||||
// Clear any existing reconnect attempts
|
||||
clearReconnect();
|
||||
clearHeartbeat();
|
||||
|
||||
const wsAddress = `ws://hydrobuddy.local/ws`;
|
||||
//console.log(`Verbinde mit: ${wsAddress}`);
|
||||
if ($websocket.get() !== null) {
|
||||
($websocket.get() as WebSocket).close(1000);
|
||||
$websocket.set(null);
|
||||
}
|
||||
$websocket.set(new WebSocket(wsAddress));
|
||||
if ($websocket.get() !== null) {
|
||||
($websocket.get() as WebSocket).onopen = onOpen;
|
||||
|
||||
($websocket.get() as WebSocket).onmessage = onMessage;
|
||||
|
||||
($websocket.get() as WebSocket).onerror = onError;
|
||||
|
||||
($websocket.get() as WebSocket).onclose = onClose;
|
||||
}
|
||||
}
|
||||
const wsAddress = getWsAddress();
|
||||
console.log(`Verbinde mit: ${wsAddress}`);
|
||||
if ($websocket.get() !== null) {
|
||||
($websocket.get() as WebSocket).close(1000);
|
||||
$websocket.set(null);
|
||||
}
|
||||
$websocket.set(new WebSocket(wsAddress));
|
||||
const ws = $websocket.get();
|
||||
if (ws !== null) {
|
||||
(ws as WebSocket).onopen = onOpen;
|
||||
(ws as WebSocket).onmessage = onMessage;
|
||||
(ws as WebSocket).onerror = onError;
|
||||
(ws as WebSocket).onclose = onClose;
|
||||
}
|
||||
};
|
||||
|
||||
export const WebSocketProvider = () => {
|
||||
|
||||
// Auto-connect WebSocket at first render
|
||||
useEffect(() => {
|
||||
//console.log("Component mounted, starting WebSocket connection...");
|
||||
connectWebSocket();
|
||||
// Cleanup beim Unmounting
|
||||
return () => {
|
||||
clearHeartbeat();
|
||||
clearReconnect();
|
||||
($websocket.get() as WebSocket).close();
|
||||
$websocket.set(null);
|
||||
$wsConnectionStatus.set("Disconnected");
|
||||
};
|
||||
}, []);
|
||||
// Auto-connect WebSocket at first render
|
||||
useEffect(() => {
|
||||
console.log("Component mounted, starting WebSocket connection...");
|
||||
connectWebSocket();
|
||||
// Cleanup beim Unmounting
|
||||
return () => {
|
||||
clearHeartbeat();
|
||||
clearReconnect();
|
||||
const cur = $websocket.get();
|
||||
if (cur) (cur as WebSocket).close();
|
||||
$websocket.set(null);
|
||||
stateSetters.setWebSocketConnectionStatus("Disconnected");
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
|
|||
508
src/components/WifiSetupWizard.astro
Normal file
508
src/components/WifiSetupWizard.astro
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
---
|
||||
import { IconPlant } from "@tabler/icons-preact";
|
||||
---
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<!-- Heading -->
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="text-emerald-600"><IconPlant size={32} strokeWidth={1.75} /></span>
|
||||
<h2 class="card-title text-2xl text-emerald-700">Let's get you online</h2>
|
||||
</div>
|
||||
<p class="opacity-70 mb-2">
|
||||
Three quick steps and your HydroBuddy will be on the network.
|
||||
</p>
|
||||
|
||||
<!-- Step indicator (DaisyUI steps) -->
|
||||
<ul class="steps steps-horizontal w-full my-4">
|
||||
<li class="step step-primary" id="step-indicator-1" data-step="1">Name</li>
|
||||
<li class="step" id="step-indicator-2" data-step="2">WiFi</li>
|
||||
<li class="step" id="step-indicator-3" data-step="3">Connect</li>
|
||||
</ul>
|
||||
|
||||
<!-- ============== STEP 1 — Name the device ============== -->
|
||||
<section id="wizard-step-1" class="space-y-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-1">What should we call your device?</h3>
|
||||
<p class="text-sm opacity-70">
|
||||
Pick a name — you'll reach the web interface at <span class="font-mono">http://<name>.local</span>
|
||||
from any device on the same network.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label
|
||||
class="input flex items-center gap-2 font-mono text-sm focus-within:bg-emerald-50 focus-within:outline-emerald-500"
|
||||
>
|
||||
<span class="opacity-60 select-none">http://</span>
|
||||
<input
|
||||
type="text"
|
||||
id="wizard-hostname-input"
|
||||
placeholder="hydrobuddy"
|
||||
maxlength="31"
|
||||
required
|
||||
class="grow min-w-0 bg-transparent border-none outline-none"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<span class="opacity-60 select-none">.local</span>
|
||||
</label>
|
||||
<p id="wizard-hostname-error" class="text-sm text-error hidden"></p>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<button id="wizard-step-1-next" class="btn btn-primary" disabled>Next</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============== STEP 2 — Pick a WiFi network ============== -->
|
||||
<section id="wizard-step-2" class="hidden space-y-4">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-1">Pick a WiFi network</h3>
|
||||
<p class="text-sm opacity-70">Scan and tap the network you want to join.</p>
|
||||
</div>
|
||||
<button id="wizard-scan-btn" class="btn btn-outline btn-primary btn-sm">
|
||||
<span id="wizard-scan-btn-label">Scan</span>
|
||||
<span id="wizard-scan-btn-spinner" class="loading loading-spinner loading-xs hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Networks list. Populated dynamically; empty/placeholder text shown when len === 0. -->
|
||||
<div
|
||||
id="wizard-networks-list"
|
||||
class="rounded-lg border border-base-300 divide-y divide-base-300 max-h-72 overflow-y-auto"
|
||||
>
|
||||
<div class="p-6 text-center text-sm opacity-60" id="wizard-networks-empty">
|
||||
Press <span class="font-semibold">Scan</span> to discover nearby networks.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual SSID entry (hidden networks, or want to type the name) -->
|
||||
<details class="collapse collapse-arrow bg-base-200/50">
|
||||
<summary class="collapse-title text-sm font-medium">
|
||||
Hidden network? Type the name manually
|
||||
</summary>
|
||||
<div class="collapse-content">
|
||||
<input
|
||||
type="text"
|
||||
id="wizard-manual-ssid"
|
||||
placeholder="SSID"
|
||||
maxlength="31"
|
||||
class="input input-bordered w-full"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="flex justify-between mt-4">
|
||||
<button id="wizard-step-2-back" class="btn btn-ghost">Back</button>
|
||||
<button id="wizard-step-2-next" class="btn btn-primary" disabled>Next</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============== STEP 3 — Password + Apply ============== -->
|
||||
<section id="wizard-step-3" class="hidden space-y-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-1">Connect to <span id="wizard-selected-ssid" class="text-emerald-700">-</span></h3>
|
||||
<p class="text-sm opacity-70">Enter the network password. Leave empty for open networks.</p>
|
||||
</div>
|
||||
|
||||
<label
|
||||
class="input flex items-center gap-2 font-mono text-sm focus-within:bg-emerald-50 focus-within:outline-emerald-500"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="wizard-password-input"
|
||||
placeholder="Password"
|
||||
maxlength="31"
|
||||
class="grow min-w-0 bg-transparent border-none outline-none"
|
||||
autocomplete="new-password"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<button type="button" id="wizard-password-toggle" class="btn btn-ghost btn-xs p-1 min-h-0 h-auto">
|
||||
<span id="wizard-password-toggle-text" class="text-xs opacity-70">show</span>
|
||||
</button>
|
||||
</label>
|
||||
|
||||
<details class="collapse collapse-arrow bg-base-200/50">
|
||||
<summary class="collapse-title text-sm font-medium">Advanced: Static IP (optional)</summary>
|
||||
<div class="collapse-content space-y-3">
|
||||
<p class="text-xs opacity-60">
|
||||
Leave blank to use DHCP (recommended). Only set these if your network requires fixed IPs.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">IP Address</span>
|
||||
<input
|
||||
type="text"
|
||||
id="wizard-static-ip"
|
||||
placeholder="192.168.1.50"
|
||||
class="input input-bordered input-sm font-mono"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">Netmask</span>
|
||||
<input
|
||||
type="text"
|
||||
id="wizard-static-mask"
|
||||
placeholder="255.255.255.0"
|
||||
class="input input-bordered input-sm font-mono"
|
||||
/>
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<span class="label-text text-xs">Gateway</span>
|
||||
<input
|
||||
type="text"
|
||||
id="wizard-static-gw"
|
||||
placeholder="192.168.1.1"
|
||||
class="input input-bordered input-sm font-mono"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p id="wizard-static-error" class="text-sm text-error hidden"></p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<span class="text-sm">
|
||||
When you apply, the device will reboot its WiFi and briefly disconnect. You'll need to switch your
|
||||
phone/laptop to the same network and reopen <span class="font-mono" id="wizard-final-url">http://hydrobuddy.local</span>.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-4 gap-3">
|
||||
<button id="wizard-step-3-back" class="btn btn-ghost">Back</button>
|
||||
<button id="wizard-step-3-apply" class="btn btn-primary">
|
||||
<span id="wizard-apply-label">Apply & Connect</span>
|
||||
<span id="wizard-apply-spinner" class="loading loading-spinner loading-xs hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import {
|
||||
ConnectionMode,
|
||||
$current_network_configuration,
|
||||
$scanned_wifi_networks,
|
||||
$selected_ssid,
|
||||
$wifi_network_pw,
|
||||
$hostname_input_field,
|
||||
scan_wifi_networks,
|
||||
set_hostname,
|
||||
send_new_network_configuration_to_alpha,
|
||||
type WiFiNetwork,
|
||||
} from "../components/NetworkManagerStore";
|
||||
import {
|
||||
HOSTNAME_REGEX,
|
||||
MAX_HOSTNAME_LENGTH,
|
||||
isValidHostname,
|
||||
normalizeHostnameValue,
|
||||
} from "../components/helper_scripts/hostname_validation";
|
||||
import {
|
||||
is_valid_ip_format,
|
||||
isValidNetmask,
|
||||
isPracticalNetmask,
|
||||
isInSameSubnet,
|
||||
isInValidHostRange,
|
||||
} from "../components/helper_scripts/IP_Subnetmask_validation";
|
||||
|
||||
// ---------- DOM lookup ----------
|
||||
const stepSections = [
|
||||
document.getElementById("wizard-step-1"),
|
||||
document.getElementById("wizard-step-2"),
|
||||
document.getElementById("wizard-step-3"),
|
||||
];
|
||||
const stepIndicators = [
|
||||
document.getElementById("step-indicator-1"),
|
||||
document.getElementById("step-indicator-2"),
|
||||
document.getElementById("step-indicator-3"),
|
||||
];
|
||||
|
||||
// Step 1
|
||||
const hostnameInput = document.getElementById("wizard-hostname-input") as HTMLInputElement | null;
|
||||
const hostnameError = document.getElementById("wizard-hostname-error");
|
||||
const step1Next = document.getElementById("wizard-step-1-next") as HTMLButtonElement | null;
|
||||
|
||||
// Step 2
|
||||
const scanBtn = document.getElementById("wizard-scan-btn") as HTMLButtonElement | null;
|
||||
const scanBtnLabel = document.getElementById("wizard-scan-btn-label");
|
||||
const scanBtnSpinner = document.getElementById("wizard-scan-btn-spinner");
|
||||
const networksList = document.getElementById("wizard-networks-list");
|
||||
const networksEmpty = document.getElementById("wizard-networks-empty");
|
||||
const manualSsid = document.getElementById("wizard-manual-ssid") as HTMLInputElement | null;
|
||||
const step2Back = document.getElementById("wizard-step-2-back");
|
||||
const step2Next = document.getElementById("wizard-step-2-next") as HTMLButtonElement | null;
|
||||
|
||||
// Step 3
|
||||
const selectedSsidDisplay = document.getElementById("wizard-selected-ssid");
|
||||
const passwordInput = document.getElementById("wizard-password-input") as HTMLInputElement | null;
|
||||
const passwordToggle = document.getElementById("wizard-password-toggle");
|
||||
const passwordToggleText = document.getElementById("wizard-password-toggle-text");
|
||||
const staticIp = document.getElementById("wizard-static-ip") as HTMLInputElement | null;
|
||||
const staticMask = document.getElementById("wizard-static-mask") as HTMLInputElement | null;
|
||||
const staticGw = document.getElementById("wizard-static-gw") as HTMLInputElement | null;
|
||||
const staticError = document.getElementById("wizard-static-error");
|
||||
const finalUrl = document.getElementById("wizard-final-url");
|
||||
const step3Back = document.getElementById("wizard-step-3-back");
|
||||
const step3Apply = document.getElementById("wizard-step-3-apply") as HTMLButtonElement | null;
|
||||
const applyLabel = document.getElementById("wizard-apply-label");
|
||||
const applySpinner = document.getElementById("wizard-apply-spinner");
|
||||
|
||||
// ---------- Step navigation ----------
|
||||
let currentStep: 1 | 2 | 3 = 1;
|
||||
|
||||
function gotoStep(n: 1 | 2 | 3) {
|
||||
currentStep = n;
|
||||
stepSections.forEach((el, i) => el?.classList.toggle("hidden", i + 1 !== n));
|
||||
stepIndicators.forEach((el, i) => el?.classList.toggle("step-primary", i + 1 <= n));
|
||||
}
|
||||
|
||||
// ---------- Step 1: hostname ----------
|
||||
function validateHostname() {
|
||||
if (!hostnameInput || !hostnameError || !step1Next) return;
|
||||
const raw = hostnameInput.value;
|
||||
const normalized = normalizeHostnameValue(raw);
|
||||
if (normalized !== raw) {
|
||||
hostnameInput.value = normalized;
|
||||
}
|
||||
if (normalized.length === 0) {
|
||||
hostnameError.classList.add("hidden");
|
||||
step1Next.disabled = true;
|
||||
return;
|
||||
}
|
||||
if (!isValidHostname(normalized)) {
|
||||
hostnameError.textContent =
|
||||
"Invalid hostname. Use lowercase letters, digits, and hyphens only. Cannot start or end with a hyphen.";
|
||||
hostnameError.classList.remove("hidden");
|
||||
step1Next.disabled = true;
|
||||
return;
|
||||
}
|
||||
hostnameError.classList.add("hidden");
|
||||
step1Next.disabled = false;
|
||||
$hostname_input_field.set(normalized);
|
||||
}
|
||||
hostnameInput?.addEventListener("input", validateHostname);
|
||||
// Prime with the device's current hostname (typically "hydrobuddy") so the user can keep it with one click.
|
||||
$current_network_configuration.subscribe((cfg) => {
|
||||
if (hostnameInput && !hostnameInput.value && cfg.MDNS_Hostname) {
|
||||
hostnameInput.value = cfg.MDNS_Hostname;
|
||||
validateHostname();
|
||||
}
|
||||
});
|
||||
|
||||
step1Next?.addEventListener("click", () => {
|
||||
// Persist the hostname change to the device immediately, then advance.
|
||||
// This way even if the user bails at step 2 they keep the friendlier name.
|
||||
const h = $hostname_input_field.get();
|
||||
if (h) set_hostname(h);
|
||||
if (finalUrl) finalUrl.textContent = `http://${h || "hydrobuddy"}.local`;
|
||||
gotoStep(2);
|
||||
});
|
||||
|
||||
// ---------- Step 2: scan + pick ----------
|
||||
function setScanning(on: boolean) {
|
||||
if (!scanBtn || !scanBtnLabel || !scanBtnSpinner) return;
|
||||
scanBtn.disabled = on;
|
||||
scanBtnLabel.textContent = on ? "Scanning…" : "Scan";
|
||||
scanBtnSpinner.classList.toggle("hidden", !on);
|
||||
}
|
||||
|
||||
scanBtn?.addEventListener("click", () => {
|
||||
setScanning(true);
|
||||
scan_wifi_networks();
|
||||
// Scan takes ~3 s on the firmware side; release the button after a generous timeout
|
||||
// even if no networks are returned (in which case the list stays empty).
|
||||
setTimeout(() => setScanning(false), 5000);
|
||||
});
|
||||
|
||||
function renderNetworks(networks: WiFiNetwork[]) {
|
||||
if (!networksList) return;
|
||||
if (networks.length === 0) {
|
||||
// Keep the "press Scan" placeholder
|
||||
networksList.innerHTML = `
|
||||
<div class="p-6 text-center text-sm opacity-60">
|
||||
Press <span class="font-semibold">Scan</span> to discover nearby networks.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
setScanning(false);
|
||||
|
||||
// Dedup + sort by signal (descending)
|
||||
const seen = new Set<string>();
|
||||
const dedup = networks.filter((n) => {
|
||||
if (!n.SSID || seen.has(n.SSID)) return false;
|
||||
seen.add(n.SSID);
|
||||
return true;
|
||||
});
|
||||
dedup.sort((a, b) => b.RSSI - a.RSSI);
|
||||
|
||||
const selectedNow = $selected_ssid.get();
|
||||
networksList.innerHTML = dedup
|
||||
.map((n) => {
|
||||
const strength = n.RSSI >= -50 ? "Excellent" : n.RSSI >= -60 ? "Good" : n.RSSI >= -70 ? "Fair" : "Weak";
|
||||
const isOpen = n.SecurityType === "Open";
|
||||
const isSel = n.SSID === selectedNow;
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 p-3 hover:bg-emerald-50 transition-colors text-left ${isSel ? "bg-emerald-100" : ""}"
|
||||
data-ssid="${escapeHtml(n.SSID)}"
|
||||
data-security="${escapeHtml(n.SecurityType)}"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-medium truncate">${escapeHtml(n.SSID)}</div>
|
||||
<div class="text-xs opacity-60 flex gap-2">
|
||||
<span>${escapeHtml(n.SecurityType)}</span>
|
||||
<span>·</span>
|
||||
<span>Ch ${n.Channel}</span>
|
||||
<span>·</span>
|
||||
<span>${strength} (${n.RSSI} dBm)</span>
|
||||
</div>
|
||||
</div>
|
||||
${isOpen ? '<span class="badge badge-ghost badge-sm shrink-0">Open</span>' : '<span class="badge badge-sm shrink-0">🔒</span>'}
|
||||
</button>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
networksList.querySelectorAll("button[data-ssid]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const ssid = (btn as HTMLElement).dataset.ssid ?? "";
|
||||
$selected_ssid.set(ssid);
|
||||
if (manualSsid) manualSsid.value = "";
|
||||
updateStep2NextEnabled();
|
||||
});
|
||||
});
|
||||
}
|
||||
$scanned_wifi_networks.subscribe(renderNetworks);
|
||||
|
||||
manualSsid?.addEventListener("input", () => {
|
||||
const v = manualSsid.value.trim();
|
||||
if (v.length > 0) {
|
||||
$selected_ssid.set(v);
|
||||
} else if ($selected_ssid.get() === manualSsid.value) {
|
||||
$selected_ssid.set("");
|
||||
}
|
||||
updateStep2NextEnabled();
|
||||
});
|
||||
|
||||
function updateStep2NextEnabled() {
|
||||
if (!step2Next) return;
|
||||
const ssid = $selected_ssid.get();
|
||||
step2Next.disabled = !ssid || ssid.length === 0;
|
||||
}
|
||||
$selected_ssid.subscribe(() => {
|
||||
updateStep2NextEnabled();
|
||||
if (selectedSsidDisplay) selectedSsidDisplay.textContent = $selected_ssid.get() || "-";
|
||||
});
|
||||
|
||||
step2Back?.addEventListener("click", () => gotoStep(1));
|
||||
step2Next?.addEventListener("click", () => gotoStep(3));
|
||||
|
||||
// ---------- Step 3: password + static IP + apply ----------
|
||||
passwordInput?.addEventListener("input", () => {
|
||||
$wifi_network_pw.set(passwordInput.value);
|
||||
});
|
||||
|
||||
passwordToggle?.addEventListener("click", () => {
|
||||
if (!passwordInput || !passwordToggleText) return;
|
||||
const show = passwordInput.type === "password";
|
||||
passwordInput.type = show ? "text" : "password";
|
||||
passwordToggleText.textContent = show ? "hide" : "show";
|
||||
});
|
||||
|
||||
function validateStaticIp(): { ok: boolean; useStatic: boolean; ip: string; mask: string; gw: string } {
|
||||
if (!staticIp || !staticMask || !staticGw || !staticError) {
|
||||
return { ok: true, useStatic: false, ip: "", mask: "", gw: "" };
|
||||
}
|
||||
const ip = staticIp.value.trim();
|
||||
const mask = staticMask.value.trim();
|
||||
const gw = staticGw.value.trim();
|
||||
const anyFilled = ip || mask || gw;
|
||||
if (!anyFilled) {
|
||||
staticError.classList.add("hidden");
|
||||
return { ok: true, useStatic: false, ip: "", mask: "", gw: "" };
|
||||
}
|
||||
if (!ip || !mask || !gw) {
|
||||
staticError.textContent = "Fill all three fields (IP, netmask, gateway) or leave all blank for DHCP.";
|
||||
staticError.classList.remove("hidden");
|
||||
return { ok: false, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
if (!is_valid_ip_format(ip)) {
|
||||
staticError.textContent = "IP address is not a valid IPv4 address.";
|
||||
staticError.classList.remove("hidden");
|
||||
return { ok: false, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
if (!isValidNetmask(mask) || !isPracticalNetmask(mask)) {
|
||||
staticError.textContent = "Netmask must be a practical IPv4 netmask (e.g. 255.255.255.0).";
|
||||
staticError.classList.remove("hidden");
|
||||
return { ok: false, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
if (!is_valid_ip_format(gw)) {
|
||||
staticError.textContent = "Gateway is not a valid IPv4 address.";
|
||||
staticError.classList.remove("hidden");
|
||||
return { ok: false, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
if (!isInSameSubnet(ip, gw, mask)) {
|
||||
staticError.textContent = "Gateway must be in the same subnet as the IP address.";
|
||||
staticError.classList.remove("hidden");
|
||||
return { ok: false, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
if (!isInValidHostRange(ip, mask) || !isInValidHostRange(gw, mask)) {
|
||||
staticError.textContent = "IP or gateway is the network/broadcast address — pick a host address.";
|
||||
staticError.classList.remove("hidden");
|
||||
return { ok: false, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
staticError.classList.add("hidden");
|
||||
return { ok: true, useStatic: true, ip, mask, gw };
|
||||
}
|
||||
staticIp?.addEventListener("input", validateStaticIp);
|
||||
staticMask?.addEventListener("input", validateStaticIp);
|
||||
staticGw?.addEventListener("input", validateStaticIp);
|
||||
|
||||
step3Back?.addEventListener("click", () => gotoStep(2));
|
||||
|
||||
step3Apply?.addEventListener("click", () => {
|
||||
const ssid = $selected_ssid.get();
|
||||
if (!ssid) {
|
||||
gotoStep(2);
|
||||
return;
|
||||
}
|
||||
const ipCheck = validateStaticIp();
|
||||
if (!ipCheck.ok) return;
|
||||
|
||||
const baseCfg = $current_network_configuration.get();
|
||||
const cfg = {
|
||||
...baseCfg,
|
||||
Connection_Mode: ipCheck.useStatic ? ConnectionMode.CONNECTION_MODE_STA_STATIC : ConnectionMode.CONNECTION_MODE_STA_DHCP,
|
||||
DHCP_Mode_Station: !ipCheck.useStatic,
|
||||
SSID_Station: ssid,
|
||||
Password_Station: passwordInput?.value ?? "",
|
||||
MDNS_Hostname: $hostname_input_field.get() || baseCfg.MDNS_Hostname || "hydrobuddy",
|
||||
Static_Cfg_IP_Address_Station: ipCheck.useStatic ? ipCheck.ip : baseCfg.Static_Cfg_IP_Address_Station,
|
||||
Static_Cfg_Netmask_Station: ipCheck.useStatic ? ipCheck.mask : baseCfg.Static_Cfg_Netmask_Station,
|
||||
Static_Cfg_Gateway_IP_Address_Station: ipCheck.useStatic ? ipCheck.gw : baseCfg.Static_Cfg_Gateway_IP_Address_Station,
|
||||
};
|
||||
|
||||
if (step3Apply) step3Apply.disabled = true;
|
||||
applyLabel?.classList.add("hidden");
|
||||
applySpinner?.classList.remove("hidden");
|
||||
|
||||
const ok = send_new_network_configuration_to_alpha(cfg);
|
||||
if (!ok) {
|
||||
if (step3Apply) step3Apply.disabled = false;
|
||||
applyLabel?.classList.remove("hidden");
|
||||
applySpinner?.classList.add("hidden");
|
||||
}
|
||||
// On success, the device will briefly drop the WS, reconnect, and broadcast
|
||||
// a new network_config_response — index.astro's pickView() then swaps the
|
||||
// wizard for the overview automatically.
|
||||
});
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]!);
|
||||
}
|
||||
</script>
|
||||
74
src/components/helper_scripts/IP_Subnetmask_validation.tsx
Normal file
74
src/components/helper_scripts/IP_Subnetmask_validation.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
export function is_valid_ip_format(ip_str: string) {
|
||||
if (
|
||||
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
|
||||
ip_str
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert IP address string to 32-bit integer
|
||||
export function ipToNumber(ip: string): number {
|
||||
const parts = ip.split(".").map(Number);
|
||||
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
||||
}
|
||||
|
||||
// Validate that netmask is valid (contiguous 1s followed by 0s)
|
||||
export function isValidNetmask(netmask: string): boolean {
|
||||
if (!is_valid_ip_format(netmask)) return false;
|
||||
|
||||
const maskNum = ipToNumber(netmask);
|
||||
// Check if mask is valid: all 1s followed by all 0s
|
||||
// Invert the mask, add 1, and check if result is a power of 2
|
||||
const inverted = ~maskNum >>> 0; // Use unsigned right shift to handle 32-bit
|
||||
const isValid = (inverted & (inverted + 1)) === 0;
|
||||
|
||||
// Also check that mask is not all 0s or all 1s
|
||||
return isValid && maskNum !== 0 && maskNum !== 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
// Check if netmask is practical for network use (has space for at least IP + Gateway)
|
||||
// Returns true if netmask allows at least 2 host addresses
|
||||
export function isPracticalNetmask(netmask: string): boolean {
|
||||
if (!isValidNetmask(netmask)) return false;
|
||||
|
||||
const maskNum = ipToNumber(netmask);
|
||||
const inverted = ~maskNum >>> 0;
|
||||
const hostBits = Math.log2(inverted + 1);
|
||||
|
||||
// Need at least 2 host addresses (IP + Gateway), so hostBits must be >= 1
|
||||
// /32 (255.255.255.255) has 0 host bits - not practical
|
||||
// /31 (255.255.255.254) has 1 host bit but only 2 addresses total (network + broadcast) - not practical for IP + Gateway
|
||||
// /30 (255.255.255.252) has 2 host bits = 4 addresses (network + 2 hosts + broadcast) - minimum practical
|
||||
return hostBits >= 2;
|
||||
}
|
||||
|
||||
// Calculate network address from IP and netmask
|
||||
export function getNetworkAddress(ip: string, netmask: string): number {
|
||||
return ipToNumber(ip) & ipToNumber(netmask);
|
||||
}
|
||||
|
||||
// Calculate broadcast address from IP and netmask
|
||||
export function getBroadcastAddress(ip: string, netmask: string): number {
|
||||
const networkAddr = getNetworkAddress(ip, netmask);
|
||||
const invertedMask = ~ipToNumber(netmask) >>> 0;
|
||||
return networkAddr | invertedMask;
|
||||
}
|
||||
|
||||
// Check if IP is in valid host range (not network address, not broadcast address)
|
||||
export function isInValidHostRange(ip: string, netmask: string): boolean {
|
||||
const ipNum = ipToNumber(ip);
|
||||
const networkAddr = getNetworkAddress(ip, netmask);
|
||||
const broadcastAddr = getBroadcastAddress(ip, netmask);
|
||||
|
||||
// IP must not be network address (all host bits 0) or broadcast address (all host bits 1)
|
||||
return ipNum !== networkAddr && ipNum !== broadcastAddr;
|
||||
}
|
||||
|
||||
// Check if two IPs are in the same subnet
|
||||
export function isInSameSubnet(ip1: string, ip2: string, netmask: string): boolean {
|
||||
return getNetworkAddress(ip1, netmask) === getNetworkAddress(ip2, netmask);
|
||||
}
|
||||
140
src/components/helper_scripts/daisy_toasts.tsx
Normal file
140
src/components/helper_scripts/daisy_toasts.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
function showToast(message: string, type: "info" | "success" | "warning" | "error" = "info") {
|
||||
// Determine the correct parent: if a <dialog> is open, append inside it
|
||||
// so toasts render in the browser's top layer above the modal backdrop
|
||||
const openDialog = document.querySelector("dialog[open]");
|
||||
const parent = openDialog || document.body;
|
||||
|
||||
// Create toast container if it doesn't exist within the target parent
|
||||
let toastContainer = parent.querySelector("#toast-container") as HTMLElement | null;
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement("div");
|
||||
toastContainer.id = "toast-container";
|
||||
toastContainer.className = "toast toast-top toast-end z-50";
|
||||
toastContainer.style.zIndex = "9999";
|
||||
parent.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement("div");
|
||||
toast.className = `alert alert-${type}`;
|
||||
toast.innerHTML = `<span>${message}</span>`;
|
||||
|
||||
// Add to container
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Play sound based on type
|
||||
playToastSound(type);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Sound function for toasts
|
||||
function playToastSound(type: "info" | "success" | "warning" | "error") {
|
||||
try {
|
||||
// Create audio context
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
|
||||
// Different frequencies for different types
|
||||
let frequency: number;
|
||||
switch (type) {
|
||||
case "success":
|
||||
frequency = 800; // Higher pitch for success
|
||||
break;
|
||||
case "warning":
|
||||
frequency = 600; // Medium pitch for warning
|
||||
break;
|
||||
case "error":
|
||||
frequency = 400; // Lower pitch for error
|
||||
break;
|
||||
case "info":
|
||||
default:
|
||||
frequency = 500; // Default pitch for info
|
||||
break;
|
||||
}
|
||||
|
||||
// Create oscillator
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
|
||||
oscillator.type = 'sine';
|
||||
|
||||
// Set volume and duration
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
|
||||
// Play sound
|
||||
// Play 3 bongs in sequence
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.1);
|
||||
|
||||
// Second bong
|
||||
setTimeout(() => {
|
||||
const oscillator2 = audioContext.createOscillator();
|
||||
const gainNode2 = audioContext.createGain();
|
||||
oscillator2.connect(gainNode2);
|
||||
gainNode2.connect(audioContext.destination);
|
||||
oscillator2.frequency.setValueAtTime(frequency, audioContext.currentTime);
|
||||
oscillator2.type = 'sine';
|
||||
gainNode2.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
|
||||
oscillator2.start(audioContext.currentTime);
|
||||
oscillator2.stop(audioContext.currentTime + 0.1);
|
||||
}, 100);
|
||||
|
||||
// Third bong (longer for error effect)
|
||||
setTimeout(() => {
|
||||
const oscillator3 = audioContext.createOscillator();
|
||||
const gainNode3 = audioContext.createGain();
|
||||
oscillator3.connect(gainNode3);
|
||||
gainNode3.connect(audioContext.destination);
|
||||
oscillator3.frequency.setValueAtTime(frequency, audioContext.currentTime);
|
||||
oscillator3.type = 'sine';
|
||||
gainNode3.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode3.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
|
||||
oscillator3.start(audioContext.currentTime);
|
||||
oscillator3.stop(audioContext.currentTime + 0.2);
|
||||
}, 200);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
const oscillator5 = audioContext.createOscillator();
|
||||
const gainNode5 = audioContext.createGain();
|
||||
oscillator5.connect(gainNode5);
|
||||
gainNode5.connect(audioContext.destination);
|
||||
oscillator5.frequency.setValueAtTime(frequency * 1.5, audioContext.currentTime); // 50% higher
|
||||
oscillator5.type = 'sine';
|
||||
gainNode5.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode5.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.15);
|
||||
oscillator5.start(audioContext.currentTime);
|
||||
oscillator5.stop(audioContext.currentTime + 0.15);
|
||||
}, 400);
|
||||
|
||||
setTimeout(() => {
|
||||
const oscillator4 = audioContext.createOscillator();
|
||||
const gainNode4 = audioContext.createGain();
|
||||
oscillator4.connect(gainNode4);
|
||||
gainNode4.connect(audioContext.destination);
|
||||
oscillator4.frequency.setValueAtTime(frequency * 0.7, audioContext.currentTime); // 30% lower
|
||||
oscillator4.type = 'sine';
|
||||
gainNode4.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode4.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
oscillator4.start(audioContext.currentTime);
|
||||
oscillator4.stop(audioContext.currentTime + 0.3);
|
||||
}, 600);
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log("Audio not supported:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export { showToast };
|
||||
54
src/components/helper_scripts/hostname_validation.tsx
Normal file
54
src/components/helper_scripts/hostname_validation.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// mDNS hostname rules (RFC 952 + RFC 1123 + RFC 6762):
|
||||
// - Lowercase letters a-z, digits 0-9, hyphens only
|
||||
// - Cannot start or end with a hyphen
|
||||
// - Single label (no dots) for mDNS
|
||||
// - Max 31 chars (firmware NVS limit, stricter than DNS label max of 63)
|
||||
export const MIN_HOSTNAME_LENGTH = 1;
|
||||
export const MAX_HOSTNAME_LENGTH = 31;
|
||||
export const HOSTNAME_PATTERN = "^[a-z0-9](?:[a-z0-9-]{0,29}[a-z0-9])?$";
|
||||
export const HOSTNAME_REGEX = new RegExp(HOSTNAME_PATTERN);
|
||||
export const HOSTNAME_URL_SUFFIX = ".local";
|
||||
export const HOSTNAME_PLACEHOLDER = "hostname";
|
||||
|
||||
export function isValidHostname(hostname: string): boolean {
|
||||
if (typeof hostname !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = hostname.trim().toLowerCase();
|
||||
const length = value.length;
|
||||
|
||||
if (length < MIN_HOSTNAME_LENGTH || length > MAX_HOSTNAME_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return HOSTNAME_REGEX.test(value);
|
||||
}
|
||||
|
||||
export function normalizeHostnameValue(rawValue: string): string {
|
||||
if (!rawValue) return "";
|
||||
let value = rawValue.trim().toLowerCase();
|
||||
if (!value) return "";
|
||||
|
||||
// Strip protocol prefix
|
||||
value = value.replace(/^https?:\/\//, "");
|
||||
|
||||
// Strip .local suffix (pasted from browser address bar)
|
||||
value = value.replace(/\.local$/i, "");
|
||||
|
||||
// Take only first label — strip everything from first URL separator onward
|
||||
value = value.split(/[/:.\s]/)[0];
|
||||
|
||||
// Strip all characters that are not a-z, 0-9, or hyphen
|
||||
value = value.replace(/[^a-z0-9-]/g, "");
|
||||
|
||||
if (value.length > MAX_HOSTNAME_LENGTH) {
|
||||
value = value.slice(0, MAX_HOSTNAME_LENGTH);
|
||||
}
|
||||
|
||||
// Leading/trailing hyphens are NOT stripped here so the user can type
|
||||
// "hostname-test" without the hyphen disappearing mid-word.
|
||||
// The validator rejects them if the user leaves the field in that state.
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
---
|
||||
import Sidebar from "../components/Sidebar.astro";
|
||||
import GlobalModals from "../components/GlobalModals";
|
||||
import MemoryProgressBar from "../components/MemoryProgressBar";
|
||||
import "../styles/global.css";
|
||||
import Router from "../components/Router.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
const { title = "Alpha Driver" } = Astro.props;
|
||||
|
||||
let currentTheme = "light";
|
||||
let sidebarOpen = false;
|
||||
---
|
||||
|
||||
<div class="drawer lg:drawer-open">
|
||||
<input
|
||||
id="drawer-toggle"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
checked={sidebarOpen}
|
||||
/>
|
||||
|
||||
<div class="drawer-content flex flex-col">
|
||||
{/* Top Navigation Bar */}
|
||||
<div class="navbar bg-base-100 shadow-md">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="drawer-toggle" class="btn btn-square btn-ghost">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block w-6 h-6 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-xl font-audiowide">Alpha Driver Webinterface</span>
|
||||
</div>
|
||||
<div class="flex-1 flex justify-center">
|
||||
<div class="w-80">
|
||||
<MemoryProgressBar showDetails={false} className="text-xs" client:load />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
{/* Theme select */}
|
||||
<select
|
||||
class="select select-bordered select-sm mr-2 theme-selector"
|
||||
id="theme-selector"
|
||||
value={currentTheme}
|
||||
>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="goggles1">Goggles</option>
|
||||
<option value="gogglesBW">Goggles B&W</option>
|
||||
<option value="emerald">Nature</option>
|
||||
<option value="dark-neon">Dark Neon</option>
|
||||
<option value="retro">Sepia</option>
|
||||
<option value="halloween">Chocolate</option>
|
||||
<option value="winter">Winter</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<main class="flex-1 p-4 overflow-x-auto" id="spa-main-content" >
|
||||
{/* Global Update Notification */}
|
||||
<div id="update-notification" class="alert alert-info mb-4 hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||
<path d="M12 9l0 3" />
|
||||
<path d="M12 15l.01 0" />
|
||||
</svg>
|
||||
<span>Software updates are available for your device.</span>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-success" onclick="if(typeof window.openUpdaterModal === 'function') window.openUpdaterModal()">
|
||||
View Updates
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" onclick="document.getElementById('update-notification').classList.add('hidden')">
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content will be loaded here by the router */}
|
||||
<Router />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div class="drawer-side z-10">
|
||||
<label for="drawer-toggle" aria-label="close sidebar" class="drawer-overlay"
|
||||
></label>
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
{/* Global Modals */}
|
||||
<GlobalModals client:load />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Note: Update notification is now controlled by SecurityPage toggles
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Type declaration for global function
|
||||
declare global {
|
||||
interface Window {
|
||||
handleNavigation: (path: string) => void;
|
||||
navigateTo?: (path: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme handling
|
||||
const themeSelector = document.getElementById(
|
||||
"theme-selector",
|
||||
) as HTMLSelectElement;
|
||||
if (themeSelector) {
|
||||
const savedTheme = localStorage.getItem("theme") || "light";
|
||||
themeSelector.value = savedTheme;
|
||||
document.documentElement.setAttribute("data-theme", savedTheme);
|
||||
|
||||
themeSelector.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const theme = target.value;
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
});
|
||||
}
|
||||
|
||||
// Drawer handling
|
||||
const drawerToggle = document.getElementById(
|
||||
"drawer-toggle",
|
||||
) as HTMLInputElement;
|
||||
if (drawerToggle) {
|
||||
drawerToggle.addEventListener("change", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const checked = target.checked;
|
||||
drawerToggle.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
// Navigation handling
|
||||
window.handleNavigation = function (path: string) {
|
||||
console.log("handleNavigation", path);
|
||||
|
||||
// Add vibration feedback on mobile for page navigation
|
||||
if ('vibrate' in navigator && window.innerWidth < 1024) {
|
||||
navigator.vibrate(25);
|
||||
}
|
||||
|
||||
if (drawerToggle) {
|
||||
drawerToggle.checked = false;
|
||||
}
|
||||
if (typeof window !== "undefined" && window.navigateTo) {
|
||||
window.navigateTo(path);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,25 +1,108 @@
|
|||
---
|
||||
import { WebSocketProvider } from "../components/WebSocketProvider";
|
||||
import { TestbenchWebSocketProvider } from "../components/TestbenchWebsocketProvider";
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Router from "../components/Router.astro";
|
||||
|
||||
import { IconPlant } from "@tabler/icons-preact";
|
||||
import WifiSetupWizard from "../components/WifiSetupWizard.astro";
|
||||
import NetworkOverview from "../components/NetworkOverview.astro";
|
||||
---
|
||||
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Testbench Webinterface</title>
|
||||
<meta name="view-transition" content="same-origin" />
|
||||
<html lang="en" data-theme="emerald">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>HydroBuddy — Setup</title>
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-b from-emerald-50 to-base-100">
|
||||
<WebSocketProvider client:load />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<Layout title="Alpha Driver - Dashboard" />
|
||||
{/*<WebSocketProvider client:load />*/}
|
||||
<WebSocketProvider client:load />
|
||||
<TestbenchWebSocketProvider client:load />
|
||||
</body>
|
||||
<!-- Hero / greeting -->
|
||||
<header class="pt-12 pb-8 px-4">
|
||||
<div class="max-w-3xl mx-auto flex flex-col items-center text-center gap-4">
|
||||
<div class="text-emerald-600 drop-shadow-sm">
|
||||
<IconPlant size={96} strokeWidth={1.5} />
|
||||
</div>
|
||||
<h1 class="text-5xl sm:text-6xl md:text-7xl font-extrabold tracking-tight text-emerald-700 leading-none">
|
||||
HydroBuddy
|
||||
</h1>
|
||||
<p class="text-lg sm:text-xl text-emerald-900/70 max-w-xl">
|
||||
Hi! I'm your plant's caretaker — managing pH, water, and airflow so your hydroponic garden thrives.
|
||||
</p>
|
||||
<p class="text-sm opacity-60 mt-1" id="connection-status-line">Connecting to the device…</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="px-4 pb-12">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
|
||||
<!-- Shown while WS is opening or before first network_config_response arrives -->
|
||||
<div id="loading-state" class="flex flex-col items-center gap-3 py-12">
|
||||
<span class="loading loading-dots loading-lg text-emerald-600"></span>
|
||||
<p class="text-sm opacity-60">Waiting for device…</p>
|
||||
</div>
|
||||
|
||||
<!-- 3-step wizard (hostname → scan → connect). Stays hidden until SSID_Station is empty. -->
|
||||
<section id="wizard-container" class="hidden">
|
||||
<WifiSetupWizard />
|
||||
</section>
|
||||
|
||||
<!-- Overview / status / reconfigure. Shown once SSID_Station is set. -->
|
||||
<section id="overview-container" class="hidden">
|
||||
<NetworkOverview />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
import { $current_network_configuration, ConnectionMode } from "../components/NetworkManagerStore";
|
||||
import { $wsConnectionStatus } from "../components/AppProviderStore";
|
||||
|
||||
const loadingEl = document.getElementById("loading-state");
|
||||
const wizardEl = document.getElementById("wizard-container");
|
||||
const overviewEl = document.getElementById("overview-container");
|
||||
const statusLineEl = document.getElementById("connection-status-line");
|
||||
|
||||
// Track whether we've ever seen a real network_config_response. Until we have,
|
||||
// we stay on the loading view so we don't flash the wizard for half a second
|
||||
// while the WS is still opening.
|
||||
let hasFirstConfig = false;
|
||||
|
||||
function pickView() {
|
||||
if (!loadingEl || !wizardEl || !overviewEl) return;
|
||||
if (!hasFirstConfig) {
|
||||
loadingEl.classList.remove("hidden");
|
||||
wizardEl.classList.add("hidden");
|
||||
overviewEl.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
const cfg = $current_network_configuration.get();
|
||||
// "Unconfigured" = STA SSID never set. Firmware boots in APSTA with empty SSID_Station,
|
||||
// so this is exactly the first-time-setup case.
|
||||
const isUnconfigured = cfg.SSID_Station === "";
|
||||
loadingEl.classList.add("hidden");
|
||||
wizardEl.classList.toggle("hidden", !isUnconfigured);
|
||||
overviewEl.classList.toggle("hidden", isUnconfigured);
|
||||
}
|
||||
|
||||
// The store starts at default values (Connection_Mode UNKNOWN, SSID_Station "").
|
||||
// The first message that flips this is "network_config_response" arriving from the device
|
||||
// — at which point hasFirstConfig becomes true and we render the real view.
|
||||
$current_network_configuration.subscribe((cfg) => {
|
||||
if (cfg.Connection_Mode !== ConnectionMode.CONNECTION_MODE_UNKNOWN || cfg.MDNS_Hostname !== "hydrobuddy") {
|
||||
hasFirstConfig = true;
|
||||
}
|
||||
pickView();
|
||||
});
|
||||
|
||||
$wsConnectionStatus.subscribe((status) => {
|
||||
if (!statusLineEl) return;
|
||||
if (status === "Connected") {
|
||||
statusLineEl.textContent = hasFirstConfig ? "Connected" : "Connected — fetching configuration…";
|
||||
} else {
|
||||
statusLineEl.textContent = "Reconnecting to the device…";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue