mirror of
https://github.com/vincentvdwal/crust-craft.git
synced 2026-08-18 09:26:37 +02:00
add ap mode, modernize ui, split spoof code
This commit is contained in:
Generated
+11
-19
@@ -12,7 +12,8 @@
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"date-fns": "^4.1.0"
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-svelte": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
@@ -736,7 +737,6 @@
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -747,7 +747,6 @@
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -758,7 +757,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -768,14 +766,12 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1124,7 +1120,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
@@ -1535,7 +1530,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
@@ -1807,7 +1801,6 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -1870,7 +1863,6 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1880,7 +1872,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2005,7 +1996,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -2378,7 +2368,6 @@
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/espree": {
|
||||
@@ -2416,7 +2405,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
|
||||
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
@@ -2737,7 +2725,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
@@ -3088,7 +3075,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
@@ -3128,11 +3114,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lucide-svelte": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-1.0.1.tgz",
|
||||
"integrity": "sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
@@ -3921,7 +3915,6 @@
|
||||
"version": "5.38.8",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.8.tgz",
|
||||
"integrity": "sha512-UDpTbM/iuZ4MaMnn4ODB3rf5JKDyPOi5oJcopP0j7YHQ9BuJtsAqsR71r2N6AnJf7ygbalTJU5y8eSWGAQZjlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
@@ -4372,7 +4365,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -47,6 +47,7 @@
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"date-fns": "^4.1.0"
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-svelte": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+178
@@ -7,6 +7,25 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@cubejs-client/core':
|
||||
specifier: ^1.3.67
|
||||
version: 1.6.44
|
||||
chart.js:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.1
|
||||
chartjs-adapter-date-fns:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(chart.js@4.5.1)(date-fns@4.1.0)
|
||||
chartjs-plugin-annotation:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(chart.js@4.5.1)
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
lucide-svelte:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(svelte@5.38.8)
|
||||
devDependencies:
|
||||
'@eslint/compat':
|
||||
specifier: ^1.2.5
|
||||
@@ -77,6 +96,9 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@cubejs-client/core@1.6.44':
|
||||
resolution: {integrity: sha512-0J2i2xb4DQMdpZDnOnPUEz28fMgI04dbkePWxsGjCuCNcdr752f+iq9W1DZGzQ0JMdZfnSLmnaBrWWrcNMjATA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -316,6 +338,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.30':
|
||||
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
||||
|
||||
'@kurkle/color@0.3.4':
|
||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -694,6 +719,21 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chart.js@4.5.1:
|
||||
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
chartjs-adapter-date-fns@3.0.0:
|
||||
resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==}
|
||||
peerDependencies:
|
||||
chart.js: '>=2.8.0'
|
||||
date-fns: '>=2.0.0'
|
||||
|
||||
chartjs-plugin-annotation@3.1.0:
|
||||
resolution: {integrity: sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==}
|
||||
peerDependencies:
|
||||
chart.js: '>=4.0.0'
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -720,6 +760,12 @@ packages:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
core-js@3.49.0:
|
||||
resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -729,6 +775,28 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-format@3.1.2:
|
||||
resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time@3.1.0:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dayjs@1.11.20:
|
||||
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -920,6 +988,10 @@ packages:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1053,6 +1125,11 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lucide-svelte@1.0.1:
|
||||
resolution: {integrity: sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==}
|
||||
peerDependencies:
|
||||
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||
|
||||
magic-string@0.30.19:
|
||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||
|
||||
@@ -1107,6 +1184,15 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1261,6 +1347,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
ramda@0.27.2:
|
||||
resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
@@ -1361,6 +1450,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
ts-api-utils@2.1.0:
|
||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
||||
engines: {node: '>=18.12'}
|
||||
@@ -1386,6 +1478,9 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-search-params-polyfill@7.0.1:
|
||||
resolution: {integrity: sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -1446,6 +1541,12 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1472,6 +1573,19 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@cubejs-client/core@1.6.44':
|
||||
dependencies:
|
||||
core-js: 3.49.0
|
||||
cross-fetch: 3.2.0
|
||||
d3-format: 3.1.2
|
||||
d3-time-format: 4.1.0
|
||||
dayjs: 1.11.20
|
||||
ramda: 0.27.2
|
||||
url-search-params-polyfill: 7.0.1
|
||||
uuid: 11.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
@@ -1632,6 +1746,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -1987,6 +2103,19 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chart.js@4.5.1:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
chartjs-adapter-date-fns@3.0.0(chart.js@4.5.1)(date-fns@4.1.0):
|
||||
dependencies:
|
||||
chart.js: 4.5.1
|
||||
date-fns: 4.1.0
|
||||
|
||||
chartjs-plugin-annotation@3.1.0(chart.js@4.5.1):
|
||||
dependencies:
|
||||
chart.js: 4.5.1
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -2005,6 +2134,14 @@ snapshots:
|
||||
|
||||
cookie@0.6.0: {}
|
||||
|
||||
core-js@3.49.0: {}
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -2013,6 +2150,24 @@ snapshots:
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
d3-array@3.2.4:
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
|
||||
d3-format@3.1.2: {}
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
dependencies:
|
||||
d3-time: 3.1.0
|
||||
|
||||
d3-time@3.1.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -2232,6 +2387,8 @@ snapshots:
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
@@ -2330,6 +2487,10 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lucide-svelte@1.0.1(svelte@5.38.8):
|
||||
dependencies:
|
||||
svelte: 5.38.8
|
||||
|
||||
magic-string@0.30.19:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -2369,6 +2530,10 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -2450,6 +2615,8 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
ramda@0.27.2: {}
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -2579,6 +2746,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.9.2):
|
||||
dependencies:
|
||||
typescript: 5.9.2
|
||||
@@ -2604,6 +2773,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-search-params-polyfill@7.0.1: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
@@ -2630,6 +2801,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 7.1.5(jiti@2.5.1)(lightningcss@1.30.1)
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
+365
-122
@@ -2,127 +2,213 @@
|
||||
@plugin '@tailwindcss/forms';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
/* ── Design tokens ─────────────────────────────────────── */
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--surface-raised: #22263a;
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--text: #e8eaf0;
|
||||
--text-muted: #7a7f96;
|
||||
--accent: #f97316;
|
||||
--accent-dim: rgba(249, 115, 22, 0.15);
|
||||
--green: #22c55e;
|
||||
--red: #ef4444;
|
||||
--radius: 14px;
|
||||
}
|
||||
|
||||
[data-theme='light'] {
|
||||
--bg: #f4f5f7;
|
||||
--surface: #ffffff;
|
||||
--surface-raised: #eef0f4;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--text: #1a1d27;
|
||||
--text-muted: #6b7280;
|
||||
--accent-dim: rgba(249, 115, 22, 0.12);
|
||||
}
|
||||
|
||||
/* ── Base ──────────────────────────────────────────────── */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family:
|
||||
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
margin: 0
|
||||
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji';
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Nav ───────────────────────────────────────────────── */
|
||||
.nav {
|
||||
background-color: black;
|
||||
color: white;
|
||||
width: full;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: xx-large;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
margin-top: 15px;
|
||||
.dark-mode-btn {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.graph {
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0.4rem;
|
||||
transition:
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.plot-wrapper {
|
||||
height: 400px;
|
||||
.dark-mode-btn:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Layout ────────────────────────────────────────────── */
|
||||
.content {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1rem 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* ── Status cards ──────────────────────────────────────── */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.card-grid > :nth-child(3) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.1rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 25%;
|
||||
padding: 1em;
|
||||
min-height: 120px;
|
||||
box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5);
|
||||
border-radius: 10px;
|
||||
gap: 0.25rem;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.reading {
|
||||
font-weight: bold;
|
||||
margin-top: 18px;
|
||||
.card-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
||||
@property --ovenColor1 {
|
||||
syntax: '<color>';
|
||||
initial-value: rgba(255, 0, 0, 0);
|
||||
inherits: false;
|
||||
.card-value {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
|
||||
@property --ovenColor2 {
|
||||
syntax: '<color>';
|
||||
initial-value: rgba(255, 0, 0, 0);
|
||||
inherits: false;
|
||||
.card-unit {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
|
||||
.oven {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
.card-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.oven-wrapper {
|
||||
background: radial-gradient(var(--ovenColor1), var(--ovenColor2), rgba(255, 0, 0, 0), rgba(255, 0, 0, 0));
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
padding: 30px;
|
||||
transition: --ovenColor1 3s, --ovenColor2 3s;
|
||||
.card-target {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
opacity: 0.8;
|
||||
margin-top: 0.25rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.oven.on .oven-wrapper {
|
||||
--ovenColor1: rgb(255, 60, 0);
|
||||
--ovenColor2: rgba(255, 60, 0, 0.4);
|
||||
@keyframes extreme-blink {
|
||||
0%,
|
||||
100% {
|
||||
color: #ef4444;
|
||||
}
|
||||
50% {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.oven .oven-img-wrapper {
|
||||
width: 65%;
|
||||
.temp-extreme {
|
||||
animation: extreme-blink 0.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.oven img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.relais {
|
||||
/* ── Relay card ────────────────────────────────────────── */
|
||||
.relay-card {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: var(--surface);
|
||||
transition:
|
||||
background 0.3s,
|
||||
box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.relay-card:hover {
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
|
||||
.relay-on {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--red),
|
||||
0 0 20px rgba(239, 68, 68, 0.25);
|
||||
}
|
||||
.relay-off {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ON/OFF toggle switch (visual-only inside the relay button) */
|
||||
.onoffswitch {
|
||||
pointer-events: none;
|
||||
margin-top: 10px;
|
||||
margin: 0.5rem 0 0.15rem;
|
||||
position: relative;
|
||||
width: 90px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.onoffswitch-checkbox {
|
||||
@@ -134,8 +220,7 @@ p {
|
||||
.onoffswitch-label {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 2px solid #999999;
|
||||
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
@@ -143,88 +228,246 @@ p {
|
||||
display: block;
|
||||
width: 200%;
|
||||
margin-left: -100%;
|
||||
transition: margin 0.3s ease-in 0s;
|
||||
transition: margin 0.25s ease-in;
|
||||
}
|
||||
|
||||
.onoffswitch-inner:before,
|
||||
.onoffswitch-inner:after {
|
||||
.onoffswitch-inner::before,
|
||||
.onoffswitch-inner::after {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 50%;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
font-family: Trebuchet, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onoffswitch-inner:before {
|
||||
content: "ON";
|
||||
background-color: #C43535;
|
||||
color: #FFFFFF;
|
||||
.onoffswitch-inner::before {
|
||||
content: 'ON';
|
||||
background-color: #c43535;
|
||||
color: #ffffff;
|
||||
padding-right: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.onoffswitch-inner:after {
|
||||
content: "OFF";
|
||||
padding-left: 10px;
|
||||
.onoffswitch-inner::after {
|
||||
content: 'OFF';
|
||||
background-color: #444;
|
||||
color: #aaa;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
background-color: #EEEEEE;
|
||||
color: #999999;
|
||||
text-align: center;
|
||||
[data-theme='light'] .onoffswitch-inner::after {
|
||||
background-color: #d1d5db;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.onoffswitch-switch {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 6px;
|
||||
background: #FFFFFF;
|
||||
margin: 4px;
|
||||
margin-left: 3px;
|
||||
background: #ffffff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 2px;
|
||||
bottom: 0;
|
||||
right: 56px;
|
||||
border: 2px solid #999999;
|
||||
border-radius: 20px;
|
||||
transition: all 0.3s ease-in 0s;
|
||||
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
transition: right 0.25s ease-in;
|
||||
}
|
||||
|
||||
.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-inner {
|
||||
.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch {
|
||||
right: 0px;
|
||||
.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch {
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.settings {
|
||||
margin-bottom: 200px;
|
||||
margin-top: 50px;
|
||||
/* ── Oven graphic ──────────────────────────────────────── */
|
||||
@property --ovenColor1 {
|
||||
syntax: '<color>';
|
||||
initial-value: rgba(255, 0, 0, 0);
|
||||
inherits: false;
|
||||
}
|
||||
@property --ovenColor2 {
|
||||
syntax: '<color>';
|
||||
initial-value: rgba(255, 0, 0, 0);
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
.settings input {
|
||||
max-width: 50px;
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
.oven {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.oven-wrapper {
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
var(--ovenColor1) 0%,
|
||||
var(--ovenColor2) 30%,
|
||||
transparent 68%
|
||||
);
|
||||
width: 60%;
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 3.5rem;
|
||||
transition:
|
||||
--ovenColor1 2.5s,
|
||||
--ovenColor2 2.5s;
|
||||
}
|
||||
|
||||
.oven.on .oven-wrapper {
|
||||
--ovenColor1: rgba(255, 110, 0, 0.85);
|
||||
--ovenColor2: rgba(255, 60, 0, 0.4);
|
||||
}
|
||||
|
||||
.oven-img-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.oven img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Chart ─────────────────────────────────────────────── */
|
||||
.chart-section {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.chart-section canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* ── Controls grid ─────────────────────────────────────── */
|
||||
.controls-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.controls-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.control-card {
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 1.1rem 1.1rem 1.2rem;
|
||||
}
|
||||
|
||||
/* ── Inputs ────────────────────────────────────────────── */
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.num-input {
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
padding: 0.35rem 0.5rem;
|
||||
width: 5.5rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.num-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.input-unit {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mode-select {
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.mode-select:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Sub-controls (PWM / PID params) ───────────────────── */
|
||||
.subcontrol-grid {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subcontrol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.sublabel {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.subinfo {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Action buttons ────────────────────────────────────── */
|
||||
.action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
padding: 0.45rem 0.9rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Utility ───────────────────────────────────────────── */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mode_span::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.chart div {
|
||||
font: 10px sans-serif;
|
||||
background-color: steelblue;
|
||||
text-align: right;
|
||||
padding: 3px;
|
||||
margin: 1px;
|
||||
color: white;
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* SpoofSocket – a fake WebSocket that simulates the ESP32 firmware.
|
||||
* Used in DEV mode only. Cast to WebSocket with `as unknown as WebSocket`.
|
||||
*
|
||||
* Thermal model:
|
||||
* – S-curve heating: rate peaks at ~50% of the target range (fast in the
|
||||
* middle, slow at start/end), matching how a real oven behaves.
|
||||
* – Newton's law of cooling when relay is off.
|
||||
* – Small Gaussian noise on each tick for realism.
|
||||
*
|
||||
* Control modes mirror the firmware: manual, pwm, pid.
|
||||
*/
|
||||
|
||||
const TICK_MS = 500; // matches WS_INTERVAL on the firmware
|
||||
const ROOM_TEMP = 22; // °C ambient
|
||||
const MAX_HEAT_RATE = 14; // °C/s at peak of S-curve
|
||||
const NOISE_AMP = 0.35; // °C peak random noise
|
||||
|
||||
/** S-curve heating factor: parabolic 4x(1-x), peaks at x = 0.5. */
|
||||
function heatFactor(temp: number, target: number): number {
|
||||
if (target <= ROOM_TEMP) return 0;
|
||||
const x = Math.max(0, Math.min(1, (temp - ROOM_TEMP) / (target - ROOM_TEMP)));
|
||||
return 4 * x * (1 - x) + 0.02; // +0.02 so the oven starts moving immediately
|
||||
}
|
||||
|
||||
/** Newton's law of cooling – rate proportional to excess over ambient. */
|
||||
function coolingRate(temp: number): number {
|
||||
return (1.4 * Math.max(0, temp - ROOM_TEMP)) / 300;
|
||||
}
|
||||
|
||||
type SensorPayload = {
|
||||
mode: string;
|
||||
temperature: number;
|
||||
relais: number;
|
||||
target_temp: number;
|
||||
pwm_on: number;
|
||||
pwm_off: number;
|
||||
pid: number;
|
||||
kp: number;
|
||||
ki: number;
|
||||
kd: number;
|
||||
};
|
||||
|
||||
type Command = { cmd: string; value?: number | string };
|
||||
|
||||
export class SpoofSocket {
|
||||
onopen: (() => void) | null = null;
|
||||
onclose: (() => void) | null = null;
|
||||
onmessage: ((e: { data: string }) => void) | null = null;
|
||||
readyState: number = WebSocket.CONNECTING;
|
||||
|
||||
private s: SensorPayload = {
|
||||
mode: 'manual',
|
||||
temperature: ROOM_TEMP,
|
||||
relais: 0,
|
||||
target_temp: 460,
|
||||
pwm_on: 2000,
|
||||
pwm_off: 4000,
|
||||
pid: 0,
|
||||
kp: 0.6,
|
||||
ki: 0.1,
|
||||
kd: 0.0
|
||||
};
|
||||
|
||||
private integral = 0;
|
||||
private lastSwitch = Date.now();
|
||||
private ticker: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor() {
|
||||
// Simulate async connection handshake
|
||||
setTimeout(() => {
|
||||
this.readyState = WebSocket.OPEN;
|
||||
this.onopen?.();
|
||||
this.ticker = setInterval(() => this.tick(), TICK_MS);
|
||||
}, 80);
|
||||
}
|
||||
|
||||
send(raw: string): void {
|
||||
try {
|
||||
const { cmd, value } = JSON.parse(raw) as Command;
|
||||
this.handle(cmd, value);
|
||||
} catch {
|
||||
// ignore malformed messages
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop the simulation ticker without triggering onclose (for cleanup). */
|
||||
destroy(): void {
|
||||
if (this.ticker !== null) {
|
||||
clearInterval(this.ticker);
|
||||
this.ticker = null;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.destroy();
|
||||
this.readyState = WebSocket.CLOSED;
|
||||
this.onclose?.();
|
||||
}
|
||||
|
||||
private handle(cmd: string, value?: number | string): void {
|
||||
const s = this.s;
|
||||
switch (cmd) {
|
||||
case 'getReadings':
|
||||
this.emit();
|
||||
break;
|
||||
case 'setTargetTemp':
|
||||
s.target_temp = Number(value);
|
||||
this.integral = 0; // reset integral on setpoint change
|
||||
break;
|
||||
case 'setMode':
|
||||
s.mode = String(value);
|
||||
if (s.mode === 'manual') s.relais = 0;
|
||||
this.integral = 0;
|
||||
break;
|
||||
case 'switchRelais':
|
||||
if (s.mode === 'manual') {
|
||||
s.relais = s.relais ? 0 : 1;
|
||||
this.lastSwitch = Date.now();
|
||||
}
|
||||
break;
|
||||
case 'setPWMOn':
|
||||
s.pwm_on = Number(value) * 1000;
|
||||
break;
|
||||
case 'setPWMOff':
|
||||
s.pwm_off = Number(value) * 1000;
|
||||
break;
|
||||
case 'setKp':
|
||||
s.kp = Number(value);
|
||||
break;
|
||||
case 'setKi':
|
||||
s.ki = Number(value);
|
||||
break;
|
||||
case 'setKd':
|
||||
s.kd = Number(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private tick(): void {
|
||||
const dt = TICK_MS / 1000;
|
||||
const s = this.s;
|
||||
const now = Date.now();
|
||||
|
||||
// ── relay control (mirrors firmware regulateRelais) ──────────────────
|
||||
if (s.mode === 'pwm') {
|
||||
const elapsed = now - this.lastSwitch;
|
||||
if (s.relais === 1 && elapsed > s.pwm_on) {
|
||||
s.relais = 0;
|
||||
this.lastSwitch = now;
|
||||
} else if (s.relais === 0 && elapsed > s.pwm_off) {
|
||||
s.relais = 1;
|
||||
this.lastSwitch = now;
|
||||
}
|
||||
} else if (s.mode === 'pid') {
|
||||
const error = s.target_temp - s.temperature;
|
||||
this.integral = Math.max(-100, Math.min(100, this.integral + s.ki * error * dt));
|
||||
const output = Math.max(0, Math.min(100, s.kp * error + this.integral));
|
||||
s.pid = output;
|
||||
|
||||
if (output > 0) {
|
||||
const offDelay = s.pwm_on / (output / 100) - s.pwm_on;
|
||||
const elapsed = now - this.lastSwitch;
|
||||
if (s.relais === 1 && offDelay > 0 && elapsed > s.pwm_on) {
|
||||
s.relais = 0;
|
||||
this.lastSwitch = now;
|
||||
} else if (s.relais === 0 && offDelay > 0 && elapsed > offDelay) {
|
||||
s.relais = 1;
|
||||
this.lastSwitch = now;
|
||||
}
|
||||
} else {
|
||||
s.relais = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── thermal model ────────────────────────────────────────────────────
|
||||
if (s.relais) {
|
||||
s.temperature += MAX_HEAT_RATE * heatFactor(s.temperature, s.target_temp) * dt;
|
||||
} else {
|
||||
s.temperature -= coolingRate(s.temperature) * dt;
|
||||
}
|
||||
s.temperature = Math.max(ROOM_TEMP, s.temperature);
|
||||
s.temperature += (Math.random() - 0.5) * NOISE_AMP;
|
||||
|
||||
this.emit();
|
||||
}
|
||||
|
||||
private emit(): void {
|
||||
this.onmessage?.({ data: JSON.stringify(this.s) });
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
export const pad = (n: string | number, amount = 2) => {
|
||||
if (amount === 2) {
|
||||
return ('0' + n).slice(-2);
|
||||
} else {
|
||||
return ('00' + n).slice(-3);
|
||||
}
|
||||
return String(n).padStart(amount, '0');
|
||||
};
|
||||
|
||||
+90
-63
@@ -10,53 +10,62 @@
|
||||
#include "secrets.h"
|
||||
#include "pid.h"
|
||||
|
||||
// #define RELAY 21;
|
||||
// #define THERMO_DO 19;
|
||||
// #define THERMO_CS 23;
|
||||
// #define THERMO_CLK 5;
|
||||
|
||||
AsyncWebServer server(80);
|
||||
AsyncWebSocket ws("/ws");
|
||||
|
||||
// Pin assignments
|
||||
constexpr int relay = 21;
|
||||
constexpr int thermoDO = 19;
|
||||
constexpr int thermoCS = 23;
|
||||
constexpr int thermoCLK = 5;
|
||||
|
||||
// Timer variables
|
||||
unsigned long lastSensor = 0;
|
||||
unsigned long SENSOR_INTERVAL = 100;
|
||||
const unsigned long SENSOR_INTERVAL = 100;
|
||||
|
||||
unsigned long lastWebSocket = 0;
|
||||
unsigned long WS_INTERVAL = 500;
|
||||
|
||||
int relay = 21;
|
||||
int thermoDO = 19;
|
||||
int thermoCS = 23;
|
||||
int thermoCLK = 5;
|
||||
const unsigned long WS_INTERVAL = 500;
|
||||
|
||||
float temperature = 0;
|
||||
int targetTemp = 300;
|
||||
float targetTemp = 460;
|
||||
|
||||
unsigned long lastSwitch = 0;
|
||||
float pwmSwitchDelayOn = 2000; // 2s
|
||||
float pwmSwitchDelayOff = 4000; // 4s
|
||||
|
||||
float power = 0;
|
||||
float power = 100;
|
||||
|
||||
float kp = 0.6;
|
||||
float ki = 0.1;
|
||||
float kd = 0.0;
|
||||
|
||||
String mode = "manual";
|
||||
enum class Mode { MANUAL, PWM, PID };
|
||||
Mode mode = Mode::MANUAL;
|
||||
|
||||
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
|
||||
|
||||
PIDController pid(kp, ki, kd, 100.0, 0.0, 100.0); // Kp, Ki, Kd, max, min, max_integral
|
||||
|
||||
static const char *modeToStr(Mode m)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case Mode::PWM:
|
||||
return "pwm";
|
||||
case Mode::PID:
|
||||
return "pid";
|
||||
default:
|
||||
return "manual";
|
||||
}
|
||||
}
|
||||
|
||||
// Get Sensor Readings and return JSON object
|
||||
String getSensorReadings()
|
||||
|
||||
{
|
||||
JsonDocument readings;
|
||||
String jsonString;
|
||||
|
||||
readings["mode"] = mode;
|
||||
readings["mode"] = modeToStr(mode);
|
||||
|
||||
temperature = thermocouple.readCelsius();
|
||||
readings["temperature"] = temperature;
|
||||
@@ -95,8 +104,18 @@ void initWiFi()
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
Serial.print("Connecting to WiFi ..");
|
||||
unsigned long startAttemptTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED)
|
||||
{
|
||||
if (millis() - startAttemptTime > 10000)
|
||||
{
|
||||
Serial.println("Wifi not found, starting AP mode");
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(ap_ssid, ap_password);
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
break;
|
||||
}
|
||||
Serial.print('.');
|
||||
delay(SENSOR_INTERVAL);
|
||||
yield();
|
||||
@@ -115,20 +134,30 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
|
||||
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
|
||||
{
|
||||
data[len] = 0;
|
||||
String message = (char *)data;
|
||||
if (message.startsWith("getReadings"))
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, (char *)data);
|
||||
if (err)
|
||||
{
|
||||
String sensorReadings = getSensorReadings();
|
||||
notifyClients(sensorReadings);
|
||||
};
|
||||
if (message.startsWith("switchRelais"))
|
||||
Serial.printf("WS JSON parse error: %s\n", err.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const char *cmd = doc["cmd"];
|
||||
if (!cmd)
|
||||
return;
|
||||
|
||||
if (strcmp(cmd, "getReadings") == 0)
|
||||
{
|
||||
notifyClients(getSensorReadings());
|
||||
}
|
||||
else if (strcmp(cmd, "switchRelais") == 0)
|
||||
{
|
||||
Serial.printf("\nSwitch Relais");
|
||||
if (digitalRead(relay) == HIGH)
|
||||
{
|
||||
digitalWrite(relay, LOW);
|
||||
lastSwitch = millis();
|
||||
mode = "manual";
|
||||
mode = Mode::MANUAL;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -136,68 +165,55 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
|
||||
lastSwitch = millis();
|
||||
}
|
||||
}
|
||||
if (message.startsWith("setTargetTemp"))
|
||||
else if (strcmp(cmd, "setTargetTemp") == 0)
|
||||
{
|
||||
String target = message.substring(15, 18);
|
||||
targetTemp = target.toFloat();
|
||||
targetTemp = doc["value"].as<float>();
|
||||
pid.setSetpoint(targetTemp);
|
||||
Serial.printf("\nTarget temp set to ");
|
||||
Serial.print(targetTemp);
|
||||
Serial.printf("°C");
|
||||
Serial.printf("\nTarget temp set to %.1f°C", targetTemp);
|
||||
}
|
||||
if (message.startsWith("setMode"))
|
||||
else if (strcmp(cmd, "setMode") == 0)
|
||||
{
|
||||
if (message.indexOf("pwm") > 0)
|
||||
const char *value = doc["value"];
|
||||
if (value && strcmp(value, "pwm") == 0)
|
||||
{
|
||||
mode = "pwm";
|
||||
mode = Mode::PWM;
|
||||
}
|
||||
else if (message.indexOf("pid") > 0)
|
||||
else if (value && strcmp(value, "pid") == 0)
|
||||
{
|
||||
mode = "pid";
|
||||
mode = Mode::PID;
|
||||
}
|
||||
else
|
||||
{
|
||||
mode = "manual";
|
||||
mode = Mode::MANUAL;
|
||||
digitalWrite(relay, LOW);
|
||||
lastSwitch = millis();
|
||||
}
|
||||
Serial.printf("\nMode set to ");
|
||||
Serial.print(mode);
|
||||
Serial.printf("\nMode set to %s", modeToStr(mode));
|
||||
}
|
||||
if (message.startsWith("setKp"))
|
||||
else if (strcmp(cmd, "setKp") == 0)
|
||||
{
|
||||
String k = message.substring(6, 12);
|
||||
kp = k.toFloat();
|
||||
kp = doc["value"].as<float>();
|
||||
pid.setSetKp(kp);
|
||||
}
|
||||
if (message.startsWith("setKi"))
|
||||
else if (strcmp(cmd, "setKi") == 0)
|
||||
{
|
||||
String k = message.substring(6, 12);
|
||||
ki = k.toFloat();
|
||||
ki = doc["value"].as<float>();
|
||||
pid.setSetKi(ki);
|
||||
}
|
||||
if (message.startsWith("setKd"))
|
||||
else if (strcmp(cmd, "setKd") == 0)
|
||||
{
|
||||
String k = message.substring(6, 12);
|
||||
kd = k.toFloat();
|
||||
kd = doc["value"].as<float>();
|
||||
pid.setSetKd(kd);
|
||||
}
|
||||
if (message.startsWith("setPWMOn"))
|
||||
else if (strcmp(cmd, "setPWMOn") == 0)
|
||||
{
|
||||
String target = message.substring(10, 15);
|
||||
pwmSwitchDelayOn = target.toFloat() * 1000;
|
||||
Serial.printf("\nPWM switch ON set to ");
|
||||
Serial.print(pwmSwitchDelayOn);
|
||||
Serial.printf("°C");
|
||||
pwmSwitchDelayOn = doc["value"].as<float>() * 1000;
|
||||
Serial.printf("\nPWM switch ON set to %.0fms", pwmSwitchDelayOn);
|
||||
}
|
||||
|
||||
if (message.startsWith("setPWMOff"))
|
||||
else if (strcmp(cmd, "setPWMOff") == 0)
|
||||
{
|
||||
String target = message.substring(11, 16);
|
||||
pwmSwitchDelayOff = target.toFloat() * 1000;
|
||||
Serial.printf("\nPWM switch OFF set to ");
|
||||
Serial.print(pwmSwitchDelayOff);
|
||||
Serial.printf("°C");
|
||||
pwmSwitchDelayOff = doc["value"].as<float>() * 1000;
|
||||
Serial.printf("\nPWM switch OFF set to %.0fms", pwmSwitchDelayOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +245,7 @@ void initWebSocket()
|
||||
|
||||
void regulateRelais()
|
||||
{
|
||||
if (mode == "pwm")
|
||||
if (mode == Mode::PWM)
|
||||
{
|
||||
if ((millis() - lastSwitch) > pwmSwitchDelayOn)
|
||||
{
|
||||
@@ -249,11 +265,20 @@ void regulateRelais()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mode == "pid")
|
||||
if (mode == Mode::PID)
|
||||
{
|
||||
// output = Kp×error + Ki×∫error×dt + Kd×(Δerror/Δt)
|
||||
power = pid.compute(temperature);
|
||||
pwmSwitchDelayOff = (pwmSwitchDelayOn / (power / 100)) - pwmSwitchDelayOn;
|
||||
if (power > 0.0f)
|
||||
{
|
||||
pwmSwitchDelayOff = (pwmSwitchDelayOn / (power / 100.0f)) - pwmSwitchDelayOn;
|
||||
}
|
||||
else
|
||||
{
|
||||
pwmSwitchDelayOff = 0.0f;
|
||||
digitalWrite(relay, LOW);
|
||||
lastSwitch = millis();
|
||||
}
|
||||
|
||||
if (pwmSwitchDelayOff > 0)
|
||||
{
|
||||
@@ -291,6 +316,8 @@ void setup()
|
||||
initLittleFS();
|
||||
initWebSocket();
|
||||
|
||||
digitalWrite(relay, HIGH);
|
||||
|
||||
// Web Server Root URL
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{ request->send(LittleFS, "/index.html", "text/html"); });
|
||||
|
||||
+397
-490
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
// Replace with your network credentials
|
||||
const char *ssid = "WIFI_SSID";
|
||||
const char *password = "WIFI_PWD";
|
||||
const char *ap_ssid = "CrustCraft";
|
||||
const char *ap_password = "crustcr4ft";
|
||||
Reference in New Issue
Block a user