From f3445ba61365622c6b2b176a7dfa8af5bd68f7df Mon Sep 17 00:00:00 2001 From: wille Date: Thu, 1 Jan 2026 16:32:11 +0100 Subject: [PATCH 1/2] Web application built with vite --- .gitignore | 4 + data/assets/app.css.gz | Bin 2537 -> 0 bytes data/assets/app.js.gz | Bin 8290 -> 0 bytes data/index.html.gz | Bin 4250 -> 0 bytes src/network/server.cpp | 8 + {data => web}/index.html | 4 +- web/package-lock.json | 1180 ++++++++++++++++++++++++++++++ web/package.json | 19 + {data/assets => web/src}/app.css | 0 web/src/app.js | 1058 +++++++++++++++++++++++++++ web/vite.config.js | 27 + 11 files changed, 2298 insertions(+), 2 deletions(-) delete mode 100644 data/assets/app.css.gz delete mode 100644 data/assets/app.js.gz delete mode 100644 data/index.html.gz rename {data => web}/index.html (99%) create mode 100644 web/package-lock.json create mode 100644 web/package.json rename {data/assets => web/src}/app.css (100%) create mode 100644 web/src/app.js create mode 100644 web/vite.config.js diff --git a/.gitignore b/.gitignore index 2c35b56..292e133 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ src/secrets.h # macOS .DS_Store + +node_modules/ +.vite/ +dist/ \ No newline at end of file diff --git a/data/assets/app.css.gz b/data/assets/app.css.gz deleted file mode 100644 index 74f10cc5da4973c09f94fbb482d5d3beb876ed98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2537 zcmVXdA|6y=&E@N|Z0Nq<_j^oA={-0A2V;G*-V)3m|4lzD>mNSP3j8L<@LpOG-wLh`j)P6>pc>#zMacx!$a>;XacDb1v!2WWB~J z9WzGd)i}F{mr6Y$R**ufI9os9NZXyElvJ?!wtT*jKArzM|M6pr1Pa9gAMv+0IoTh? z%}-`iX7$Tr%IpDj=`f}J;_;2kdvQlQloWHzMErA(-(zlMhg*U=ty!HDnXqb4j84{?zO`sIK{9>f(wC}X@4-Q5cIeUmiMXvgFDn5hjJna zg}oWhkswcajD&}U$wR==ut|WLcxp|si6W=`_Z};>5|oMnnvXOw;_jnO%!QB9ypErt zp!`l_{nAG1J7oVd` zv$FZ5tZW_jUh~)Ptmu@K@G?gb0dnfu3wi z9v)@u9-Ty9U(}NKX9{1voV(F;uGPfyvd+v8S}h=tP?pnE2#Ogd>w(@4DRx>GZFOo) z_l_W|c?U0{5D#N4P8b&xT}>?;B1_qDUSUXIT|wC{@LAZd2|yP7)O`P*Z*Ycrv%g)p(^ z0*Q);F`95Z_sCe}$%WyGNuvDkD>Un@@Ey`ii;frH>jm}wZHyU?tT%Il4gZP!0q!a2<{*?$JV7&faRg1e8sE&5mH6v@MFyt0K4;969W zBWg}fS>+JG7CazOj1^$1k0#;1d&DmpLr2Wp3x+z3zMj>25!g~lA4GsNEczSC*Lp~g z=Ij0nH&b4cLJj4Nf+O}TL`FdmP$H&yf2T4R)VOxF(N-JFatHyV;>7=^PPr5f7GS`V zUv0Pct122sitNemi_LY}U3y6s zv=TDWn#%BgY1v9sv+~!NwIy~LO)9P$K>fBeL-W0wVtQP=8*Il6?sH}_F2ju)ppq3N zLAf2$-ujTPzO#Xz_E?(*c>$4)r>r@dov$?ljI;(xZghAE(<}hkorX@PyOv>W598t{HWOJ94F^ zJtGP*D8GSda|F7;@`zT)0wi5zO#W-?;g-HaAr^&=KPl>Q)aUB5YXP_R5lNs(KC}&MEOK%U; z@~yLg)s>7~BQwir48JzJP8^H6SlZ38jOVC*s0-Pz-}j`-q0wEPts7Kpq4AOCn;i@~I&29VE2+h>st!Fv*He!1na5OaxtbEY8w;>zoY0j#mXeKKIZ@&yxKH08JX+6sE zpRM6#-5DEwxHuREzx9A_3jX@*1ql_~Z@t6LvntJv!)uYbcH;sajexVUVYY80ADN;B zzorH8Zyr~mGZmRQTl!uaIvPE{HL(jJ%gL=U@N6u{)4$ z#@RF+TUQfxu9-%(2tBd8zoBj)eK-1SIechl%hz%>KP6{*qS3(P{k!0=1R9dYf{;=` z$^d5>eBc2pQgEvr3xRp??tQP$gG;7O>dfi?T;M!AAAgf^Bx*VfrF`ZI17$$MIiM#Z zeUu+56$dl2f9BR)Rc|e5l^jfa#)x&tT^vZ^DVSS*^sxVK6GcU5z}s8u+qX1M6kZ84 zj1fakM!9TdBJJk|-JGb^+K0%48Q)*kBQD=~Q0N4PJU@tV*7$mRxL9*XC121HP#?Me zQEgM1GWvljDZspm85({(uKqDT@h@0H-3n~NN+)qlo;6Tz;ZDab! z`Lan3`UDtpEa8h;hqM@eGGaoJ47X~ygL!kqICb30$^ar*L7)EuR#odHc{>0A)p6w> diff --git a/data/assets/app.js.gz b/data/assets/app.js.gz deleted file mode 100644 index b9b44cfdde06f0e64f7773196dc975864db556ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8290 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 diff --git a/src/network/server.cpp b/src/network/server.cpp index 1ef7f99..2ba617a 100644 --- a/src/network/server.cpp +++ b/src/network/server.cpp @@ -53,6 +53,13 @@ static String contentTypeFromPath(const String& path) { return "application/octet-stream"; } +static String cacheControl(const String& path) { + if (path.endsWith(".css")) return "private, max-age=604800, immutable"; + if (path.endsWith(".js")) return "private, max-age=604800, immutable"; + + return "no-cache, max-age=0"; +} + static void appendColorArray(JsonArray& arr, const CRGB& color) { arr.add(color.r); arr.add(color.g); @@ -397,6 +404,7 @@ void setupServer() { } if (LittleFS.exists(path)) { + request->addHeader("Cache-Control", cacheControl(path)); request->send(LittleFS, path, contentTypeFromPath(path)); return; } diff --git a/data/index.html b/web/index.html similarity index 99% rename from data/index.html rename to web/index.html index acdf564..a10f0e6 100644 --- a/data/index.html +++ b/web/index.html @@ -5,7 +5,7 @@ LUME - +
@@ -493,6 +493,6 @@

