add ap mode, modernize ui, split spoof code

This commit is contained in:
Vincent van der Wal
2026-05-09 09:50:28 +02:00
parent 2f3780dad3
commit ce627ef7d4
9 changed files with 1295 additions and 758 deletions
+11 -19
View File
@@ -12,7 +12,8 @@
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-annotation": "^3.1.0",
"date-fns": "^4.1.0" "date-fns": "^4.1.0",
"lucide-svelte": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",
@@ -736,7 +737,6 @@
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -747,7 +747,6 @@
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
@@ -758,7 +757,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -768,14 +766,12 @@
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.30", "version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@@ -1124,7 +1120,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"acorn": "^8.9.0" "acorn": "^8.9.0"
@@ -1535,7 +1530,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
@@ -1807,7 +1801,6 @@
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@@ -1870,7 +1863,6 @@
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -1880,7 +1872,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -2005,7 +1996,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -2378,7 +2368,6 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/espree": { "node_modules/espree": {
@@ -2416,7 +2405,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
"integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2737,7 +2725,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.6" "@types/estree": "^1.0.6"
@@ -3088,7 +3075,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/locate-path": { "node_modules/locate-path": {
@@ -3128,11 +3114,19 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/magic-string": {
"version": "0.30.19", "version": "0.30.19",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
@@ -3921,7 +3915,6 @@
"version": "5.38.8", "version": "5.38.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.8.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.8.tgz",
"integrity": "sha512-UDpTbM/iuZ4MaMnn4ODB3rf5JKDyPOi5oJcopP0j7YHQ9BuJtsAqsR71r2N6AnJf7ygbalTJU5y8eSWGAQZjlQ==", "integrity": "sha512-UDpTbM/iuZ4MaMnn4ODB3rf5JKDyPOi5oJcopP0j7YHQ9BuJtsAqsR71r2N6AnJf7ygbalTJU5y8eSWGAQZjlQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
@@ -4372,7 +4365,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
} }
} }
+2 -1
View File
@@ -47,6 +47,7 @@
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-annotation": "^3.1.0",
"date-fns": "^4.1.0" "date-fns": "^4.1.0",
"lucide-svelte": "^1.0.1"
} }
} }
+178
View File
@@ -7,6 +7,25 @@ settings:
importers: 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: devDependencies:
'@eslint/compat': '@eslint/compat':
specifier: ^1.2.5 specifier: ^1.2.5
@@ -77,6 +96,9 @@ importers:
packages: packages:
'@cubejs-client/core@1.6.44':
resolution: {integrity: sha512-0J2i2xb4DQMdpZDnOnPUEz28fMgI04dbkePWxsGjCuCNcdr752f+iq9W1DZGzQ0JMdZfnSLmnaBrWWrcNMjATA==}
'@esbuild/aix-ppc64@0.25.9': '@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -316,6 +338,9 @@ packages:
'@jridgewell/trace-mapping@0.3.30': '@jridgewell/trace-mapping@0.3.30':
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -694,6 +719,21 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} 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: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -720,6 +760,12 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'} 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: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -729,6 +775,28 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
hasBin: true 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: debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -920,6 +988,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} 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: is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1053,6 +1125,11 @@ packages:
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 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: magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
@@ -1107,6 +1184,15 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 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: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1261,6 +1347,9 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
ramda@0.27.2:
resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==}
readdirp@4.1.2: readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
@@ -1361,6 +1450,9 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
ts-api-utils@2.1.0: ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'} engines: {node: '>=18.12'}
@@ -1386,6 +1478,9 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-search-params-polyfill@7.0.1:
resolution: {integrity: sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==}
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -1446,6 +1541,12 @@ packages:
vite: vite:
optional: true 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: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1472,6 +1573,19 @@ packages:
snapshots: 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': '@esbuild/aix-ppc64@0.25.9':
optional: true optional: true
@@ -1632,6 +1746,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@kurkle/color@0.3.4': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -1987,6 +2103,19 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.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: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@@ -2005,6 +2134,14 @@ snapshots:
cookie@0.6.0: {} 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: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@@ -2013,6 +2150,24 @@ snapshots:
cssesc@3.0.0: {} 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: debug@4.4.1:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -2232,6 +2387,8 @@ snapshots:
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
internmap@2.0.3: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-glob@4.0.3: is-glob@4.0.3:
@@ -2330,6 +2487,10 @@ snapshots:
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
lucide-svelte@1.0.1(svelte@5.38.8):
dependencies:
svelte: 5.38.8
magic-string@0.30.19: magic-string@0.30.19:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -2369,6 +2530,10 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@@ -2450,6 +2615,8 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
ramda@0.27.2: {}
readdirp@4.1.2: {} readdirp@4.1.2: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
@@ -2579,6 +2746,8 @@ snapshots:
totalist@3.0.1: {} totalist@3.0.1: {}
tr46@0.0.3: {}
ts-api-utils@2.1.0(typescript@5.9.2): ts-api-utils@2.1.0(typescript@5.9.2):
dependencies: dependencies:
typescript: 5.9.2 typescript: 5.9.2
@@ -2604,6 +2773,8 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
url-search-params-polyfill@7.0.1: {}
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@11.1.0: {} uuid@11.1.0: {}
@@ -2630,6 +2801,13 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 7.1.5(jiti@2.5.1)(lightningcss@1.30.1) 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: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
+394 -151
View File
@@ -2,229 +2,472 @@
@plugin '@tailwindcss/forms'; @plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography'; @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, html,
body { body {
font-family: font-family:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji';
'Noto Color Emoji'; background: var(--bg);
margin: 0 color: var(--text);
margin: 0;
min-height: 100dvh;
} }
p { p {
margin: 0; margin: 0;
} }
/* ── Nav ───────────────────────────────────────────────── */
.nav { .nav {
background-color: black; background: var(--surface);
color: white; border-bottom: 1px solid var(--border);
width: full; padding: 0.75rem 1rem;
text-align: center; display: flex;
padding: 1em; align-items: center;
justify-content: center;
position: relative;
} }
.nav-title { .nav-title {
font-size: xx-large; font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.05em;
} }
.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;
align-items: center;
justify-content: center;
padding: 0.4rem;
transition:
color 0.15s,
border-color 0.15s;
}
.dark-mode-btn:hover {
color: var(--accent);
border-color: var(--accent);
}
/* ── Layout ────────────────────────────────────────────── */
.content { .content {
max-width: 1000px; max-width: 960px;
margin: auto; margin: 0 auto;
padding: 1.5rem 1rem 4rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
} }
/* ── Status cards ──────────────────────────────────────── */
.card-grid { .card-grid {
margin-top: 15px; display: grid;
display: flex; grid-template-columns: repeat(3, 1fr);
justify-content: center; gap: 1rem;
align-items: center;
gap: 2em;
} }
.graph { @media (max-width: 540px) {
height: 400px; .card-grid {
overflow: hidden; grid-template-columns: 1fr 1fr;
display: flex; }
justify-content: center; .card-grid > :nth-child(3) {
} grid-column: span 2;
}
.plot-wrapper {
height: 400px;
} }
.card { .card {
display: flex; background: var(--surface);
flex-direction: column; border: 1px solid var(--border);
justify-content: center; border-radius: var(--radius);
align-items: center; padding: 1.1rem 1rem;
min-width: 25%; display: flex;
padding: 1em; flex-direction: column;
min-height: 120px; align-items: center;
box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5); gap: 0.25rem;
border-radius: 10px; transition: box-shadow 0.2s;
} }
.reading { .card-label {
font-weight: bold; font-size: 0.7rem;
margin-top: 18px; font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
} }
.card-value {
@property --ovenColor1 { font-size: 2.2rem;
syntax: '<color>'; font-weight: 700;
initial-value: rgba(255, 0, 0, 0); line-height: 1;
inherits: false; color: var(--text);
} }
.card-unit {
@property --ovenColor2 { font-size: 1rem;
syntax: '<color>'; font-weight: 400;
initial-value: rgba(255, 0, 0, 0); color: var(--text-muted);
inherits: false; margin-left: 0.1em;
} }
.oven { .card-sub {
display: flex; font-size: 0.75rem;
justify-content: center; color: var(--text-muted);
margin: auto; margin-top: 0.1rem;
} }
.oven-wrapper { .card-target {
background: radial-gradient(var(--ovenColor1), var(--ovenColor2), rgba(255, 0, 0, 0), rgba(255, 0, 0, 0)); font-size: 0.9rem;
width: 50%; font-weight: 600;
display: flex; color: var(--accent);
justify-content: center; opacity: 0.8;
overflow: visible; margin-top: 0.25rem;
padding: 30px; letter-spacing: 0.02em;
transition: --ovenColor1 3s, --ovenColor2 3s;
} }
.oven.on .oven-wrapper { @keyframes extreme-blink {
--ovenColor1: rgb(255, 60, 0); 0%,
--ovenColor2: rgba(255, 60, 0, 0.4); 100% {
color: #ef4444;
}
50% {
color: #ffffff;
}
} }
.oven .oven-img-wrapper { .temp-extreme {
width: 65%; animation: extreme-blink 0.6s ease-in-out infinite;
} }
.oven img { /* ── Relay card ────────────────────────────────────────── */
width: 100%; .relay-card {
cursor: pointer;
border: none;
background: var(--surface);
transition:
background 0.3s,
box-shadow 0.3s;
} }
.relais { .relay-card:hover {
cursor: pointer; 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 { .onoffswitch {
pointer-events: none; pointer-events: none;
margin-top: 10px; margin: 0.5rem 0 0.15rem;
position: relative; position: relative;
width: 90px; width: 90px;
-webkit-user-select: none; user-select: none;
-moz-user-select: none;
-ms-user-select: none;
} }
.onoffswitch-checkbox { .onoffswitch-checkbox {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
} }
.onoffswitch-label { .onoffswitch-label {
display: block; display: block;
overflow: hidden; overflow: hidden;
cursor: pointer; border: 2px solid rgba(255, 255, 255, 0.15);
border: 2px solid #999999; border-radius: 20px;
border-radius: 20px;
} }
.onoffswitch-inner { .onoffswitch-inner {
display: block; display: block;
width: 200%; width: 200%;
margin-left: -100%; margin-left: -100%;
transition: margin 0.3s ease-in 0s; transition: margin 0.25s ease-in;
} }
.onoffswitch-inner:before, .onoffswitch-inner::before,
.onoffswitch-inner:after { .onoffswitch-inner::after {
display: block; display: block;
float: left; float: left;
width: 50%; width: 50%;
height: 30px; height: 30px;
padding: 0; line-height: 30px;
line-height: 30px; font-size: 13px;
font-size: 14px; font-weight: 700;
color: white; box-sizing: border-box;
font-family: Trebuchet, Arial, sans-serif; text-align: center;
font-weight: bold;
box-sizing: border-box;
} }
.onoffswitch-inner:before { .onoffswitch-inner::before {
content: "ON"; content: 'ON';
background-color: #C43535; background-color: #c43535;
color: #FFFFFF; color: #ffffff;
padding-right: 8px; padding-right: 8px;
text-align: center;
} }
.onoffswitch-inner:after { .onoffswitch-inner::after {
content: "OFF"; content: 'OFF';
padding-left: 10px; background-color: #444;
color: #aaa;
padding-left: 14px;
}
background-color: #EEEEEE; [data-theme='light'] .onoffswitch-inner::after {
color: #999999; background-color: #d1d5db;
text-align: center; color: #6b7280;
} }
.onoffswitch-switch { .onoffswitch-switch {
display: block; display: block;
width: 22px; width: 22px;
height: 22px; height: 22px;
margin: 6px; margin: 4px;
background: #FFFFFF; margin-left: 3px;
position: absolute; background: #ffffff;
top: 0; position: absolute;
bottom: 0; top: 2px;
right: 56px; bottom: 0;
border: 2px solid #999999; right: 56px;
border-radius: 20px; border: 2px solid rgba(255, 255, 255, 0.15);
transition: all 0.3s ease-in 0s; 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; margin-left: 0;
} }
.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch { .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch {
right: 0px; right: 3px;
} }
.settings { /* ── Oven graphic ──────────────────────────────────────── */
margin-bottom: 200px; @property --ovenColor1 {
margin-top: 50px; 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 { .oven {
max-width: 50px; display: flex;
font-size: larger; justify-content: center;
font-weight: bold;
} }
.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 { .hidden {
display: none; display: none !important;
} }
#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;
}
+191
View File
@@ -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
View File
@@ -1,7 +1,3 @@
export const pad = (n: string | number, amount = 2) => { export const pad = (n: string | number, amount = 2) => {
if (amount === 2) { return String(n).padStart(amount, '0');
return ('0' + n).slice(-2);
} else {
return ('00' + n).slice(-3);
}
}; };
+90 -63
View File
@@ -10,53 +10,62 @@
#include "secrets.h" #include "secrets.h"
#include "pid.h" #include "pid.h"
// #define RELAY 21;
// #define THERMO_DO 19;
// #define THERMO_CS 23;
// #define THERMO_CLK 5;
AsyncWebServer server(80); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws("/ws");
// Pin assignments
constexpr int relay = 21;
constexpr int thermoDO = 19;
constexpr int thermoCS = 23;
constexpr int thermoCLK = 5;
// Timer variables // Timer variables
unsigned long lastSensor = 0; unsigned long lastSensor = 0;
unsigned long SENSOR_INTERVAL = 100; const unsigned long SENSOR_INTERVAL = 100;
unsigned long lastWebSocket = 0; unsigned long lastWebSocket = 0;
unsigned long WS_INTERVAL = 500; const unsigned long WS_INTERVAL = 500;
int relay = 21;
int thermoDO = 19;
int thermoCS = 23;
int thermoCLK = 5;
float temperature = 0; float temperature = 0;
int targetTemp = 300; float targetTemp = 460;
unsigned long lastSwitch = 0; unsigned long lastSwitch = 0;
float pwmSwitchDelayOn = 2000; // 2s float pwmSwitchDelayOn = 2000; // 2s
float pwmSwitchDelayOff = 4000; // 4s float pwmSwitchDelayOff = 4000; // 4s
float power = 0; float power = 100;
float kp = 0.6; float kp = 0.6;
float ki = 0.1; float ki = 0.1;
float kd = 0.0; float kd = 0.0;
String mode = "manual"; enum class Mode { MANUAL, PWM, PID };
Mode mode = Mode::MANUAL;
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO); MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
PIDController pid(kp, ki, kd, 100.0, 0.0, 100.0); // Kp, Ki, Kd, max, min, max_integral 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 // Get Sensor Readings and return JSON object
String getSensorReadings() String getSensorReadings()
{ {
JsonDocument readings; JsonDocument readings;
String jsonString; String jsonString;
readings["mode"] = mode; readings["mode"] = modeToStr(mode);
temperature = thermocouple.readCelsius(); temperature = thermocouple.readCelsius();
readings["temperature"] = temperature; readings["temperature"] = temperature;
@@ -95,8 +104,18 @@ void initWiFi()
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password); WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi .."); Serial.print("Connecting to WiFi ..");
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED) 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('.'); Serial.print('.');
delay(SENSOR_INTERVAL); delay(SENSOR_INTERVAL);
yield(); 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) if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
{ {
data[len] = 0; data[len] = 0;
String message = (char *)data; JsonDocument doc;
if (message.startsWith("getReadings")) DeserializationError err = deserializeJson(doc, (char *)data);
if (err)
{ {
String sensorReadings = getSensorReadings(); Serial.printf("WS JSON parse error: %s\n", err.c_str());
notifyClients(sensorReadings); return;
}; }
if (message.startsWith("switchRelais"))
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"); Serial.printf("\nSwitch Relais");
if (digitalRead(relay) == HIGH) if (digitalRead(relay) == HIGH)
{ {
digitalWrite(relay, LOW); digitalWrite(relay, LOW);
lastSwitch = millis(); lastSwitch = millis();
mode = "manual"; mode = Mode::MANUAL;
} }
else else
{ {
@@ -136,68 +165,55 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
lastSwitch = millis(); lastSwitch = millis();
} }
} }
if (message.startsWith("setTargetTemp")) else if (strcmp(cmd, "setTargetTemp") == 0)
{ {
String target = message.substring(15, 18); targetTemp = doc["value"].as<float>();
targetTemp = target.toFloat();
pid.setSetpoint(targetTemp); pid.setSetpoint(targetTemp);
Serial.printf("\nTarget temp set to "); Serial.printf("\nTarget temp set to %.1f°C", targetTemp);
Serial.print(targetTemp);
Serial.printf("°C");
} }
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 else
{ {
mode = "manual"; mode = Mode::MANUAL;
digitalWrite(relay, LOW); digitalWrite(relay, LOW);
lastSwitch = millis(); lastSwitch = millis();
} }
Serial.printf("\nMode set to "); Serial.printf("\nMode set to %s", modeToStr(mode));
Serial.print(mode);
} }
if (message.startsWith("setKp")) else if (strcmp(cmd, "setKp") == 0)
{ {
String k = message.substring(6, 12); kp = doc["value"].as<float>();
kp = k.toFloat();
pid.setSetKp(kp); pid.setSetKp(kp);
} }
if (message.startsWith("setKi")) else if (strcmp(cmd, "setKi") == 0)
{ {
String k = message.substring(6, 12); ki = doc["value"].as<float>();
ki = k.toFloat();
pid.setSetKi(ki); pid.setSetKi(ki);
} }
if (message.startsWith("setKd")) else if (strcmp(cmd, "setKd") == 0)
{ {
String k = message.substring(6, 12); kd = doc["value"].as<float>();
kd = k.toFloat();
pid.setSetKd(kd); pid.setSetKd(kd);
} }
if (message.startsWith("setPWMOn")) else if (strcmp(cmd, "setPWMOn") == 0)
{ {
String target = message.substring(10, 15); pwmSwitchDelayOn = doc["value"].as<float>() * 1000;
pwmSwitchDelayOn = target.toFloat() * 1000; Serial.printf("\nPWM switch ON set to %.0fms", pwmSwitchDelayOn);
Serial.printf("\nPWM switch ON set to ");
Serial.print(pwmSwitchDelayOn);
Serial.printf("°C");
} }
else if (strcmp(cmd, "setPWMOff") == 0)
if (message.startsWith("setPWMOff"))
{ {
String target = message.substring(11, 16); pwmSwitchDelayOff = doc["value"].as<float>() * 1000;
pwmSwitchDelayOff = target.toFloat() * 1000; Serial.printf("\nPWM switch OFF set to %.0fms", pwmSwitchDelayOff);
Serial.printf("\nPWM switch OFF set to ");
Serial.print(pwmSwitchDelayOff);
Serial.printf("°C");
} }
} }
} }
@@ -229,7 +245,7 @@ void initWebSocket()
void regulateRelais() void regulateRelais()
{ {
if (mode == "pwm") if (mode == Mode::PWM)
{ {
if ((millis() - lastSwitch) > pwmSwitchDelayOn) 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) // output = Kp×error + Ki×∫error×dt + Kd×(Δerror/Δt)
power = pid.compute(temperature); 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) if (pwmSwitchDelayOff > 0)
{ {
@@ -291,6 +316,8 @@ void setup()
initLittleFS(); initLittleFS();
initWebSocket(); initWebSocket();
digitalWrite(relay, HIGH);
// Web Server Root URL // Web Server Root URL
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(LittleFS, "/index.html", "text/html"); }); { request->send(LittleFS, "/index.html", "text/html"); });
+426 -519
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,3 +1,5 @@
// Replace with your network credentials // Replace with your network credentials
const char *ssid = "WIFI_SSID"; const char *ssid = "WIFI_SSID";
const char *password = "WIFI_PWD"; const char *password = "WIFI_PWD";
const char *ap_ssid = "CrustCraft";
const char *ap_password = "crustcr4ft";