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:
KingKDot
2026-05-30 20:38:32 -05:00
parent f7ea8f207a
commit 5933297686
15 changed files with 661 additions and 333 deletions
+9 -11
View File
@@ -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
View File
@@ -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=="],
+7 -5
View File
@@ -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"
+54
View File
@@ -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();
},
};
}
+246 -67
View File
@@ -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();
+171 -63
View File
@@ -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();
+61 -16
View File
@@ -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";
editorInstance.setOption("mode", mode);
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,
tabSize: 2,
lineWrapping: true,
});
editorInstance.setSize(null, "24rem");
window._vbCodeMirror = editorInstance;
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,
insertSpaces: true,
padding: { top: 12, bottom: 12 },
renderLineHighlight: "all",
});
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();
}
}
+4 -34
View File
@@ -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"
+4 -40
View File
@@ -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>
-11
View File
@@ -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>
+20
View File
@@ -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 ...");
+10 -2
View File
@@ -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();
+6 -24
View File
@@ -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
View File
@@ -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.
---