completely move off sharp, use monaco for the editor now and use tabulator for tables (much easier and better performance).
This commit is contained in:
+9
-11
@@ -109,9 +109,12 @@ RUN if [ -f dist-clients/BackstageCapture.x64.dll ]; then \
|
||||
echo "WARNING: BackstageCapture DLL not available (build with MSVC on Windows)"; \
|
||||
fi
|
||||
|
||||
# Tailwind CSS + vendored frontend assets
|
||||
RUN bun run build:css && bun run vendor \
|
||||
&& test -s ./public/assets/tailwind.css && test -d ./public/vendor/fontawesome
|
||||
# Tailwind CSS, vendored frontend assets, and bundled Bun server runtime.
|
||||
RUN bun run build \
|
||||
&& test -s ./public/assets/tailwind.css \
|
||||
&& test -d ./public/vendor/fontawesome \
|
||||
&& test -s ./dist/index.js \
|
||||
&& test -s ./dist/server/plugin-runtime/worker-host.js
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -151,10 +154,8 @@ COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
bun install --production --frozen-lockfile
|
||||
|
||||
# Server source.
|
||||
COPY Overlord-Server/ ./
|
||||
|
||||
# Built artifacts from the builder stage (overwrites anything from the source copy).
|
||||
# Built runtime artifacts from the builder stage.
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/dist-clients ./dist-clients
|
||||
|
||||
@@ -180,7 +181,4 @@ ENV NODE_ENV=production
|
||||
ENV OVERLORD_ROOT=/app
|
||||
ENV NODE_PATH=/app/node_modules
|
||||
|
||||
# NOTE: We intentionally do NOT use `bun build --compile` here.
|
||||
# The compiled standalone binary runs from a virtual bunfs filesystem that
|
||||
# cannot reliably load native modules like `sharp` from /app/node_modules.
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
CMD ["bun", "dist/index.js"]
|
||||
|
||||
+12
-59
@@ -12,7 +12,6 @@
|
||||
"geoip-lite": "latest",
|
||||
"jose": "^6.1.3",
|
||||
"msgpackr": "1.11.8",
|
||||
"sharp": "^0.34.5",
|
||||
"uuid": "^9.0.1",
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.11.0",
|
||||
@@ -38,7 +37,9 @@
|
||||
"highlight.js": "11.9.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"leaflet": "1.9.4",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"postcss": "^8.5.6",
|
||||
"tabulator-tables": "^6.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"terser": "^5.46.1",
|
||||
"typescript": "^5.6.3",
|
||||
@@ -52,64 +53,12 @@
|
||||
|
||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||
|
||||
"@fontsource/jetbrains-mono": ["@fontsource/jetbrains-mono@5.2.8", "", {}, "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ=="],
|
||||
|
||||
"@fortawesome/fontawesome-free": ["@fortawesome/fontawesome-free@6.5.2", "", {}, "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -224,6 +173,8 @@
|
||||
|
||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
|
||||
|
||||
"@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="],
|
||||
@@ -310,6 +261,8 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
|
||||
|
||||
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
@@ -426,10 +379,14 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="],
|
||||
|
||||
"minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
@@ -480,10 +437,6 @@
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
@@ -504,6 +457,8 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"tabulator-tables": ["tabulator-tables@6.4.0", "", {}, "sha512-Lxh+leFNoBo/Yyr4USs6gxqbfo8anYUaUMmoT91pfVLtoUgl/dE+qV7ahnFrKVMCYYqGG33aIMPR7FzpPBaNYA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
@@ -546,8 +501,6 @@
|
||||
|
||||
"yauzl": ["yauzl@3.2.1", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A=="],
|
||||
|
||||
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"@solana/errors/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"start": "bun run build:css && bun run vendor && bun run src/index.ts",
|
||||
"build": "bun run build:css && bun run vendor && bun build --production --external sharp src/index.ts --target bun --outdir dist",
|
||||
"start": "bun run build && bun dist/index.js",
|
||||
"build": "bun run build:css && bun run vendor && bun run build:bundle",
|
||||
"build:bundle": "bun build --production src/index.ts --target bun --outfile dist/index.js && bun build --production src/server/plugin-runtime/worker-host.ts --target bun --outfile dist/server/plugin-runtime/worker-host.js",
|
||||
"vendor": "bun run scripts/vendor.ts",
|
||||
"build:prod:win": "bun build --production --external sharp src/index.ts --compile --target bun-windows-x64 --outfile dist/overlord-server.exe",
|
||||
"build:prod:linux": "bun build --production --external sharp src/index.ts --compile --target bun-linux-x64 --outfile dist/overlord-server-linux-x64",
|
||||
"build:prod:win": "bun build --production src/index.ts --compile --target bun-windows-x64 --outfile dist/overlord-server.exe",
|
||||
"build:prod:linux": "bun build --production src/index.ts --compile --target bun-linux-x64 --outfile dist/overlord-server-linux-x64",
|
||||
"build:css": "tailwindcss -i ./src/styles.css -o ./public/assets/tailwind.css --minify",
|
||||
"watch:css": "tailwindcss -i ./src/styles.css -o ./public/assets/tailwind.css --watch",
|
||||
"minify": "bun run scripts/minify-public.ts"
|
||||
@@ -22,7 +23,6 @@
|
||||
"geoip-lite": "latest",
|
||||
"jose": "^6.1.3",
|
||||
"msgpackr": "1.11.8",
|
||||
"sharp": "^0.34.5",
|
||||
"uuid": "^9.0.1",
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.11.0"
|
||||
@@ -48,7 +48,9 @@
|
||||
"highlight.js": "11.9.0",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"leaflet": "1.9.4",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"postcss": "^8.5.6",
|
||||
"tabulator-tables": "^6.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"terser": "^5.46.1",
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
@@ -35,6 +35,60 @@
|
||||
--color-danger: #ef4444;
|
||||
}
|
||||
|
||||
.monaco-editor,
|
||||
.monaco-editor .margin,
|
||||
.monaco-editor-background {
|
||||
--vscode-editor-background: #020617;
|
||||
}
|
||||
|
||||
.overlord-tabulator.tabulator {
|
||||
background: #020617;
|
||||
border: 1px solid rgb(30 41 59);
|
||||
border-radius: 0.75rem;
|
||||
color: #e2e8f0;
|
||||
min-height: 12rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-header {
|
||||
background: #0f172a;
|
||||
border-bottom-color: rgb(30 41 59);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-header .tabulator-col {
|
||||
background: #0f172a;
|
||||
border-right-color: rgb(30 41 59);
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-header .tabulator-col:hover {
|
||||
background: #162033;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-row {
|
||||
background: #020617;
|
||||
border-bottom-color: rgba(30, 41, 59, 0.85);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-row.tabulator-row-even {
|
||||
background: #07101f;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-row:hover {
|
||||
background: #111827;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-cell {
|
||||
border-right-color: rgba(30, 41, 59, 0.75);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.overlord-tabulator .tabulator-placeholder {
|
||||
background: #020617;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
:root:not(.app-ready) #app-loader {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
let monacoPromise = null;
|
||||
|
||||
export function loadMonaco() {
|
||||
if (window.monaco?.editor) return Promise.resolve(window.monaco);
|
||||
if (monacoPromise) return monacoPromise;
|
||||
|
||||
monacoPromise = new Promise((resolve, reject) => {
|
||||
const configure = () => {
|
||||
if (typeof window.require !== "function") {
|
||||
reject(new Error("Monaco loader is unavailable"));
|
||||
return;
|
||||
}
|
||||
window.require.config({ paths: { vs: "/vendor/monaco/vs" } });
|
||||
window.require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
|
||||
};
|
||||
|
||||
if (typeof window.require === "function") {
|
||||
configure();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "/vendor/monaco/vs/loader.js";
|
||||
script.defer = true;
|
||||
script.onload = configure;
|
||||
script.onerror = () => reject(new Error("Failed to load Monaco"));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return monacoPromise;
|
||||
}
|
||||
|
||||
export function createMonacoEditorAdapter(editor, monaco) {
|
||||
return {
|
||||
getValue: () => editor.getValue(),
|
||||
setValue(value) {
|
||||
editor.setValue(value || "");
|
||||
},
|
||||
setLanguage(language) {
|
||||
const model = editor.getModel();
|
||||
if (model) monaco.editor.setModelLanguage(model, language);
|
||||
},
|
||||
setSize(_width, height) {
|
||||
const domNode = editor.getDomNode();
|
||||
const host = domNode?.parentElement;
|
||||
if (host && height) host.style.height = height;
|
||||
editor.layout();
|
||||
},
|
||||
refresh() {
|
||||
editor.layout();
|
||||
},
|
||||
focus() {
|
||||
editor.focus();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
requestDesktopNotificationPermission,
|
||||
setDesktopNotificationsEnabled,
|
||||
} from "./notify-client.js";
|
||||
import { createMonacoEditorAdapter, loadMonaco } from "./monaco-loader.js";
|
||||
import { TabulatorFull as Tabulator } from "/vendor/tabulator/tabulator_esm.min.js";
|
||||
|
||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||
const wsStatus = document.getElementById("ws-status");
|
||||
const listEl = document.getElementById("notification-list");
|
||||
const tableEl = document.getElementById("notification-table");
|
||||
const emptyState = document.getElementById("empty-state");
|
||||
const notificationScopeHint = document.getElementById("notification-scope-hint");
|
||||
const tableHead = document.getElementById("notification-thead");
|
||||
const searchInput = document.getElementById("notification-search");
|
||||
const resultHint = document.getElementById("notification-result-hint");
|
||||
|
||||
@@ -83,6 +84,7 @@ const MAX_ROWS = 200;
|
||||
let defaultWebhookTemplate = "";
|
||||
let defaultTelegramTemplate = "";
|
||||
let webhookTemplateEditor = null;
|
||||
let notificationTable = null;
|
||||
|
||||
function tryFormatJsonText(text) {
|
||||
const source = String(text || "").trim();
|
||||
@@ -94,26 +96,57 @@ function tryFormatJsonText(text) {
|
||||
}
|
||||
}
|
||||
|
||||
function initWebhookTemplateEditor() {
|
||||
if (!webhookTemplateInput || typeof window.ace === "undefined") return;
|
||||
async function initWebhookTemplateEditor() {
|
||||
if (!webhookTemplateInput) return;
|
||||
try {
|
||||
webhookTemplateEditor = window.ace.edit("webhook-template");
|
||||
webhookTemplateEditor.setTheme("ace/theme/tomorrow_night");
|
||||
webhookTemplateEditor.session.setMode("ace/mode/json");
|
||||
webhookTemplateEditor.session.setUseWrapMode(true);
|
||||
webhookTemplateEditor.setShowPrintMargin(false);
|
||||
webhookTemplateEditor.setOption("fontSize", "12px");
|
||||
webhookTemplateEditor.setOption("tabSize", 2);
|
||||
webhookTemplateEditor.setOption("useSoftTabs", true);
|
||||
webhookTemplateEditor.on("blur", () => {
|
||||
const formatted = tryFormatJsonText(webhookTemplateEditor.getValue());
|
||||
if (formatted !== null && formatted !== webhookTemplateEditor.getValue()) {
|
||||
const position = webhookTemplateEditor.getCursorPosition();
|
||||
webhookTemplateEditor.setValue(formatted, -1);
|
||||
webhookTemplateEditor.moveCursorToPosition(position);
|
||||
const monaco = await loadMonaco();
|
||||
const host = document.createElement("div");
|
||||
host.className = "w-full rounded-lg border border-slate-800 overflow-hidden";
|
||||
host.style.height = "180px";
|
||||
webhookTemplateInput.classList.add("hidden");
|
||||
webhookTemplateInput.insertAdjacentElement("afterend", host);
|
||||
|
||||
monaco.editor.defineTheme("overlord-json", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
"editor.background": "#020617",
|
||||
"editor.foreground": "#e2e8f0",
|
||||
"editor.lineHighlightBackground": "#0f172a",
|
||||
"editorCursor.foreground": "#38bdf8",
|
||||
"editor.selectionBackground": "#2563eb66",
|
||||
"editorGutter.background": "#020617",
|
||||
},
|
||||
});
|
||||
|
||||
const editor = monaco.editor.create(host, {
|
||||
value: webhookTemplateInput.value || "",
|
||||
language: "json",
|
||||
theme: "overlord-json",
|
||||
automaticLayout: true,
|
||||
fontFamily: '"JetBrains Mono", Consolas, "Courier New", monospace',
|
||||
fontSize: 12,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
tabSize: 2,
|
||||
insertSpaces: true,
|
||||
wordWrap: "on",
|
||||
padding: { top: 8, bottom: 8 },
|
||||
});
|
||||
|
||||
webhookTemplateEditor = createMonacoEditorAdapter(editor, monaco);
|
||||
editor.onDidBlurEditorWidget(() => {
|
||||
const current = webhookTemplateEditor.getValue();
|
||||
const formatted = tryFormatJsonText(current);
|
||||
if (formatted !== null && formatted !== current) {
|
||||
const position = editor.getPosition();
|
||||
webhookTemplateEditor.setValue(formatted);
|
||||
if (position) editor.setPosition(position);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn("Monaco webhook editor unavailable; falling back to textarea", err);
|
||||
webhookTemplateEditor = null;
|
||||
}
|
||||
}
|
||||
@@ -247,6 +280,35 @@ function compareEntries(a, b) {
|
||||
return tableState.sortDir === "asc" ? cmp : -cmp;
|
||||
}
|
||||
|
||||
function sourceHtml(entry) {
|
||||
if (entry.kind === "event") {
|
||||
return CLIENT_EVENT_BADGE[entry.item.event] ||
|
||||
`<span class="text-xs text-slate-400">${escapeHtml(entry.item.event)}</span>`;
|
||||
}
|
||||
const isClipboard = entry.item.category === "clipboard";
|
||||
return isClipboard
|
||||
? `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-violet-900/60 text-violet-300 border border-violet-700/50"><i class="fa-solid fa-clipboard text-xs"></i> clipboard</span>`
|
||||
: `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-blue-900/60 text-blue-300 border border-blue-700/50"><i class="fa-solid fa-desktop text-xs"></i> window</span>`;
|
||||
}
|
||||
|
||||
function entryToRow(entry) {
|
||||
const item = entry.item;
|
||||
return {
|
||||
uid: entry.uid,
|
||||
kind: entry.kind,
|
||||
item,
|
||||
isLive: entry.isLive,
|
||||
ts: Number(item.ts) || 0,
|
||||
clientId: item.clientId || "",
|
||||
user: item.user || "-",
|
||||
title: entry.kind === "event" ? "client event" : (item.title || "-"),
|
||||
process: entry.kind === "event" ? (item.os || "-") : (item.process || "-"),
|
||||
keyword: entry.kind === "event" ? "-" : (item.keyword || "-"),
|
||||
source: entry.kind === "event" ? (item.event || "") : (item.category || "window"),
|
||||
sourceHtml: sourceHtml(entry),
|
||||
};
|
||||
}
|
||||
|
||||
function entryMatchesSearch(entry, query) {
|
||||
if (!query) return true;
|
||||
const item = entry.item;
|
||||
@@ -268,16 +330,12 @@ function entryMatchesSearch(entry, query) {
|
||||
}
|
||||
|
||||
function applyTableView() {
|
||||
if (!listEl) return;
|
||||
const q = tableState.search.trim().toLowerCase();
|
||||
const all = Array.from(tableState.entries.values());
|
||||
const filtered = q ? all.filter((entry) => entryMatchesSearch(entry, q)) : all;
|
||||
filtered.sort(compareEntries);
|
||||
|
||||
listEl.innerHTML = "";
|
||||
for (const entry of filtered) {
|
||||
listEl.appendChild(entry.element);
|
||||
}
|
||||
const rows = filtered.map(entryToRow);
|
||||
if (notificationTable) notificationTable.replaceData(rows);
|
||||
|
||||
if (emptyState) {
|
||||
if (all.length === 0) {
|
||||
@@ -317,11 +375,7 @@ function upsertEntry(item, kind, isLive) {
|
||||
existing.item = { ...existing.item, ...item };
|
||||
return existing;
|
||||
}
|
||||
const element =
|
||||
kind === "event"
|
||||
? createClientEventRowElement(item)
|
||||
: createNotificationRowElement(item, isLive);
|
||||
const entry = { item, kind, element, isLive };
|
||||
const entry = { uid, item, kind, isLive };
|
||||
tableState.entries.set(uid, entry);
|
||||
trimEntries();
|
||||
return entry;
|
||||
@@ -370,6 +424,95 @@ function clientIdCellHtml(clientId) {
|
||||
return `<td class="py-2 pr-4 whitespace-nowrap"><button type="button" class="client-id-toggle inline-flex items-center gap-1 font-mono text-xs px-2 py-0.5 rounded border border-slate-700 bg-slate-900/80 hover:bg-slate-800 hover:border-slate-600 text-slate-300" data-full="${safeFull}" data-short="${safeShort}" title="${safeFull} (click to expand)"><span class="client-id-text">${safeShort}<span class="text-slate-500">…</span></span><i class="fa-solid fa-chevron-right text-[10px] text-slate-500"></i></button></td>`;
|
||||
}
|
||||
|
||||
function clientIdFormatter(cell) {
|
||||
const id = String(cell.getValue() || "");
|
||||
if (!id) return `<span class="text-slate-500">-</span>`;
|
||||
if (id.length <= CLIENT_ID_SHORT_LEN) {
|
||||
return `<span class="font-mono text-xs text-slate-300">${escapeHtml(id)}</span>`;
|
||||
}
|
||||
const safeFull = escapeHtml(id);
|
||||
const safeShort = escapeHtml(id.slice(0, CLIENT_ID_SHORT_LEN));
|
||||
return `<button type="button" class="client-id-toggle inline-flex items-center gap-1 font-mono text-xs px-2 py-0.5 rounded border border-slate-700 bg-slate-900/80 hover:bg-slate-800 hover:border-slate-600 text-slate-300" data-full="${safeFull}" data-short="${safeShort}" title="${safeFull} (click to expand)"><span class="client-id-text">${safeShort}<span class="text-slate-500">...</span></span><i class="fa-solid fa-chevron-right text-[10px] text-slate-500"></i></button>`;
|
||||
}
|
||||
|
||||
function previewFormatter(cell) {
|
||||
const row = cell.getRow().getData();
|
||||
const item = row.item || {};
|
||||
const wrapper = document.createElement("div");
|
||||
if (row.kind === "event") {
|
||||
wrapper.textContent = "";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
const notificationId = item?.id || "";
|
||||
const skipFetch = !notificationId || (!row.isLive && !item?.screenshotId);
|
||||
if (skipFetch) {
|
||||
wrapper.textContent = "-";
|
||||
wrapper.className = "text-slate-500";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (item._previewObjectUrl) {
|
||||
const img = createPreviewImage(item._previewObjectUrl);
|
||||
wrapper.appendChild(img);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
wrapper.textContent = "Loading...";
|
||||
wrapper.className = "text-slate-500";
|
||||
let attempts = 0;
|
||||
const maxAttempts = 5;
|
||||
const fetchPreview = async () => {
|
||||
attempts += 1;
|
||||
try {
|
||||
const url = `/api/notifications/${encodeURIComponent(notificationId)}/screenshot?ts=${Date.now()}`;
|
||||
const res = await fetch(url, { cache: "no-store" });
|
||||
if (!res.ok) {
|
||||
if (res.status === 404 && attempts < maxAttempts) {
|
||||
setTimeout(fetchPreview, 1000 * attempts);
|
||||
return;
|
||||
}
|
||||
wrapper.textContent = "-";
|
||||
wrapper.className = "text-slate-500";
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
item._previewObjectUrl = objectUrl;
|
||||
wrapper.innerHTML = "";
|
||||
wrapper.className = "";
|
||||
wrapper.appendChild(createPreviewImage(objectUrl));
|
||||
} catch {
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(fetchPreview, 1000 * attempts);
|
||||
return;
|
||||
}
|
||||
wrapper.textContent = "-";
|
||||
wrapper.className = "text-slate-500";
|
||||
}
|
||||
};
|
||||
fetchPreview();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function createPreviewImage(objectUrl) {
|
||||
const img = document.createElement("img");
|
||||
img.className = "max-h-32 w-auto rounded border border-slate-800/80 cursor-zoom-in opacity-0";
|
||||
img.loading = "lazy";
|
||||
img.alt = "Notification screenshot";
|
||||
img.src = objectUrl;
|
||||
img.dataset.previewUrl = objectUrl;
|
||||
img.addEventListener("load", () => img.classList.remove("opacity-0"));
|
||||
img.addEventListener("click", () => {
|
||||
if (!previewModal || !previewModalImg) return;
|
||||
previewModalImg.src = img.dataset.previewUrl || objectUrl;
|
||||
previewModal.classList.remove("hidden");
|
||||
previewModal.classList.add("flex");
|
||||
});
|
||||
if (img.decode) img.decode().catch(() => {});
|
||||
return img;
|
||||
}
|
||||
|
||||
// ── Notification row factory ─────────────────────────────────────────────────
|
||||
function createNotificationRowElement(item, isLive) {
|
||||
const isClipboard = item.category === "clipboard";
|
||||
@@ -892,26 +1035,9 @@ function wireEventChannelsSave() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateSortIndicators() {
|
||||
if (!tableHead) return;
|
||||
const headers = tableHead.querySelectorAll("th[data-sort-key]");
|
||||
for (const th of headers) {
|
||||
const icon = th.querySelector(".sort-icon");
|
||||
if (!icon) continue;
|
||||
const key = th.getAttribute("data-sort-key");
|
||||
if (key === tableState.sortBy) {
|
||||
icon.className = `sort-icon fa-solid ${tableState.sortDir === "asc" ? "fa-sort-up" : "fa-sort-down"} text-xs text-slate-300`;
|
||||
th.classList.add("text-slate-200");
|
||||
} else {
|
||||
icon.className = "sort-icon fa-solid fa-sort text-xs text-slate-600";
|
||||
th.classList.remove("text-slate-200");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wireClientIdToggle() {
|
||||
if (!listEl) return;
|
||||
listEl.addEventListener("click", (event) => {
|
||||
if (!tableEl) return;
|
||||
tableEl.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest(".client-id-toggle");
|
||||
if (!btn) return;
|
||||
const textEl = btn.querySelector(".client-id-text");
|
||||
@@ -931,23 +1057,6 @@ function wireClientIdToggle() {
|
||||
}
|
||||
|
||||
function wireTableControls() {
|
||||
if (tableHead) {
|
||||
tableHead.addEventListener("click", (event) => {
|
||||
const th = event.target.closest("th[data-sort-key]");
|
||||
if (!th) return;
|
||||
const key = th.getAttribute("data-sort-key");
|
||||
if (!key) return;
|
||||
if (tableState.sortBy === key) {
|
||||
tableState.sortDir = tableState.sortDir === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
tableState.sortBy = key;
|
||||
tableState.sortDir = key === "ts" ? "desc" : "asc";
|
||||
}
|
||||
updateSortIndicators();
|
||||
applyTableView();
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
let debounce = null;
|
||||
searchInput.addEventListener("input", () => {
|
||||
@@ -958,12 +1067,82 @@ function wireTableControls() {
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateSortIndicators();
|
||||
function initNotificationTable() {
|
||||
if (!tableEl) return;
|
||||
notificationTable = new Tabulator(tableEl, {
|
||||
data: [],
|
||||
height: "24rem",
|
||||
layout: "fitDataStretch",
|
||||
reactiveData: false,
|
||||
placeholder: "No notifications yet.",
|
||||
virtualDom: true,
|
||||
index: "uid",
|
||||
initialSort: [{ column: "ts", dir: "desc" }],
|
||||
columns: [
|
||||
{
|
||||
title: "Time",
|
||||
field: "ts",
|
||||
width: 190,
|
||||
sorter: "number",
|
||||
formatter: (cell) => `<span class="text-slate-400">${formatTime(cell.getValue())}</span>`,
|
||||
},
|
||||
{
|
||||
title: "Client",
|
||||
field: "clientId",
|
||||
width: 150,
|
||||
formatter: clientIdFormatter,
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
field: "user",
|
||||
width: 130,
|
||||
formatter: "plaintext",
|
||||
},
|
||||
{
|
||||
title: "Title / Content",
|
||||
field: "title",
|
||||
minWidth: 240,
|
||||
formatter: (cell) => {
|
||||
const row = cell.getRow().getData();
|
||||
const value = escapeHtml(cell.getValue() || "-");
|
||||
const tone = row.kind === "event" ? "italic text-slate-400" : "text-slate-100";
|
||||
return `<span class="${tone}" title="${value}">${value}</span>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Process",
|
||||
field: "process",
|
||||
width: 160,
|
||||
formatter: "plaintext",
|
||||
},
|
||||
{
|
||||
title: "Keyword",
|
||||
field: "keyword",
|
||||
width: 130,
|
||||
formatter: "plaintext",
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
field: "source",
|
||||
width: 140,
|
||||
formatter: (cell) => cell.getRow().getData().sourceHtml || "",
|
||||
},
|
||||
{
|
||||
title: "Preview",
|
||||
field: "uid",
|
||||
width: 180,
|
||||
headerSort: false,
|
||||
formatter: previewFormatter,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||
initWebhookTemplateEditor();
|
||||
initNotificationTable();
|
||||
wireKeywordSave();
|
||||
wireWebhookSave();
|
||||
wireTelegramSave();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const body = document.getElementById("enrollment-body");
|
||||
const loadingRow = document.getElementById("enrollment-loading");
|
||||
import { TabulatorFull as Tabulator } from "/vendor/tabulator/tabulator_esm.min.js";
|
||||
|
||||
const enrollmentTableEl = document.getElementById("enrollment-table");
|
||||
const emptyEl = document.getElementById("enrollment-empty");
|
||||
const selectAllEl = document.getElementById("select-all");
|
||||
const bulkApproveBtn = document.getElementById("bulk-approve-btn");
|
||||
const bulkDenyBtn = document.getElementById("bulk-deny-btn");
|
||||
const statPending = document.getElementById("stat-pending");
|
||||
@@ -10,9 +10,9 @@ const statDenied = document.getElementById("stat-denied");
|
||||
const statBannedIps = document.getElementById("stat-banned-ips");
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const bannedIpsSection = document.getElementById("banned-ips-section");
|
||||
const bannedIpsBody = document.getElementById("banned-ips-body");
|
||||
const bannedIpsTableEl = document.getElementById("banned-ips-table");
|
||||
const bannedIpsEmpty = document.getElementById("banned-ips-empty");
|
||||
const clientsTable = document.getElementById("enrollment-table")?.closest(".bg-slate-900\\/50");
|
||||
const clientsTable = document.getElementById("clients-table-wrap");
|
||||
const addBanBtn = document.getElementById("add-ban-btn");
|
||||
const manualBanForm = document.getElementById("manual-ban-form");
|
||||
const banIpInput = document.getElementById("ban-ip-input");
|
||||
@@ -44,6 +44,9 @@ const denyReasonModalBackdrop = document.getElementById("deny-reason-modal-backd
|
||||
let currentFilter = "pending";
|
||||
let searchQuery = "";
|
||||
let clients = [];
|
||||
let enrollmentTable = null;
|
||||
let bannedIpsTable = null;
|
||||
const selectedIds = new Set();
|
||||
const expandedCells = new Set(); // tracks "clientId:field" keys
|
||||
|
||||
const SUSPICIOUS_FLAG_LABELS = {
|
||||
@@ -100,9 +103,8 @@ async function loadStats() {
|
||||
}
|
||||
|
||||
async function loadClients() {
|
||||
loadingRow.classList.remove("hidden");
|
||||
emptyEl.classList.add("hidden");
|
||||
body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove());
|
||||
if (enrollmentTable) enrollmentTable.replaceData([]);
|
||||
|
||||
const fetchFilter = currentFilter === "suspicious" ? "all" : currentFilter;
|
||||
try {
|
||||
@@ -134,44 +136,23 @@ async function loadClients() {
|
||||
})
|
||||
: base;
|
||||
|
||||
loadingRow.classList.add("hidden");
|
||||
|
||||
const showBulkActions = (currentFilter === "pending" || currentFilter === "suspicious") &&
|
||||
filtered.some((c) => (c.enrollmentStatus || "pending") === "pending");
|
||||
approveAllBtn.classList.toggle("hidden", !showBulkActions);
|
||||
denyAllBtn.classList.toggle("hidden", !showBulkActions);
|
||||
|
||||
const visibleIds = new Set(filtered.map((c) => c.id));
|
||||
for (const id of selectedIds) {
|
||||
if (!visibleIds.has(id)) selectedIds.delete(id);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
emptyEl.classList.remove("hidden");
|
||||
updateBulkButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const c of filtered) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "hover:bg-slate-800/40 transition-colors";
|
||||
tr.dataset.id = c.id;
|
||||
|
||||
const statusPill = statusBadgeWithReason(c.enrollmentStatus || "pending", c.denyReason);
|
||||
const fp = c.keyFingerprint ? c.keyFingerprint.substring(0, 16) + "..." : "-";
|
||||
const lastSeen = c.lastSeen ? timeAgo(c.lastSeen) : "-";
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-3"><input type="checkbox" class="row-check h-4 w-4 rounded border-slate-600" data-id="${esc(c.id)}" /></td>
|
||||
<td class="px-4 py-3 text-sm font-medium text-slate-200">${esc(c.host || c.id)}${suspiciousBadges(c.suspiciousFlags)}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.user || "-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.os || "-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${expandableCell(c.id, "cpu", c.cpu)}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${c.gpu ? dedupeGpu(c.gpu) : esc("-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.ram || "-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.ip || "-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(c.country || "-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-500 font-mono">${esc(fp)}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-500">${lastSeen}</td>
|
||||
<td class="px-4 py-3">${statusPill}</td>
|
||||
<td class="px-4 py-3"><div class="flex items-center justify-end gap-2 flex-nowrap">${actionButtons(c)}</div></td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
}
|
||||
if (enrollmentTable) enrollmentTable.replaceData(filtered);
|
||||
updateBulkButtons();
|
||||
}
|
||||
|
||||
@@ -251,6 +232,129 @@ function expandableCell(clientId, field, value) {
|
||||
`</span>`;
|
||||
}
|
||||
|
||||
function selectionHeaderFormatter() {
|
||||
const rows = enrollmentTable?.getData() || [];
|
||||
const allChecked = rows.length > 0 && rows.every((row) => selectedIds.has(row.id));
|
||||
return `<input type="checkbox" class="enrollment-select-all h-4 w-4 rounded border-slate-600" ${allChecked ? "checked" : ""} />`;
|
||||
}
|
||||
|
||||
function rowSelectionFormatter(cell) {
|
||||
const id = cell.getRow().getData().id;
|
||||
return `<input type="checkbox" class="row-check h-4 w-4 rounded border-slate-600" data-id="${esc(id)}" ${selectedIds.has(id) ? "checked" : ""} />`;
|
||||
}
|
||||
|
||||
function initEnrollmentTable() {
|
||||
if (!enrollmentTableEl) return;
|
||||
enrollmentTable = new Tabulator(enrollmentTableEl, {
|
||||
data: [],
|
||||
height: "32rem",
|
||||
layout: "fitDataStretch",
|
||||
placeholder: "No clients in this category.",
|
||||
index: "id",
|
||||
virtualDom: true,
|
||||
columns: [
|
||||
{
|
||||
title: "",
|
||||
field: "id",
|
||||
width: 54,
|
||||
hozAlign: "center",
|
||||
headerSort: false,
|
||||
headerFormatter: selectionHeaderFormatter,
|
||||
formatter: rowSelectionFormatter,
|
||||
},
|
||||
{
|
||||
title: "Host",
|
||||
field: "host",
|
||||
width: 190,
|
||||
formatter: (cell) => {
|
||||
const c = cell.getRow().getData();
|
||||
return `<span class="font-medium text-slate-200">${esc(c.host || c.id)}</span>${suspiciousBadges(c.suspiciousFlags)}`;
|
||||
},
|
||||
},
|
||||
{ title: "User", field: "user", width: 140, formatter: (cell) => esc(cell.getValue() || "-") },
|
||||
{ title: "OS", field: "os", width: 120, formatter: (cell) => esc(cell.getValue() || "-") },
|
||||
{
|
||||
title: "CPU",
|
||||
field: "cpu",
|
||||
width: 210,
|
||||
formatter: (cell) => {
|
||||
const c = cell.getRow().getData();
|
||||
return expandableCell(c.id, "cpu", c.cpu);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "GPU",
|
||||
field: "gpu",
|
||||
width: 220,
|
||||
formatter: (cell) => cell.getValue() ? dedupeGpu(cell.getValue()) : esc("-"),
|
||||
},
|
||||
{ title: "RAM", field: "ram", width: 100, formatter: (cell) => esc(cell.getValue() || "-") },
|
||||
{ title: "IP", field: "ip", width: 140, formatter: (cell) => esc(cell.getValue() || "-") },
|
||||
{ title: "Country", field: "country", width: 120, formatter: (cell) => esc(cell.getValue() || "-") },
|
||||
{
|
||||
title: "Key Fingerprint",
|
||||
field: "keyFingerprint",
|
||||
width: 170,
|
||||
formatter: (cell) => `<span class="font-mono text-slate-500">${esc(cell.getValue() ? cell.getValue().substring(0, 16) + "..." : "-")}</span>`,
|
||||
},
|
||||
{
|
||||
title: "Last Seen",
|
||||
field: "lastSeen",
|
||||
width: 120,
|
||||
sorter: "number",
|
||||
formatter: (cell) => `<span class="text-slate-500">${cell.getValue() ? timeAgo(cell.getValue()) : "-"}</span>`,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
field: "enrollmentStatus",
|
||||
width: 150,
|
||||
formatter: (cell) => {
|
||||
const c = cell.getRow().getData();
|
||||
return statusBadgeWithReason(c.enrollmentStatus || "pending", c.denyReason);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
field: "id",
|
||||
width: 300,
|
||||
headerSort: false,
|
||||
hozAlign: "right",
|
||||
formatter: (cell) => `<div class="flex items-center justify-end gap-2 flex-nowrap">${actionButtons(cell.getRow().getData())}</div>`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function initBannedIpsTable() {
|
||||
if (!bannedIpsTableEl) return;
|
||||
bannedIpsTable = new Tabulator(bannedIpsTableEl, {
|
||||
data: [],
|
||||
height: "24rem",
|
||||
layout: "fitColumns",
|
||||
placeholder: "No banned IPs.",
|
||||
index: "ip",
|
||||
columns: [
|
||||
{ title: "IP Address", field: "ip", width: 180, formatter: (cell) => `<span class="font-mono text-slate-200">${esc(cell.getValue())}</span>` },
|
||||
{ title: "Reason", field: "reason", formatter: (cell) => esc(cell.getValue() || "-") },
|
||||
{
|
||||
title: "Banned At",
|
||||
field: "createdAt",
|
||||
width: 150,
|
||||
sorter: "number",
|
||||
formatter: (cell) => `<span class="text-slate-500">${cell.getValue() ? timeAgo(cell.getValue()) : "-"}</span>`,
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
field: "ip",
|
||||
width: 120,
|
||||
headerSort: false,
|
||||
hozAlign: "right",
|
||||
formatter: (cell) => `<button class="act-unban px-2 py-1 rounded text-xs font-medium bg-slate-600 hover:bg-slate-700 text-white" data-ip="${esc(cell.getValue())}"><i class="fa-solid fa-unlock mr-1"></i>Unban</button>`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function timeAgo(ts) {
|
||||
const diff = Date.now() - ts;
|
||||
if (diff < 60000) return "just now";
|
||||
@@ -297,21 +401,24 @@ async function bulkAction(action) {
|
||||
} catch (e) {
|
||||
if (window.showToast) window.showToast(`Failed: ${e.message}`, "error");
|
||||
}
|
||||
selectAllEl.checked = false;
|
||||
selectedIds.clear();
|
||||
await Promise.all([loadClients(), loadStats()]);
|
||||
}
|
||||
|
||||
function getSelectedIds() {
|
||||
return [...document.querySelectorAll(".row-check:checked")].map(
|
||||
(el) => el.dataset.id,
|
||||
);
|
||||
return [...selectedIds];
|
||||
}
|
||||
|
||||
function updateBulkButtons() {
|
||||
const count = getSelectedIds().length;
|
||||
const visibleIds = new Set((enrollmentTable?.getData() || []).map((row) => row.id));
|
||||
for (const id of selectedIds) {
|
||||
if (!visibleIds.has(id)) selectedIds.delete(id);
|
||||
}
|
||||
const count = selectedIds.size;
|
||||
bulkApproveBtn.classList.toggle("hidden", count === 0);
|
||||
bulkDenyBtn.classList.toggle("hidden", count === 0);
|
||||
bulkBanBtn.classList.toggle("hidden", count === 0);
|
||||
enrollmentTable?.redraw(true);
|
||||
}
|
||||
|
||||
// ── Tab switching ──────────────────────────────────────────────────
|
||||
@@ -347,7 +454,7 @@ document.querySelectorAll(".enrollment-tab").forEach((tab) => {
|
||||
});
|
||||
|
||||
// ── Table delegation ───────────────────────────────────────────────
|
||||
body.addEventListener("click", (e) => {
|
||||
enrollmentTableEl?.addEventListener("click", (e) => {
|
||||
// Expandable CPU/GPU cells
|
||||
const expander = e.target.closest(".hw-expand");
|
||||
if (expander) {
|
||||
@@ -372,11 +479,24 @@ body.addEventListener("click", (e) => {
|
||||
else if (btn.classList.contains("act-ban-ip")) banClientIp(id);
|
||||
});
|
||||
|
||||
body.addEventListener("change", () => updateBulkButtons());
|
||||
selectAllEl.addEventListener("change", () => {
|
||||
document.querySelectorAll(".row-check").forEach((cb) => {
|
||||
cb.checked = selectAllEl.checked;
|
||||
});
|
||||
enrollmentTableEl?.addEventListener("change", (e) => {
|
||||
const selectAll = e.target.closest(".enrollment-select-all");
|
||||
if (selectAll) {
|
||||
const rows = enrollmentTable?.getData() || [];
|
||||
for (const row of rows) {
|
||||
if (selectAll.checked) selectedIds.add(row.id);
|
||||
else selectedIds.delete(row.id);
|
||||
}
|
||||
updateBulkButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
const checkbox = e.target.closest(".row-check");
|
||||
if (!checkbox) return;
|
||||
const id = checkbox.dataset.id;
|
||||
if (!id) return;
|
||||
if (checkbox.checked) selectedIds.add(id);
|
||||
else selectedIds.delete(id);
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
@@ -455,8 +575,8 @@ async function banClientIp(clientId) {
|
||||
}
|
||||
|
||||
async function loadBannedIps() {
|
||||
bannedIpsBody.innerHTML = "";
|
||||
bannedIpsEmpty.classList.add("hidden");
|
||||
if (bannedIpsTable) bannedIpsTable.replaceData([]);
|
||||
|
||||
try {
|
||||
const data = await api("/api/enrollment/banned-ips");
|
||||
@@ -467,27 +587,13 @@ async function loadBannedIps() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of items) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "hover:bg-slate-800/40 transition-colors";
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-3 text-sm font-mono text-slate-200">${esc(entry.ip)}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-400">${esc(entry.reason || "-")}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-500">${entry.createdAt ? timeAgo(entry.createdAt) : "-"}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<button class="act-unban px-2 py-1 rounded text-xs font-medium bg-slate-600 hover:bg-slate-700 text-white" data-ip="${esc(entry.ip)}">
|
||||
<i class="fa-solid fa-unlock mr-1"></i>Unban
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
bannedIpsBody.appendChild(tr);
|
||||
}
|
||||
if (bannedIpsTable) bannedIpsTable.replaceData(items);
|
||||
} catch {
|
||||
bannedIpsEmpty.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
bannedIpsBody.addEventListener("click", async (e) => {
|
||||
bannedIpsTableEl?.addEventListener("click", async (e) => {
|
||||
const btn = e.target.closest("button");
|
||||
if (!btn) return;
|
||||
if (btn.classList.contains("act-unban")) {
|
||||
@@ -667,6 +773,8 @@ denyReasonModalConfirm.addEventListener("click", async () => {
|
||||
});
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────────
|
||||
initEnrollmentTable();
|
||||
initBannedIpsTable();
|
||||
loadSettings();
|
||||
loadStats();
|
||||
loadClients();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createMonacoEditorAdapter, loadMonaco } from "./monaco-loader.js";
|
||||
|
||||
const clientList = document.getElementById("client-list");
|
||||
const clientSearch = document.getElementById("client-search");
|
||||
const osFilter = document.getElementById("os-filter");
|
||||
@@ -29,7 +31,7 @@ let editorInstance = null;
|
||||
const EDITOR_MODES = {
|
||||
powershell: "powershell",
|
||||
bash: "shell",
|
||||
cmd: "shell",
|
||||
cmd: "bat",
|
||||
python: "python",
|
||||
sh: "shell",
|
||||
};
|
||||
@@ -50,7 +52,11 @@ function setEditorValue(value) {
|
||||
function setEditorMode(type) {
|
||||
if (!editorInstance) return;
|
||||
const mode = EDITOR_MODES[type] || "powershell";
|
||||
if (typeof editorInstance.setLanguage === "function") {
|
||||
editorInstance.setLanguage(mode);
|
||||
} else if (typeof editorInstance.setOption === "function") {
|
||||
editorInstance.setOption("mode", mode);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
@@ -785,17 +791,56 @@ Promise.all([
|
||||
loadAutoTasks();
|
||||
renderTemplatePalette();
|
||||
|
||||
if (window.CodeMirror && scriptEditor) {
|
||||
editorInstance = window.CodeMirror.fromTextArea(scriptEditor, {
|
||||
lineNumbers: true,
|
||||
mode: EDITOR_MODES[scriptType?.value || "powershell"] || "powershell",
|
||||
theme: "material-darker",
|
||||
indentUnit: 2,
|
||||
async function initScriptEditor() {
|
||||
if (!scriptEditor) return;
|
||||
try {
|
||||
const monaco = await loadMonaco();
|
||||
const host = document.createElement("div");
|
||||
host.id = "script-editor-monaco";
|
||||
host.className = "w-full flex-1 min-h-[24rem] rounded-lg border border-slate-700 overflow-hidden";
|
||||
host.style.height = "24rem";
|
||||
scriptEditor.classList.add("hidden");
|
||||
scriptEditor.insertAdjacentElement("afterend", host);
|
||||
|
||||
const editor = monaco.editor.create(host, {
|
||||
value: scriptEditor.value || "",
|
||||
language: EDITOR_MODES[scriptType?.value || "powershell"] || "powershell",
|
||||
theme: "vs-dark",
|
||||
automaticLayout: true,
|
||||
fontFamily: '"JetBrains Mono", Consolas, "Courier New", monospace',
|
||||
fontSize: 13,
|
||||
minimap: { enabled: true, side: "right" },
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "on",
|
||||
tabSize: 2,
|
||||
lineWrapping: true,
|
||||
insertSpaces: true,
|
||||
padding: { top: 12, bottom: 12 },
|
||||
renderLineHighlight: "all",
|
||||
});
|
||||
editorInstance.setSize(null, "24rem");
|
||||
|
||||
monaco.editor.defineTheme("overlord-dark", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
"editor.background": "#020617",
|
||||
"editor.foreground": "#e2e8f0",
|
||||
"editor.lineHighlightBackground": "#0f172a",
|
||||
"editorCursor.foreground": "#34d399",
|
||||
"editor.selectionBackground": "#2563eb66",
|
||||
"editorGutter.background": "#020617",
|
||||
"editorLineNumber.foreground": "#64748b",
|
||||
"editorLineNumber.activeForeground": "#cbd5e1",
|
||||
},
|
||||
});
|
||||
monaco.editor.setTheme("overlord-dark");
|
||||
|
||||
editorInstance = createMonacoEditorAdapter(editor, monaco);
|
||||
window._vbCodeMirror = editorInstance;
|
||||
syncEditorHeight();
|
||||
} catch (err) {
|
||||
console.warn("Monaco editor unavailable; falling back to textarea", err);
|
||||
}
|
||||
}
|
||||
|
||||
const modeToggleCode = document.getElementById("mode-toggle-code");
|
||||
@@ -824,8 +869,7 @@ function syncEditorHeight() {
|
||||
const target = Math.max(384, rightHeight - padY - headerH - headerMb);
|
||||
editorInstance.setSize(null, `${target}px`);
|
||||
}
|
||||
if (editorInstance && rightColumn) {
|
||||
syncEditorHeight();
|
||||
if (rightColumn) {
|
||||
if (typeof ResizeObserver === "function") {
|
||||
const ro = new ResizeObserver(() => syncEditorHeight());
|
||||
ro.observe(rightColumn);
|
||||
@@ -833,6 +877,7 @@ if (editorInstance && rightColumn) {
|
||||
window.addEventListener("resize", syncEditorHeight);
|
||||
lgMediaQuery.addEventListener?.("change", syncEditorHeight);
|
||||
}
|
||||
initScriptEditor();
|
||||
let visualBuilderInited = false;
|
||||
|
||||
function setMode(mode) {
|
||||
@@ -856,7 +901,7 @@ function setMode(mode) {
|
||||
modeToggleCode.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-emerald-600 text-white";
|
||||
modeToggleVisual.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-slate-800 text-slate-300 hover:bg-slate-700";
|
||||
if (editorInstance) {
|
||||
editorInstance.refresh();
|
||||
editorInstance.refresh?.();
|
||||
syncEditorHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<link rel="stylesheet" href="/assets/tailwind.css" />
|
||||
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
|
||||
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
|
||||
<script defer src="/vendor/ace-builds/ace.js"></script>
|
||||
<link rel="stylesheet" href="/vendor/tabulator/tabulator_midnight.min.css" />
|
||||
<link rel="stylesheet" href="/assets/main.css" />
|
||||
<link rel="stylesheet" href="/assets/custom.css" />
|
||||
<script defer src="/assets/toast.js"></script>
|
||||
@@ -171,11 +171,11 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<textarea
|
||||
id="webhook-template"
|
||||
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 focus-within:border-blue-500"
|
||||
style="height: 180px;"
|
||||
></div>
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-slate-500">
|
||||
Variables: <code class="text-slate-400">{title}</code>
|
||||
<code class="text-slate-400">{keyword}</code>
|
||||
@@ -415,37 +415,7 @@
|
||||
<span id="notification-result-hint" class="text-xs text-slate-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-96">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead id="notification-thead" class="text-slate-400 sticky top-0 bg-slate-900">
|
||||
<tr class="text-left">
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="ts">
|
||||
<span class="inline-flex items-center gap-1">Time<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="clientId">
|
||||
<span class="inline-flex items-center gap-1">Client<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="user">
|
||||
<span class="inline-flex items-center gap-1">User<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="title">
|
||||
<span class="inline-flex items-center gap-1">Title / Content<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="process">
|
||||
<span class="inline-flex items-center gap-1">Process<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="keyword">
|
||||
<span class="inline-flex items-center gap-1">Keyword<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4 select-none cursor-pointer hover:text-slate-200" data-sort-key="source">
|
||||
<span class="inline-flex items-center gap-1">Source<i class="sort-icon fa-solid fa-sort text-xs text-slate-600"></i></span>
|
||||
</th>
|
||||
<th class="py-2 pr-4">Preview</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="notification-list" class="text-slate-200"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="notification-table" class="overlord-tabulator"></div>
|
||||
<div
|
||||
id="empty-state"
|
||||
class="py-6 text-center text-slate-400"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/tailwind.css" />
|
||||
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
|
||||
<link rel="stylesheet" href="/vendor/tabulator/tabulator_midnight.min.css" />
|
||||
<link rel="stylesheet" href="/assets/main.css" />
|
||||
<link rel="stylesheet" href="/assets/custom.css" />
|
||||
<script defer src="/assets/toast.js"></script>
|
||||
@@ -113,35 +114,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Clients Table -->
|
||||
<div class="bg-slate-900/50 border border-slate-800 rounded-xl overflow-x-auto">
|
||||
<table class="w-full min-w-[1200px]" id="enrollment-table">
|
||||
<thead class="bg-slate-800/50 border-b border-slate-700">
|
||||
<tr>
|
||||
<th class="w-10 px-4 py-3">
|
||||
<input type="checkbox" id="select-all" class="h-4 w-4 rounded border-slate-600" />
|
||||
</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Host</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">User</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">OS</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">CPU</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">GPU</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">RAM</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">IP</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Country</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Key Fingerprint</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Last Seen</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Status</th>
|
||||
<th class="text-right px-4 py-3 text-sm font-semibold text-slate-300">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="enrollment-body" class="divide-y divide-slate-800">
|
||||
<tr id="enrollment-loading">
|
||||
<td colspan="13" class="px-6 py-12 text-center text-slate-500">
|
||||
<i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="clients-table-wrap" class="bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden">
|
||||
<div id="enrollment-table" class="overlord-tabulator"></div>
|
||||
<div id="enrollment-empty" class="hidden px-6 py-12 text-center text-slate-500">
|
||||
<i class="fa-solid fa-inbox text-3xl mb-3 block"></i>
|
||||
<p>No clients in this category.</p>
|
||||
@@ -183,17 +157,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-800/50 border-b border-slate-700">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">IP Address</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Reason</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-slate-300">Banned At</th>
|
||||
<th class="text-right px-4 py-3 text-sm font-semibold text-slate-300">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="banned-ips-body" class="divide-y divide-slate-800"></tbody>
|
||||
</table>
|
||||
<div id="banned-ips-table" class="overlord-tabulator"></div>
|
||||
<div id="banned-ips-empty" class="hidden px-6 py-12 text-center text-slate-500">
|
||||
<i class="fa-solid fa-shield-halved text-3xl mb-3 block"></i>
|
||||
<p>No banned IPs.</p>
|
||||
|
||||
@@ -12,13 +12,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/tailwind.css" />
|
||||
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
|
||||
<link rel="stylesheet" href="/vendor/codemirror/lib/codemirror.css" />
|
||||
<link rel="stylesheet" href="/vendor/codemirror/theme/material-darker.css" />
|
||||
<style>
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/assets/main.css" />
|
||||
<link rel="stylesheet" href="/assets/custom.css" />
|
||||
<script defer src="/assets/toast.js"></script>
|
||||
@@ -258,10 +251,6 @@
|
||||
</main>
|
||||
|
||||
<script type="module" src="/assets/nav.js"></script>
|
||||
<script defer src="/vendor/codemirror/lib/codemirror.js"></script>
|
||||
<script defer src="/vendor/codemirror/mode/powershell/powershell.js"></script>
|
||||
<script defer src="/vendor/codemirror/mode/shell/shell.js"></script>
|
||||
<script defer src="/vendor/codemirror/mode/python/python.js"></script>
|
||||
<script type="module" src="/assets/scripts.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -166,6 +166,26 @@ if (existsSync(path.join(leafRoot, "images"))) {
|
||||
copyDir(path.join(leafRoot, "images"), path.join(VENDOR, "leaflet", "images"));
|
||||
}
|
||||
|
||||
/* ── Monaco Editor ───────────────────────────────────────────────── */
|
||||
|
||||
console.log("Copying Monaco Editor ...");
|
||||
copyDir(
|
||||
path.join(NM, "monaco-editor", "min", "vs"),
|
||||
path.join(VENDOR, "monaco", "vs"),
|
||||
);
|
||||
|
||||
/* ── Tabulator ───────────────────────────────────────────────────── */
|
||||
|
||||
console.log("Copying Tabulator ...");
|
||||
copyFile(
|
||||
path.join(NM, "tabulator-tables", "dist", "js", "tabulator_esm.min.js"),
|
||||
path.join(VENDOR, "tabulator", "tabulator_esm.min.js"),
|
||||
);
|
||||
copyFile(
|
||||
path.join(NM, "tabulator-tables", "dist", "css", "tabulator_midnight.min.css"),
|
||||
path.join(VENDOR, "tabulator", "tabulator_midnight.min.css"),
|
||||
);
|
||||
|
||||
/* ── highlight.js (bundle core + languages) ──────────────────────── */
|
||||
|
||||
console.log("Bundling highlight.js ...");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ServerWebSocket } from "bun";
|
||||
import { decodeMessage, encodeMessage, type WireMessage, type PluginManifest } from "./protocol";
|
||||
import { logger } from "./logger";
|
||||
import { fileURLToPath } from "url";
|
||||
import { fileURLToPath, pathToFileURL } from "url";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
@@ -202,6 +202,14 @@ const DEPLOY_ROOT = path.join(DATA_DIR, "deploy");
|
||||
const WINRE_ROOT = path.join(DATA_DIR, "winre");
|
||||
const FILE_SHARE_ROOT = path.join(DATA_DIR, "file-share");
|
||||
|
||||
function resolvePluginWorkerHostUrl(): string {
|
||||
const builtWorker = path.join(RUNTIME_ROOT, "dist", "server", "plugin-runtime", "worker-host.js");
|
||||
if (existsSync(builtWorker)) {
|
||||
return pathToFileURL(builtWorker).href;
|
||||
}
|
||||
return new URL("./server/plugin-runtime/worker-host.ts", import.meta.url).href;
|
||||
}
|
||||
|
||||
const TLS_CERT_PATH = config.tls.certPath;
|
||||
const TLS_KEY_PATH = config.tls.keyPath;
|
||||
const TLS_CA_PATH = config.tls.caPath;
|
||||
@@ -240,7 +248,7 @@ const loadPluginState = async () => {
|
||||
|
||||
const pluginRuntime = createPluginRuntime({
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
workerHostUrl: new URL("./server/plugin-runtime/worker-host.ts", import.meta.url).href,
|
||||
workerHostUrl: resolvePluginWorkerHostUrl(),
|
||||
setLastError: (pluginId, error) => {
|
||||
pluginState.lastError[pluginId] = error;
|
||||
void savePluginState();
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
let _sharp: any = null;
|
||||
let _sharpLoadAttempted = false;
|
||||
|
||||
async function getSharp(): Promise<any> {
|
||||
if (_sharp) return _sharp;
|
||||
if (_sharpLoadAttempted) throw new Error("sharp module unavailable");
|
||||
_sharpLoadAttempted = true;
|
||||
try {
|
||||
_sharp = (await import("sharp")).default as any;
|
||||
return _sharp!;
|
||||
} catch (err) {
|
||||
console.error("[thumbnails] Failed to load sharp module. Thumbnails will be unavailable.", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const THUMBNAIL_WIDTH = Math.max(64, Number(process.env.OVERLORD_THUMBNAIL_WIDTH || 1920));
|
||||
const THUMBNAIL_HEIGHT = Math.max(48, Number(process.env.OVERLORD_THUMBNAIL_HEIGHT || 1080));
|
||||
const THUMBNAIL_QUALITY = Math.min(95, Math.max(40, Number(process.env.OVERLORD_THUMBNAIL_QUALITY || 88)));
|
||||
@@ -133,18 +117,16 @@ async function buildThumbnailBytes(bytes: Uint8Array, format: string): Promise<U
|
||||
return null;
|
||||
}
|
||||
|
||||
const sharp = await getSharp();
|
||||
const output = await sharp(Buffer.from(bytes), { failOn: "none" })
|
||||
.rotate()
|
||||
.resize({
|
||||
width: THUMBNAIL_WIDTH,
|
||||
height: THUMBNAIL_HEIGHT,
|
||||
const output = await new Bun.Image(bytes, {
|
||||
autoOrient: true,
|
||||
maxPixels: THUMBNAIL_WIDTH * THUMBNAIL_HEIGHT * 16,
|
||||
})
|
||||
.resize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, {
|
||||
fit: "inside",
|
||||
withoutEnlargement: true,
|
||||
fastShrinkOnLoad: true,
|
||||
})
|
||||
.webp({ quality: THUMBNAIL_QUALITY })
|
||||
.toBuffer();
|
||||
.bytes();
|
||||
|
||||
return new Uint8Array(output);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1041,7 +1041,7 @@ The SSE stream keeps itself alive with a comment heartbeat every 25 s, which wor
|
||||
- **One worker per plugin.** RPC calls into the same plugin serialise. Long-running RPCs block other RPCs to that plugin (not other plugins). Do heavy work in `onEvent` and let RPCs read pre-computed state.
|
||||
- **`bun:sqlite` is single-writer per database file.** Since each plugin gets its own DB this is fine; just don't open the same `plugin.db` from another process.
|
||||
- **The worker is not a security sandbox.** It has the same filesystem and network access as the main server. Treat `server.js` with the same trust as your other server code, and lean on plugin signing if you load third-party bundles.
|
||||
- **Compiled binaries.** Workers spawned from the bundled `--compile` build need the worker host module embedded; if you ship that build, run `bun build` with the worker host included as an entry point or stick with `bun run` for now.
|
||||
- **Bundled builds.** Workers spawned from a bundled server need the worker host module emitted beside the main server bundle. The production build does this by building both `src/index.ts` and `src/server/plugin-runtime/worker-host.ts`.
|
||||
- **No automatic restart on worker crash.** A crashed worker is logged and `pluginState.lastError` is set, but it will not loop-restart on its own. Disable + re-enable the plugin to bring it back up.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user