🤖 AI Assi
- + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..3784fbe --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1180 @@ +{ + "name": "assets", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "assets", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "terser": "^5.44.1", + "vite": "^7.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "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", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "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" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "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.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "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/acorn": { + "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" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..b175b2f --- /dev/null +++ b/web/package.json @@ -0,0 +1,19 @@ +{ + "name": "lume", + "private": true, + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "app.js", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "terser": "^5.44.1", + "vite": "^7.3.0" + } +} diff --git a/data/assets/app.css b/web/src/app.css similarity index 100% rename from data/assets/app.css rename to web/src/app.css diff --git a/web/src/app.js b/web/src/app.js new file mode 100644 index 0000000..24ccdd4 --- /dev/null +++ b/web/src/app.js @@ -0,0 +1,1058 @@ +// Modal management +function openConfigModal() { + document.getElementById("configModal").classList.add("show"); + loadSegmentsConfig(); // Load segments when modal opens +} + +function closeConfigModal() { + document.getElementById("configModal").classList.remove("show"); +} + +// Close modal when clicking outside +document.addEventListener("click", function (e) { + const modal = document.getElementById("configModal"); + if (e.target === modal) { + closeConfigModal(); + } +}); + +// Theme management +function toggleTheme() { + const html = document.documentElement; + const currentTheme = html.getAttribute("data-theme") || "dark"; + const newTheme = currentTheme === "dark" ? "light" : "dark"; + + html.setAttribute("data-theme", newTheme); + localStorage.setItem("theme", newTheme); + + // Update icon + const icon = document.getElementById("themeIcon"); + icon.textContent = newTheme === "dark" ? "🌙" : "☀️"; +} + +// Load theme from localStorage on page load +function loadTheme() { + const savedTheme = localStorage.getItem("theme") || "dark"; + document.documentElement.setAttribute("data-theme", savedTheme); + const icon = document.getElementById("themeIcon"); + icon.textContent = savedTheme === "dark" ? "🌙" : "☀️"; +} + +// Load theme immediately +loadTheme(); + +// Load palette preset (v2 cannot read preset back reliably) +(function loadPalettePreset() { + const saved = localStorage.getItem("palettePreset"); + if (saved) { + const pal = document.getElementById("palette"); + if (pal) pal.value = saved; + selectTileByValue("paletteTiles", saved); + } +})(); + +// State +let sliderBindings = {}; +let effectMetadata = {}; // Map of effect ID -> metadata (usesPalette, usesSpeed, etc.) +let activeSegmentId = 0; // Currently selected segment + +// Segment name helpers (stored in localStorage) +function getSegmentName(segmentId) { + const names = JSON.parse(localStorage.getItem("segmentNames") || "{}"); + return names[segmentId] || null; +} + +function setSegmentName(segmentId, name) { + const names = JSON.parse(localStorage.getItem("segmentNames") || "{}"); + if (name && name.trim()) { + names[segmentId] = name.trim(); + } else { + delete names[segmentId]; + } + localStorage.setItem("segmentNames", JSON.stringify(names)); +} + +// Toast notification +function showToast(message, type = "info") { + const toast = document.getElementById("toast"); + toast.textContent = message; + toast.className = "toast show " + type; + setTimeout(() => toast.classList.remove("show"), 3000); +} + +// API helpers +async function api(endpoint, method = "GET", body = null) { + const options = { + method, + headers: { "Content-Type": "application/json" }, + }; + if (body) options.body = JSON.stringify(body); + + const response = await fetch("/api" + endpoint, options); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); +} + +// v2 API helpers (segments/controller) +const PALETTE_PRESETS = { + rainbow: 0, + lava: 1, + ocean: 2, + party: 3, + forest: 4, + cloud: 5, + heat: 6, +}; + +async function apiV2(path, method = "GET", body = null) { + const options = { + method, + headers: { "Content-Type": "application/json" }, + }; + if (body) options.body = JSON.stringify(body); + + const response = await fetch("/api/v2" + path, options); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); +} + +// WebSocket (optional): server should expose /ws and send {type:'state', controller:{...}, segments:[...]} +let ws = null; +function connectWebSocket() { + try { + const proto = location.protocol === "https:" ? "wss" : "ws"; + ws = new WebSocket(`${proto}://${location.host}/ws`); + ws.onopen = () => console.log("WS connected"); + ws.onclose = () => { + console.log("WS disconnected, retrying..."); + setTimeout(connectWebSocket, 2000); + }; + ws.onmessage = (evt) => { + try { + const msg = JSON.parse(evt.data); + if (msg.type === "state") { + if (msg.controller) applyControllerToUI(msg.controller); + if (msg.segments) { + // Update the currently active segment, not always segment 0 + const activeSeg = msg.segments.find( + (s) => s.id === activeSegmentId + ); + if (activeSeg) applySegmentToUI(activeSeg); + } + } + } catch (e) { + console.warn("WS message parse error", e); + } + }; + } catch (e) { + console.warn("WS init failed", e); + } +} + +function selectActiveSegment(segments) { + // Current UI is single-segment oriented; default to segment 0 if present, else first segment. + if (!Array.isArray(segments) || segments.length === 0) return null; + const s0 = segments.find((s) => s.id === 0); + return s0 || segments[0]; +} + +function applyControllerToUI(controller) { + if (!controller) return; + document.getElementById("powerToggle").checked = controller.power !== false; + // Brightness input is user-controlled only, never updated from server +} + +function applySegmentToUI(seg) { + if (!seg) return; + document.getElementById("effect").value = seg.effect || "rainbow"; + // Speed input is user-controlled only, never updated from server + + // Palette cannot be read back reliably in v2 (see docs); keep last selected in localStorage. + const savedPalette = localStorage.getItem("palettePreset") || "rainbow"; + document.getElementById("palette").value = savedPalette; + + selectTileByValue("effectTiles", seg.effect || "rainbow"); + selectTileByValue("paletteTiles", savedPalette); + + if (seg.primaryColor) { + const [r, g, b] = seg.primaryColor; + document.getElementById("primaryColor").value = rgbToHex(r, g, b); + } + if (seg.secondaryColor) { + const [r, g, b] = seg.secondaryColor; + document.getElementById("secondaryColor").value = rgbToHex(r, g, b); + } +} + +// Load segments into dropdown selector +async function loadSegments() { + try { + const data = await apiV2("/segments"); + const selector = document.getElementById("segmentSelector"); + + if (data.segments && data.segments.length > 0) { + selector.innerHTML = data.segments + .map((seg) => { + const customName = getSegmentName(seg.id); + const label = customName + ? `${customName} (LEDs ${seg.start}-${seg.stop})` + : `Segment ${seg.id} (LEDs ${seg.start}-${seg.stop})`; + return ``; + }) + .join(""); + + // Load the first segment's state + activeSegmentId = data.segments[0].id; + selector.value = activeSegmentId; + await loadSegmentState(activeSegmentId); + } else { + selector.innerHTML = + ''; + } + } catch (e) { + console.error("Failed to load segments:", e); + } +} + +// Switch to a different segment +async function switchSegment(segmentId) { + activeSegmentId = segmentId; + await loadSegmentState(segmentId); +} + +// Load a specific segment's state into UI controls +async function loadSegmentState(segmentId) { + try { + const seg = await apiV2(`/segments/${segmentId}`); + + document.getElementById("effect").value = seg.effect || "rainbow"; + document.getElementById("speed").value = seg.speed || 100; + document.getElementById("speedValue").textContent = seg.speed || 100; + document.getElementById("intensity").value = seg.intensity ?? 128; + document.getElementById("intensityValue").textContent = + seg.intensity ?? 128; + selectTileByValue("effectTiles", seg.effect || "rainbow"); + + const savedPalette = localStorage.getItem("palettePreset") || "rainbow"; + document.getElementById("palette").value = savedPalette; + selectTileByValue("paletteTiles", savedPalette); + + if (seg.primaryColor) { + const [r, g, b] = seg.primaryColor; + document.getElementById("primaryColor").value = rgbToHex(r, g, b); + } + if (seg.secondaryColor) { + const [r, g, b] = seg.secondaryColor; + document.getElementById("secondaryColor").value = rgbToHex(r, g, b); + } + + // Update control visibility based on effect + updateEffectControls(seg.effect); + } catch (e) { + console.error(`Failed to load segment ${segmentId} state:`, e); + } +} + +// v2: Load LED state (controller + segments) +async function loadLedState() { + try { + const state = await apiV2("/segments"); + // Update controller state + document.getElementById("powerToggle").checked = state.power !== false; + document.getElementById("brightness").value = state.brightness ?? 128; + document.getElementById("brightnessValue").textContent = + state.brightness ?? 128; + + // Load segments into dropdown + await loadSegments(); + } catch (e) { + console.error("Failed to load LED state (v2):", e); + } +} + +// Load effect metadata from API +async function loadEffectMetadata() { + try { + const data = await apiV2("/effects"); + if (data.effects) { + data.effects.forEach((effect) => { + effectMetadata[effect.id] = { + usesPalette: effect.usesPalette, + usesPrimaryColor: effect.usesPrimaryColor, + usesSecondaryColor: effect.usesSecondaryColor, + usesSpeed: effect.usesSpeed, + usesIntensity: effect.usesIntensity, + }; + }); + } + } catch (e) { + console.error("Failed to load effect metadata:", e); + } +} + +// Update control visibility based on selected effect +function updateEffectControls(effectId) { + const metadata = effectMetadata[effectId]; + if (!metadata) return; // Metadata not loaded yet or effect not found + + // Palette controls + const paletteSection = document.querySelector(".palette-section"); + if (paletteSection) { + paletteSection.style.display = metadata.usesPalette ? "" : "none"; + } + + // Speed controls + const speedControl = document.querySelector(".speed-control"); + if (speedControl) { + speedControl.style.display = metadata.usesSpeed ? "" : "none"; + } + + // Intensity controls + const intensityControl = document.querySelector(".intensity-control"); + if (intensityControl) { + intensityControl.style.display = metadata.usesIntensity ? "" : "none"; + } + + // Primary color + const primaryColorControl = document.querySelector(".primary-color"); + if (primaryColorControl) { + primaryColorControl.style.display = metadata.usesPrimaryColor ? "" : "none"; + } + + // Secondary color + const secondaryColorControl = document.querySelector(".secondary-color"); + if (secondaryColorControl) { + secondaryColorControl.style.display = metadata.usesSecondaryColor + ? "" + : "none"; + } + + // Hide entire color section if neither color is used + // EXCEPT when custom palette is selected (needs color input) + const colorControls = document.querySelector(".color-controls"); + if (colorControls) { + const selectedPalette = document.getElementById("palette")?.value; + const isCustomPalette = selectedPalette === "custom"; + const usesAnyColor = + metadata.usesPrimaryColor || + metadata.usesSecondaryColor || + (metadata.usesPalette && isCustomPalette); + colorControls.style.display = usesAnyColor ? "" : "none"; + + // Show both colors for custom palette + if (isCustomPalette && metadata.usesPalette) { + if (primaryColorControl) primaryColorControl.style.display = ""; + if (secondaryColorControl) secondaryColorControl.style.display = ""; + } + } +} + +// v2: Apply LED state - updates controller + segment 0 +async function applyLedState() { + const controller = { + power: document.getElementById("powerToggle").checked, + brightness: parseInt(document.getElementById("brightness").value), + }; + + const paletteName = document.getElementById("palette").value; + localStorage.setItem("palettePreset", paletteName); + + const segment = { + effect: document.getElementById("effect").value, + speed: parseInt(document.getElementById("speed").value), + intensity: parseInt(document.getElementById("intensity").value), + primaryColor: hexToRgb(document.getElementById("primaryColor").value), + secondaryColor: hexToRgb(document.getElementById("secondaryColor").value), + palette: PALETTE_PRESETS[paletteName] ?? 0, + }; + + try { + // Update controller + await apiV2("/controller", "PUT", controller); + + // Update active segment + if (activeSegmentId >= 0) { + await apiV2(`/segments/${activeSegmentId}`, "PUT", segment); + showToast("Settings applied!", "success"); + } else { + showToast("No segment selected", "error"); + } + } catch (e) { + showToast("Failed to apply settings (v2)", "error"); + console.error(e); + } +} + +// Load status +async function loadStatus() { + try { + const status = await api("/status"); + + document.getElementById("wifiDot").className = "status-dot"; + document.getElementById("wifiStatus").textContent = + status.wifi || "Connected"; + document.getElementById("ipAddress").textContent = status.ip || "--"; + + const uptime = status.uptime || 0; + const hours = Math.floor(uptime / 3600); + const mins = Math.floor((uptime % 3600) / 60); + document.getElementById("uptime").textContent = `${hours}h ${mins}m`; + + // Update sACN status + const sacn = status.sacn || {}; + const sacnDot = document.getElementById("sacnDot"); + const sacnText = document.getElementById("sacnStatusText"); + if (!sacn.enabled) { + sacnDot.className = "status-dot offline"; + sacnText.textContent = "Not enabled"; + } else if (sacn.receiving) { + sacnDot.className = "status-dot"; + sacnText.textContent = `Receiving (${sacn.packets} pkts, uni ${sacn.universe})`; + } else { + sacnDot.className = "status-dot loading"; + sacnText.textContent = `Waiting for data (uni ${sacn.universe})`; + } + + // Update MQTT status + const mqtt = status.mqtt || {}; + const mqttDot = document.getElementById("mqttDot"); + const mqttText = document.getElementById("mqttStatusText"); + if (!mqtt.enabled) { + mqttDot.className = "status-dot offline"; + mqttText.textContent = "Not enabled"; + } else if (mqtt.connected) { + mqttDot.className = "status-dot"; + mqttText.textContent = `Connected to ${mqtt.broker}`; + } else { + mqttDot.className = "status-dot loading"; + mqttText.textContent = "Connecting..."; + } + } catch (e) { + document.getElementById("wifiDot").className = "status-dot offline"; + document.getElementById("wifiStatus").textContent = "Offline"; + } +} + +// Load LED state (v2) defined above + +// Simple slider handlers - update display on input, apply on release +function setupSlider(inputId, valueId) { + const input = document.getElementById(inputId); + const display = document.getElementById(valueId); + if (!input || !display) return; + + let isDragging = false; + + // Update display while dragging + input.addEventListener("input", () => { + display.textContent = input.value; + }); + + // Mark as dragging + ["pointerdown", "mousedown", "touchstart"].forEach((evt) => { + input.addEventListener(evt, () => { + isDragging = true; + }); + }); + + // Send update when released + ["pointerup", "mouseup", "touchend"].forEach((evt) => { + input.addEventListener(evt, () => { + if (isDragging) { + isDragging = false; + applyLedState(); + } + }); + }); +} + +setupSlider("brightness", "brightnessValue"); +setupSlider("speed", "speedValue"); +setupSlider("intensity", "intensityValue"); + +// Tile selector functions - auto-apply on selection +function selectEffect(tile) { + const container = document.getElementById("effectTiles"); + container + .querySelectorAll(".tile") + .forEach((t) => t.classList.remove("selected")); + tile.classList.add("selected"); + const effectId = tile.dataset.value; + document.getElementById("effect").value = effectId; + updateEffectControls(effectId); + applyLedState(); +} + +function selectPalette(tile) { + localStorage.setItem("palettePreset", tile.dataset.value); + const container = document.getElementById("paletteTiles"); + container + .querySelectorAll(".tile") + .forEach((t) => t.classList.remove("selected")); + tile.classList.add("selected"); + document.getElementById("palette").value = tile.dataset.value; + + // Update control visibility (custom palette needs color pickers) + const currentEffect = document.getElementById("effect").value; + updateEffectControls(currentEffect); + + applyLedState(); +} + +function selectTileByValue(containerId, value) { + const container = document.getElementById(containerId); + if (!container) return; + container.querySelectorAll(".tile").forEach((t) => { + if (t.dataset.value === value) { + t.classList.add("selected"); + } else { + t.classList.remove("selected"); + } + }); +} + +// Apply LED state (v2) defined above + +// Nightlight functions +let nightlightPollInterval = null; + +async function loadNightlightStatus() { + try { + const status = await api("/nightlight"); + updateNightlightUI(status); + } catch (e) { + console.error("Failed to load nightlight status:", e); + } +} + +// Flag to prevent toggle change handler from firing during programmatic updates +let updatingNightlightUI = false; + +function updateNightlightUI(status) { + const isActive = status.active; + const controls = document.getElementById("nightlightControls"); + const toggle = document.getElementById("nightlightToggle"); + + // Prevent change handler from running when we set checked programmatically + updatingNightlightUI = true; + toggle.checked = isActive; + updatingNightlightUI = false; + + document.getElementById("startNightlightBtn").style.display = isActive + ? "none" + : ""; + document.getElementById("stopNightlightBtn").style.display = isActive + ? "" + : "none"; + document.getElementById("nightlightProgress").style.display = isActive + ? "" + : "none"; + + // Expand/collapse controls based on active state + if (isActive) { + controls.classList.add("expanded"); + } else { + controls.classList.remove("expanded"); + } + + if (isActive) { + const progress = Math.round((status.progress || 0) * 100); + document.getElementById("nightlightProgressBar").style.width = + progress + "%"; + document.getElementById("nightlightProgressText").textContent = + progress + "% complete"; + + // Start polling for progress updates + if (!nightlightPollInterval) { + nightlightPollInterval = setInterval(loadNightlightStatus, 2000); + } + } else { + // Stop polling + if (nightlightPollInterval) { + clearInterval(nightlightPollInterval); + nightlightPollInterval = null; + } + } +} + +function toggleNightlightControls(show) { + const controls = document.getElementById("nightlightControls"); + if (show) { + controls.classList.add("expanded"); + } else { + controls.classList.remove("expanded"); + } +} + +async function startNightlight() { + const durationMinutes = parseInt( + document.getElementById("nightlightDuration").value + ); + const targetBrightness = parseInt( + document.getElementById("nightlightTarget").value + ); + + try { + const result = await api("/nightlight", "POST", { + duration: durationMinutes * 60, // Convert to seconds + targetBrightness: targetBrightness, + }); + showToast("Nightlight started!", "success"); + // Wait a moment for server to process, then start polling + setTimeout(() => { + loadNightlightStatus(); + }, 200); + } catch (e) { + showToast("Failed to start nightlight", "error"); + // Reset toggle on error + const toggle = document.getElementById("nightlightToggle"); + updatingNightlightUI = true; + toggle.checked = false; + updatingNightlightUI = false; + } +} + +async function stopNightlight() { + try { + await api("/nightlight/stop", "POST", {}); + showToast("Nightlight stopped", "success"); + // Stop polling immediately + if (nightlightPollInterval) { + clearInterval(nightlightPollInterval); + nightlightPollInterval = null; + } + // Update UI immediately without waiting for server + const toggle = document.getElementById("nightlightToggle"); + const controls = document.getElementById("nightlightControls"); + updatingNightlightUI = true; + toggle.checked = false; + updatingNightlightUI = false; + controls.classList.remove("expanded"); + document.getElementById("startNightlightBtn").style.display = ""; + document.getElementById("stopNightlightBtn").style.display = "none"; + document.getElementById("nightlightProgress").style.display = "none"; + } catch (e) { + showToast("Failed to stop nightlight", "error"); + } +} + +// Load config +async function loadConfig() { + try { + const config = await api("/config"); + + document.getElementById("wifiSSID").value = config.wifiSSID || ""; + document.getElementById("ledCount").value = config.ledCount || 160; + + // AI settings + document.getElementById("aiApiKey").value = + config.aiApiKey && config.aiApiKey !== "****" ? "" : ""; + document.getElementById("aiModel").value = + config.aiModel || "claude-3-5-sonnet-20241022"; + + // sACN settings + const sacnEnabled = config.sacnEnabled || false; + document.getElementById("sacnEnabled").checked = sacnEnabled; + document.getElementById("sacnUniverse").value = config.sacnUniverse || 1; + document.getElementById("sacnStartChannel").value = + config.sacnStartChannel || 1; + toggleSettings("sacnSettings", sacnEnabled); + + // MQTT settings + const mqttEnabled = config.mqttEnabled || false; + document.getElementById("mqttEnabled").checked = mqttEnabled; + document.getElementById("mqttBroker").value = config.mqttBroker || ""; + document.getElementById("mqttPort").value = config.mqttPort || 1883; + document.getElementById("mqttUsername").value = + config.mqttUsername && config.mqttUsername !== "****" + ? config.mqttUsername + : ""; + document.getElementById("mqttPassword").value = + config.mqttPassword && config.mqttPassword !== "****" ? "" : ""; + document.getElementById("mqttTopicPrefix").value = + config.mqttTopicPrefix || "lume"; + toggleSettings("mqttSettings", mqttEnabled); + } catch (e) { + console.error("Failed to load config:", e); + } +} + +// Load segments for config modal +async function loadSegmentsConfig() { + try { + const data = await apiV2("/segments"); + const container = document.getElementById("segmentsList"); + + if (!data.segments || data.segments.length === 0) { + container.innerHTML = + '

No segments configured

'; + return; + } + + container.innerHTML = data.segments + .map((seg) => { + const customName = getSegmentName(seg.id); + const displayName = customName || `Segment ${seg.id}`; + return ` +
+
+
${displayName}
+
+ LEDs ${seg.start}-${seg.stop} (${seg.length + } LEDs) + ${seg.effect ? `• ${seg.effect}` : ""} +
+
+ + ${seg.length > 1 + ? `` + : "" + } + +
+ `; + }) + .join(""); + } catch (e) { + console.error("Failed to load segments:", e); + } +} + +async function editSegmentName(id) { + const currentName = getSegmentName(id) || ""; + const newName = prompt(`Enter name for segment ${id}:`, currentName); + + if (newName !== null) { + setSegmentName(id, newName); + await loadSegmentsConfig(); // Refresh config list + await loadSegments(); // Refresh dropdown + } +} + +async function splitSegment(id, start, length) { + const splitAt = prompt( + `Split segment ${id} at LED position? (${start + 1} to ${start + length - 1 + })` + ); + if (!splitAt) return; + + const splitPos = parseInt(splitAt); + if (isNaN(splitPos) || splitPos <= start || splitPos >= start + length) { + showToast("Invalid split position", "error"); + return; + } + + try { + // Calculate new segment lengths + const firstLength = splitPos - start; + const secondLength = length - firstLength; + + // Delete original segment + await fetch("/api/v2/segments/" + id, { method: "DELETE" }); + + // Create first segment + await apiV2("/segments", "POST", { + start: start, + length: firstLength, + }); + + // Create second segment + await apiV2("/segments", "POST", { + start: splitPos, + length: secondLength, + }); + + showToast("Segment split successfully!", "success"); + loadSegmentsConfig(); + } catch (e) { + showToast("Failed to split segment", "error"); + console.error(e); + } +} + +async function addSegment() { + const start = parseInt(document.getElementById("newSegmentStart").value); + const length = parseInt(document.getElementById("newSegmentLength").value); + + if (isNaN(start) || isNaN(length) || start < 0 || length < 1) { + showToast("Invalid segment range", "error"); + return; + } + + try { + await apiV2("/segments", "POST", { + start: start, + length: length, + }); + showToast("Segment created!", "success"); + loadSegmentsConfig(); + + // Update start field for next segment + document.getElementById("newSegmentStart").value = start + length; + } catch (e) { + showToast("Failed to create segment", "error"); + console.error(e); + } +} + +async function deleteSegmentConfig(id) { + if (!confirm(`Delete segment ${id}?`)) { + return; + } + + try { + await fetch("/api/v2/segments/" + id, { method: "DELETE" }); + showToast("Segment deleted", "success"); + loadSegmentsConfig(); + } catch (e) { + showToast("Failed to delete segment", "error"); + console.error(e); + } +} + +// Save config +async function saveConfig() { + const config = { + wifiSSID: document.getElementById("wifiSSID").value, + wifiPassword: document.getElementById("wifiPassword").value, + ledCount: parseInt(document.getElementById("ledCount").value), + aiApiKey: document.getElementById("aiApiKey").value, + aiModel: document.getElementById("aiModel").value, + sacnEnabled: document.getElementById("sacnEnabled").checked, + sacnUniverse: parseInt(document.getElementById("sacnUniverse").value), + sacnStartChannel: parseInt( + document.getElementById("sacnStartChannel").value + ), + mqttEnabled: document.getElementById("mqttEnabled").checked, + mqttBroker: document.getElementById("mqttBroker").value, + mqttPort: parseInt(document.getElementById("mqttPort").value), + mqttUsername: document.getElementById("mqttUsername").value, + mqttPassword: document.getElementById("mqttPassword").value, + mqttTopicPrefix: document.getElementById("mqttTopicPrefix").value, + }; + + // Don't send masked password/key + if (config.wifiPassword === "") delete config.wifiPassword; + if (config.mqttPassword === "") delete config.mqttPassword; + if (config.aiApiKey === "") delete config.aiApiKey; + + try { + await api("/config", "POST", config); + showToast("Configuration saved!", "success"); + loadConfig(); + } catch (e) { + showToast("Failed to save configuration", "error"); + } +} + +// Scene management +async function loadScenes() { + try { + const scenes = await api("/scenes"); + const container = document.getElementById("scenesList"); + + if (!scenes || scenes.length === 0) { + container.innerHTML = + '

No saved scenes yet

'; + return; + } + + container.innerHTML = scenes + .map( + (scene) => ` +
+ ${escapeHtml( + scene.name + )} +
+ + +
+
+ ` + ) + .join(""); + } catch (e) { + console.error("Failed to load scenes:", e); + } +} + +function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +async function applyScene(id) { + try { + const response = await fetch("/api/scenes/" + id + "/apply", { + method: "POST", + }); + + if (!response.ok) { + const result = await response.json(); + showToast("Failed: " + (result.error || response.status), "error"); + return; + } + + showToast("Scene applied!", "success"); + loadLedState(); + } catch (e) { + showToast("Failed to apply scene: " + e.message, "error"); + } +} + +async function deleteScene(id) { + if (!confirm("Delete this scene?")) { + return; + } + + try { + await fetch("/api/scenes/" + id, { method: "DELETE" }); + showToast("Scene deleted", "success"); + loadScenes(); + } catch (e) { + showToast("Failed to delete scene", "error"); + } +} + +// Color helpers +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16), + ] + : [0, 0, 255]; +} + +function rgbToHex(r, g, b) { + return "#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join(""); +} + +// Toggle settings visibility +function toggleSettings(settingsId, show) { + const settings = document.getElementById(settingsId); + if (!settings) return; + settings.style.maxHeight = show ? "500px" : "0"; +} + +// Event listeners - sliders handled by SliderBinding helpers + +// Color pickers auto-apply on change (when picker closes) +document.getElementById("primaryColor").addEventListener("change", function () { + applyLedState(); +}); + +document + .getElementById("secondaryColor") + .addEventListener("change", function () { + applyLedState(); + }); + +document.getElementById("powerToggle").addEventListener("change", function () { + applyLedState(); +}); + +// Nightlight slider listeners +document + .getElementById("nightlightDuration") + .addEventListener("input", function () { + document.getElementById("nightlightDurationValue").textContent = + this.value + " min"; + }); + +document + .getElementById("nightlightTarget") + .addEventListener("input", function () { + const val = parseInt(this.value); + document.getElementById("nightlightTargetValue").textContent = + val === 0 ? "0 (off)" : val; + }); + +document + .getElementById("nightlightToggle") + .addEventListener("change", async function () { + // Ignore programmatic changes + if (updatingNightlightUI) return; + + if (this.checked) { + // Toggle ON - expand controls to show settings + toggleNightlightControls(true); + } else { + // Toggle OFF - stop if running, otherwise just hide + await stopNightlight(); + } + }); + +// AI Prompt functions +async function sendAIPrompt() { + const prompt = document.getElementById("aiPrompt").value.trim(); + if (!prompt) { + showToast("Please enter a prompt", "error"); + return; + } + + const statusDiv = document.getElementById("aiStatus"); + const statusText = document.getElementById("aiStatusText"); + + statusDiv.style.display = "block"; + statusText.textContent = "Processing your request..."; + + try { + const result = await api("/prompt", "POST", { prompt: prompt }); + + if (result.success) { + showToast("✨ Lights updated!", "success"); + statusText.textContent = result.message || "Applied successfully!"; + setTimeout(() => { + statusDiv.style.display = "none"; + }, 3000); + + // Reload LED state to show changes + await loadLedState(); + } else { + showToast(result.error || "Failed to process prompt", "error"); + statusDiv.style.display = "none"; + } + } catch (e) { + showToast("Error: " + (e.message || "Network error"), "error"); + statusDiv.style.display = "none"; + console.error("AI prompt error:", e); + } +} + +// sACN and MQTT toggle handlers +document.getElementById("sacnEnabled").addEventListener("change", function () { + toggleSettings("sacnSettings", this.checked); +}); + +document.getElementById("mqttEnabled").addEventListener("change", function () { + toggleSettings("mqttSettings", this.checked); +}); + +// Initialize +async function initialize() { + loadStatus(); + await loadEffectMetadata(); // Load effect metadata FIRST + await loadLedState(); // Then load LED state (which needs metadata) + loadConfig(); + loadScenes(); + loadNightlightStatus(); + setInterval(loadStatus, 10000); +} + +initialize(); + +// Start WebSocket (optional) +connectWebSocket(); diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..5fadbca --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + server: { + port: 5173, + open: true, + // Proxy API requests to the ESP32 device during development + proxy: { + '/api': { + target: 'http://lume.local', + changeOrigin: true, + }, + '/ws': { + target: 'ws://lume.local', + ws: true, + }, + }, + }, + + // Resolve assets from the assets directory + resolve: { + alias: { + '@': resolve(__dirname), + }, + }, +}); From 341c65a8e0c584968ee4c86af6e83c847b560c5a Mon Sep 17 00:00:00 2001 From: wille Date: Thu, 1 Jan 2026 16:34:16 +0100 Subject: [PATCH 2/2] Update lockfile --- web/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 3784fbe..9f67330 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "assets", + "name": "lume", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "assets", + "name": "lume", "version": "1.0.0", "license": "ISC", "devDependencies": {