diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml new file mode 100644 index 0000000..68499ff --- /dev/null +++ b/.github/workflows/deploy-demo.yml @@ -0,0 +1,52 @@ +name: Deploy demo to GitHub Pages + +on: + push: + branches: [main] + paths: + - "demo/**" + - ".github/workflows/deploy-demo.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: demo + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + cache-dependency-path: demo/package-lock.json + + - run: npm install + - run: npm run build + env: + VITE_BASE_PATH: /LogOctopus/ + + - uses: actions/upload-pages-artifact@v3 + with: + path: demo/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index bd254c4..643237e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,16 @@ LogOctopus automatically collects, stores, and visualises logs and system data f Built for automated and distributed testing workflows, it captures logs, system events, performance metrics, and command outputs from multiple devices within the context of a single test scenario β€” making debugging and root cause analysis significantly faster. Instead of manually gathering logs from different machines after a failed test, LogOctopus centralises all test evidence into one place and exposes it through an interactive web UI and REST API. +## Live Demo + +
+ +[![Try the live demo](docs/demo-preview.gif)](https://kkuuba.github.io/LogOctopus/) + +*πŸ‘† Click the preview above to open the interactive demo* + +
+ ## Fast Track **What is it?** - LogOctopus is an SSH-native test observability platform that collects logs and execution data from multiple remote devices and organises everything per test session or scenario. diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..5d874a5 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,14 @@ + + + + + + LogOctopus β€” Live Demo + + + + +
+ + + diff --git a/demo/package-lock.json b/demo/package-lock.json new file mode 100644 index 0000000..a532d4f --- /dev/null +++ b/demo/package-lock.json @@ -0,0 +1,1680 @@ +{ + "name": "logoctopus-demo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "logoctopus-demo", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", + "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000..20ed234 --- /dev/null +++ b/demo/package.json @@ -0,0 +1,19 @@ +{ + "name": "logoctopus-demo", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.0" + } +} diff --git a/demo/src/LogOctopus.jsx b/demo/src/LogOctopus.jsx new file mode 100644 index 0000000..f206d0e --- /dev/null +++ b/demo/src/LogOctopus.jsx @@ -0,0 +1,6122 @@ +import { useState, useEffect, useCallback, useRef, useMemo } from "react"; + +// ── CONFIG ──────────────────────────────────────────────────────────────────── +const API_BASE = (import.meta.env.VITE_API_BASE) || "http://localhost:8050" +; + +// Plotly is expected as a global (loaded via CDN script tag in index.html): +// +// Or install: npm i plotly.js-dist-min β†’ import Plotly from 'plotly.js-dist-min' + +async function apiFetch(path, options = {}) { + const res = await fetch(`${API_BASE}${path}`, { + headers: { "Content-Type": "application/json" }, + ...options, + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw Object.assign(new Error(err.error || res.statusText), { status: res.status }); + } + if (res.status === 204) return null; + return res.json(); +} + +// ── AUTH CONTEXT ────────────────────────────────────────────────────────────── +// Server-side auth: credentials are validated by POST /api/auth/login which +// compares a SHA-256 hash against the one stored in settings.json. The server +// returns a random token that is kept in sessionStorage (tab-scoped, never +// persisted to disk by the browser). +// +// FIX: the previous implementation stored the admin password in plain-text in +// localStorage and performed all comparison client-side, meaning any XSS or +// browser extension could trivially extract the password. Auth is now backed +// by the backend's /api/auth/login and /api/auth/logout endpoints. + +function useAuth() { + const [role, setRole] = useState(() => sessionStorage.getItem("lo_role") || "guest"); + const [token, setToken] = useState(() => sessionStorage.getItem("lo_token") || ""); + + // login: calls the backend; returns true on success, false on bad credentials, + // throws on network error so the caller can show a toast. + const login = async (user, pass) => { + const res = await fetch(`${API_BASE}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: user, password: pass }), + }); + if (res.status === 401) return false; + if (!res.ok) throw new Error(`Login failed (${res.status})`); + const { token: tok } = await res.json(); + sessionStorage.setItem("lo_role", "admin"); + sessionStorage.setItem("lo_token", tok); + setRole("admin"); + setToken(tok); + return true; + }; + + const logout = () => { + const tok = sessionStorage.getItem("lo_token") || ""; + sessionStorage.removeItem("lo_role"); + sessionStorage.removeItem("lo_token"); + setRole("guest"); + setToken(""); + // Best-effort server-side token revocation + if (tok) { + fetch(`${API_BASE}/api/auth/logout`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token: tok }), + }).catch(() => {}); + } + }; + + // changePassword: validates the current password server-side, then updates + // the hash via the existing /api/settings/change-password endpoint. + // Returns true on success, false if currentPass is wrong, throws on error. + const changePassword = async (currentPass, newPass) => { + // Re-authenticate to verify the current password before allowing a change. + const adminUser = import.meta?.env?.VITE_ADMIN_USER || "admin"; + const checkRes = await fetch(`${API_BASE}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: adminUser, password: currentPass }), + }); + if (checkRes.status === 401) return false; + if (!checkRes.ok) throw new Error(`Verification failed (${checkRes.status})`); + + await apiFetch("/api/settings/change-password", { + method: "POST", + body: JSON.stringify({ new_password: newPass }), + }); + return true; + }; + + return { role, isAdmin: role === "admin", token, login, logout, changePassword }; +} + +// ── PLOTLY CHART PANEL ──────────────────────────────────────────────────────── +/** + * Renders one Plotly chart for a single snapshot's row data. + * Supports numeric line charts and categorical scatter plots. + * Hover tooltips, zoom, and pan are enabled by default via Plotly config. + */ +function PlotlyChart({ rows, title, index, dataUnit }) { + const divRef = useRef(null); + + useEffect(() => { + if (!divRef.current || !rows || rows.length === 0) return; + + const Plotly = window.Plotly; + if (!Plotly) { + divRef.current.innerHTML = + '

Plotly not loaded β€” add the CDN script to index.html

'; + return; + } + + const isNumeric = rows.some( + (d) => d.content !== null && !isNaN(parseFloat(d.content)) + ); + + const xValues = rows.map((d) => d.time); + const yValues = isNumeric + ? rows.map((d) => parseFloat(d.content)) + : rows.map((d) => d.content); + + // Cycle through a palette of accent colours for multi-chart display + const palette = ["#818cf8", "#a78bfa", "#f472b6", "#fb923c", "#34d399", "#60a5fa"]; + const lineColor = palette[index % palette.length]; + + const trace = isNumeric + ? { + x: xValues, + y: yValues, + type: "scatter", + mode: "lines+markers", + name: title, + line: { color: lineColor, width: 2.5, shape: "spline", smoothing: 0.8 }, + marker: { size: 5, color: lineColor, symbol: "circle" }, + hovertemplate: dataUnit ? `%{y} ${dataUnit}` : "%{y}", + } + : { + x: xValues, + y: yValues, + type: "scatter", + mode: "markers", + name: title, + marker: { size: 8, color: lineColor, opacity: 0.85, symbol: "diamond" }, + hovertemplate: dataUnit ? `%{y} ${dataUnit}` : "%{y}", + }; + + const layout = { + title: { + text: title, + font: { color: "#e8eaf0", size: 13, family: "JetBrains Mono, monospace" }, + x: 0.04, + }, + paper_bgcolor: "transparent", + plot_bgcolor: "rgba(9,9,15,0.6)", + font: { color: "#6b7280", family: "JetBrains Mono, monospace", size: 11 }, + xaxis: { + gridcolor: "rgba(255,255,255,0.06)", + zerolinecolor: "rgba(255,255,255,0.08)", + tickfont: { color: "#6b7280", size: 10 }, + showspikes: true, + spikecolor: "rgba(129,140,248,0.4)", + spikethickness: 1, + spikedash: "dot", + }, + yaxis: { + gridcolor: "rgba(255,255,255,0.06)", + zerolinecolor: "rgba(255,255,255,0.08)", + tickfont: { color: "#6b7280", size: 10 }, + showspikes: true, + spikecolor: "rgba(129,140,248,0.4)", + spikethickness: 1, + spikedash: "dot", + ...(dataUnit ? { title: { text: dataUnit, font: { color: "#6b7280", size: 11, family: "JetBrains Mono, monospace" } } } : {}), + }, + margin: { t: 40, r: 20, b: 48, l: 56 }, + hovermode: "x unified", + hoverlabel: { + bgcolor: "#111827", + bordercolor: lineColor, + font: { color: "#e8eaf0", size: 12, family: "JetBrains Mono, monospace" }, + }, + showlegend: false, + }; + + const config = { + responsive: true, + displayModeBar: true, + modeBarButtonsToRemove: ["select2d", "lasso2d", "toggleSpikelines"], + displaylogo: false, + toImageButtonOptions: { + format: "png", + filename: title.replace(/\s+/g, "_"), + scale: 2, + }, + }; + + Plotly.newPlot(divRef.current, [trace], layout, config); + + return () => { + if (divRef.current) Plotly.purge(divRef.current); + }; + }, [rows, title, index, dataUnit]); + + if (!rows || rows.length === 0) { + return ( +
+ No data for this snapshot +
+ ); + } + + return ( +
+
+
+ ); +} + +// ── MULTI-CHART LOG CONTENT VIEW ────────────────────────────────────────────── +/** + * When isChart=true and multiple snapshots are selected, renders each + * snapshot as its own titled Plotly panel inside the modal β€” side by side + * (2-column grid) or stacked depending on count. + */ +function ChartContentView({ chartGroups }) { + // chartGroups: [{ snapInfo, rows }] + if (!chartGroups || chartGroups.length === 0) + return

No chart data.

; + + return ( +
+ {chartGroups.map((g, i) => { + const label = `${g.snapInfo.deviceName} β€” ${g.snapInfo.logName}`; + return ( +
+ +
+ {g.snapInfo.logName} + {g.snapInfo.deviceName} + {g.rows.length} points + Session: {g.snapInfo.sessionId} + Data unit: {g.snapInfo.dataUnit} +
+
+ ); + })} +
+ ); +} + +// ── MONACO LOADER HOOK ──────────────────────────────────────────────────────── +// Lazily injects the Monaco AMD bundle from jsDelivr the first time it is +// needed, then resolves the global `monaco` object. Subsequent calls return +// immediately from the module-level promise cache. +let _monacoPromise = null; +function loadMonaco() { + if (_monacoPromise) return _monacoPromise; + _monacoPromise = new Promise((resolve, reject) => { + if (window.monaco) { resolve(window.monaco); return; } + + // Loader script + const loaderScript = document.createElement("script"); + loaderScript.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"; + loaderScript.onload = () => { + window.require.config({ + paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs" }, + }); + window.require(["vs/editor/editor.main"], () => { + // ── Register LogOctopus language ─────────────────────────────────── + const LANG = "logoctopus"; + if (!window.monaco.languages.getLanguages().some((l) => l.id === LANG)) { + window.monaco.languages.register({ id: LANG }); + + window.monaco.languages.setMonarchTokensProvider(LANG, { + tokenizer: { + root: [ + // Timestamp [2024-01-01 12:00:00] + [/\[\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*\]/, "log.timestamp"], + // Device column [device_name] + [/\[[^\]]+\](?=\s*\[)/, "log.device"], + // Log-name column [log_name] + [/\[[^\]]+\](?!\s*\[)/, "log.logname"], + // Severity keywords + [/\bERROR\b/, "log.error"], + [/\bWARN(?:ING)?\b/, "log.warn"], + [/\bINFO\b/, "log.info"], + [/\bDEBUG\b/, "log.debug"], + [/\bCRITICAL\b|\bFATAL\b/, "log.critical"], + // IP addresses + [/\b\d{1,3}(?:\.\d{1,3}){3}\b/, "log.ip"], + // Hex / numbers + [/\b0x[0-9a-fA-F]+\b/, "log.hex"], + [/\b\d+\b/, "log.number"], + // Quoted strings + [/"[^"]*"/, "log.string"], + [/'[^']*'/, "log.string"], + ], + }, + }); + + // ── Dark theme ───────────────────────────────────────────────── + window.monaco.editor.defineTheme("logoctopus-dark", { + base: "vs-dark", + inherit: true, + rules: [ + { token: "log.timestamp", foreground: "6b7280" }, + { token: "log.device", foreground: "818cf8", fontStyle: "bold" }, + { token: "log.logname", foreground: "22d3ee" }, + { token: "log.error", foreground: "f87171", fontStyle: "bold" }, + { token: "log.critical", foreground: "ff4444", fontStyle: "bold" }, + { token: "log.warn", foreground: "fbbf24", fontStyle: "bold" }, + { token: "log.info", foreground: "4ade80" }, + { token: "log.debug", foreground: "94a3b8" }, + { token: "log.ip", foreground: "fb923c" }, + { token: "log.hex", foreground: "a78bfa" }, + { token: "log.number", foreground: "f472b6" }, + { token: "log.string", foreground: "34d399" }, + ], + colors: { + "editor.background": "#09090f", + "editor.foreground": "#e4e4f0", + "editorLineNumber.foreground": "#3f3f5a", + "editorLineNumber.activeForeground": "#818cf8", + "editor.lineHighlightBackground": "#ffffff0a", + "editorCursor.foreground": "#818cf8", + "editor.selectionBackground": "#818cf820", + "editorBracketMatch.background": "#818cf830", + "scrollbar.shadow": "#00000000", + "scrollbarSlider.background": "#ffffff14", + "scrollbarSlider.hoverBackground": "#ffffff22", + }, + }); + + // ── Color-mode theme (per-logname background stripes) ────────── + window.monaco.editor.defineTheme("logoctopus-color", { + base: "vs-dark", + inherit: true, + rules: [ + { token: "log.timestamp", foreground: "6b7280" }, + { token: "log.device", foreground: "c084fc", fontStyle: "bold" }, + { token: "log.logname", foreground: "67e8f9" }, + { token: "log.error", foreground: "fca5a5", fontStyle: "bold" }, + { token: "log.critical", foreground: "ff6666", fontStyle: "bold" }, + { token: "log.warn", foreground: "fde68a", fontStyle: "bold" }, + { token: "log.info", foreground: "86efac" }, + { token: "log.debug", foreground: "cbd5e1" }, + { token: "log.ip", foreground: "fdba74" }, + { token: "log.hex", foreground: "c4b5fd" }, + { token: "log.number", foreground: "f9a8d4" }, + { token: "log.string", foreground: "6ee7b7" }, + ], + colors: { + "editor.background": "#0d0d1a", + "editor.foreground": "#f0f0ff", + "editorLineNumber.foreground": "#4040608", + "editorLineNumber.activeForeground": "#a78bfa", + "editor.lineHighlightBackground": "#ffffff0d", + "editorCursor.foreground": "#a78bfa", + "editor.selectionBackground": "#a78bfa25", + "scrollbarSlider.background": "#ffffff18", + "scrollbarSlider.hoverBackground": "#ffffff28", + }, + }); + } + resolve(window.monaco); + }); + }; + loaderScript.onerror = reject; + document.head.appendChild(loaderScript); + }); + return _monacoPromise; +} + +// ── LINE-NUMBER COLOR PALETTE ───────────────────────────────────────────────── +// Each unique (device_name, log_name) pair gets a stable color from this list. +const LN_COLOR_PALETTE = [ + "#818cf8", // indigo + "#34d399", // emerald + "#fb923c", // orange + "#f472b6", // pink + "#60a5fa", // blue + "#a78bfa", // violet + "#facc15", // yellow + "#2dd4bf", // teal + "#f87171", // red + "#c084fc", // purple +]; + +// Singleton +
+
Change Admin Password
+
+
+
Current Password
+ { setCurPass(e.target.value); setPwError(""); }} + placeholder="Current password" style={inputStyle} /> +
+
+
New Password
+ { setNewPass(e.target.value); setPwError(""); }} + placeholder="Min. 6 characters" style={inputStyle} /> +
+
+
Confirm New Password
+ { setConfPass(e.target.value); setPwError(""); }} + onKeyDown={e => e.key === "Enter" && submitPasswordChange()} + placeholder="Repeat new password" style={inputStyle} /> +
+ {pwError && ( +
+ ⚠ {pwError} +
+ )} + + Update Password + +
+
+ +
+
Session
+
+ Currently signed in as admin. Session persists until you sign out or close the browser. +
+ { auth.logout(); onClose(); addToast("Signed out.", "info"); }}> + Sign Out + +
+
+ )} + + )} + + {/* ── NETWORK CAPTURE / DISSECTORS TAB ── */} + {tab === "dissectors" && ( +
+ {/* Info card */} +
+
+ 🌐 +
Custom Dissectors
+
+
+ Upload Lua scripts (.lua) or compiled plugin libraries (.so / .dll). + tshark loads them automatically when decoding packet details, enriching the protocol tree with your custom fields. +
+
+ + {/* Upload area */} +
+
Upload dissector files
+ +
!dissectorUploading && dissectorFileRef.current?.click()} + style={{ + border: `2px dashed ${dissectorUploading ? "rgba(34,211,238,0.5)" : "var(--border)"}`, + borderRadius: 10, + padding: "24px 20px", + textAlign: "center", + cursor: dissectorUploading ? "wait" : "pointer", + background: dissectorUploading ? "rgba(34,211,238,0.04)" : "rgba(255,255,255,0.02)", + transition: "all 0.18s", + }} + onMouseEnter={e => { if (!dissectorUploading) e.currentTarget.style.borderColor = "rgba(34,211,238,0.45)"; e.currentTarget.style.background = "rgba(34,211,238,0.04)"; }} + onMouseLeave={e => { if (!dissectorUploading) e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.background = "rgba(255,255,255,0.02)"; }} + > + {dissectorUploading ? ( +
+ + + + Uploading… +
+ ) : ( + <> +
πŸ“‚
+
+ Click to upload .lua, .so, or .dll files +
+
+ Multiple files supported +
+ + )} +
+ {dissectorError && ( +
+ ⚠ {dissectorError} +
+ )} +
+ + {/* Installed dissectors list */} +
+
+
Installed dissectors ({dissectors.length})
+ +
+ + {dissectorsLoading ? ( +
+ + + + Loading… +
+ ) : dissectors.length === 0 ? ( +
+ No dissectors installed β€” upload a Lua script above. +
+ ) : ( +
+ {dissectors.map(d => ( +
+ {/* Extension chip */} + + {d.extension.replace(".", "")} + + {/* Name */} + + {d.name} + + {/* Size */} + + {d.size_bytes < 1024 ? `${d.size_bytes} B` : `${(d.size_bytes / 1024).toFixed(1)} kB`} + + {/* Delete */} + +
+ ))} +
+ )} +
+
+ )} + + + + ); +} + +// ── LOGIN MODAL ─────────────────────────────────────────────────────────────── +function LoginModal({ open, onClose, onLogin }) { + const [user, setUser] = useState(""); + const [pass, setPass] = useState(""); + const [error, setError] = useState(""); + const [shaking, setShaking] = useState(false); + const [loading, setLoading] = useState(false); + + // FIX: onLogin is now async (calls /api/auth/login). Handle the Promise and + // surface network errors rather than silently showing "Invalid credentials". + const attempt = async () => { + if (loading) return; + setLoading(true); + try { + const ok = await onLogin(user, pass); + if (ok) { + setUser(""); setPass(""); setError(""); onClose(); + } else { + setError("Invalid credentials"); + setShaking(true); + setTimeout(() => setShaking(false), 500); + } + } catch (e) { + setError(`Login error: ${e.message}`); + setShaking(true); + setTimeout(() => setShaking(false), 500); + } finally { + setLoading(false); + } + }; + + if (!open) return null; + + return ( +
{ if (e.target === e.currentTarget) onClose(); }} + > +
+
+ +
+
+ πŸ” +
+

+ Admin Login +

+

+ Required to view device configuration +

+
+ +
+ setUser(e.target.value)} + placeholder="Username" + autoFocus + onKeyDown={(e) => e.key === "Enter" && attempt()} + style={inputStyle} + /> + setPass(e.target.value)} + placeholder="Password" + onKeyDown={(e) => e.key === "Enter" && attempt()} + style={inputStyle} + /> + {error && ( +
+ ⚠ {error} +
+ )} + + {loading ? "Signing in…" : "Sign In"} + + + Cancel + +
+
+
+ ); +} + +const inputStyle = { + background: "rgba(255,255,255,0.04)", + border: "1px solid var(--border)", + borderRadius: 8, + color: "var(--text)", + fontFamily: "var(--font-mono)", + fontSize: 13, + padding: "10px 14px", + outline: "none", + width: "100%", + boxSizing: "border-box", +}; + +// ── MODAL ───────────────────────────────────────────────────────────────────── +function Modal({ open, onClose, title, size = "lg", children, footer }) { + useEffect(() => { + const handler = (e) => { if (e.key === "Escape") onClose(); }; + if (open) document.addEventListener("keydown", handler); + return () => document.removeEventListener("keydown", handler); + }, [open, onClose]); + + if (!open) return null; + const widths = { sm: 480, md: 600, lg: 860, xl: 1100, full: "calc(100vw - 40px)" }; + return ( +
{ if (e.target === e.currentTarget) onClose(); }} + > +
+
+
+

+ {title} +

+ +
+
+ {children} +
+ {footer && ( +
+ {footer} +
+ )} +
+
+ ); +} + +// ── CONFIRM DIALOG ──────────────────────────────────────────────────────────── +/** + * Generic confirmation dialog for destructive actions (device removal, + * snapshot removal, etc). Built on top of the shared `Modal` component. + * + * Props: + * open - whether the dialog is visible + * title - modal title (e.g. "Remove Devices?") + * message - short description of what will happen + * count - optional number of items affected, shown as a badge-like line + * itemLabel - noun describing the affected items, e.g. "device" / "snapshot" + * confirmLabel - label for the confirm button (default "Remove") + * onConfirm - called when the user confirms + * onCancel - called when the user cancels / closes the dialog + * loading - disables buttons and shows a busy confirm label while pending + */ +function ConfirmDialog({ open, title, message, count, itemLabel = "item", confirmLabel = "Remove", onConfirm, onCancel, loading = false }) { + return ( + {} : onCancel} + title={title} + size="sm" + footer={ + <> + + {loading ? "Removing…" : `πŸ—‘ ${confirmLabel}`} + + Cancel + + } + > +
+

+ {message} +

+ {typeof count === "number" && ( +

+ {count} {itemLabel}{count !== 1 ? "s" : ""} selected +

+ )} +

+ This action cannot be undone. +

+
+
+ ); +} + +// ── BADGE ───────────────────────────────────────────────────────────────────── +function Badge({ color = "default", children }) { + const colors = { + green: { bg: "rgba(74,222,128,0.13)", text: "#4ade80", border: "rgba(74,222,128,0.28)" }, + red: { bg: "rgba(248,113,113,0.13)", text: "#f87171", border: "rgba(248,113,113,0.28)" }, + yellow: { bg: "rgba(251,191,36,0.13)", text: "#fbbf24", border: "rgba(251,191,36,0.28)" }, + cyan: { bg: "rgba(129,140,248,0.14)", text: "#818cf8", border: "rgba(129,140,248,0.3)" }, + violet: { bg: "rgba(167,139,250,0.14)", text: "#a78bfa", border: "rgba(167,139,250,0.3)" }, + default: { bg: "rgba(255,255,255,0.06)", text: "var(--muted)", border: "var(--border)" }, + }; + const c = colors[color] || colors.default; + return ( + + {children} + + ); +} + +// ── BUTTON ──────────────────────────────────────────────────────────────────── +function Btn({ variant = "default", size = "md", onClick, disabled, children, style }) { + const base = { + cursor: disabled ? "not-allowed" : "pointer", + border: "none", + borderRadius: 8, + fontFamily: "var(--font-display)", + fontWeight: 600, + letterSpacing: "0.03em", + transition: "all 0.15s", + display: "inline-flex", + alignItems: "center", + gap: 7, + opacity: disabled ? 0.5 : 1, + }; + const sizes = { + sm: { padding: "5px 12px", fontSize: 12 }, + md: { padding: "9px 18px", fontSize: 13 }, + lg: { padding: "12px 24px", fontSize: 14 }, + }; + const variants = { + default: { background: "var(--card-bg)", color: "var(--text)", border: "1px solid var(--border)" }, + primary: { background: "var(--accent)", color: "#06061a" }, + success: { background: "#4ade80", color: "#06061a" }, + danger: { background: "#f87171", color: "#06061a" }, + ghost: { background: "transparent", color: "var(--muted)", border: "1px solid var(--border)" }, + subtle: { background: "rgba(255,255,255,0.05)", color: "var(--text)", border: "1px solid var(--border)" }, + admin: { background: "rgba(167,139,250,0.13)", color: "#a78bfa", border: "1px solid rgba(167,139,250,0.3)" }, + }; + return ( + + ); +} + +// ── TOAST ───────────────────────────────────────────────────────────────────── +function Toast({ message, type = "error", onDismiss }) { + useEffect(() => { + const t = setTimeout(onDismiss, 4000); + return () => clearTimeout(t); + }, [onDismiss]); + const colors = { + error: { bg: "rgba(248,113,113,0.12)", border: "rgba(248,113,113,0.3)", text: "#f87171" }, + success: { bg: "rgba(74,222,128,0.12)", border: "rgba(74,222,128,0.3)", text: "#4ade80" }, + info: { bg: "rgba(129,140,248,0.12)", border: "rgba(129,140,248,0.3)", text: "#818cf8" }, + }; + const c = colors[type] || colors.info; + return ( +
+ {message} + +
+ ); +} + +// ── DEVICE GROUP ────────────────────────────────────────────────────────────── +function DeviceGroup({ group, groupDevices, collapsed, onToggleCollapse, selectedDevices, onSelect, onSelectAll, onInfo, onAutoCollectionSave, onDropDevice, onRemoveDevice, onReorderDevice, onRename, onDelete, addToast, isUngrouped }) { + const [dragOver, setDragOver] = useState(false); + const [editingName, setEditingName] = useState(false); + const [nameInput, setNameInput] = useState(group.name || ""); + // Intra-group card reordering state + const [dragSrcId, setDragSrcId] = useState(null); + const [dragOverId, setDragOverId] = useState(null); + + const handleDragOver = (e) => { e.preventDefault(); setDragOver(true); }; + const handleDragLeave = () => setDragOver(false); + const handleDrop = (e) => { + e.preventDefault(); + setDragOver(false); + setDragOverId(null); + const deviceId = e.dataTransfer.getData("deviceId"); + const srcGroup = e.dataTransfer.getData("srcGroupId"); + if (!deviceId) return; + // Intra-group reorder: source and target group are the same + if (srcGroup === group.id && dragSrcId && dragOverId && dragSrcId !== dragOverId && onReorderDevice) { + onReorderDevice(group.id, dragSrcId, dragOverId); + } else if (isUngrouped) { + onRemoveDevice(deviceId); + } else { + onDropDevice(deviceId, group.id); + } + setDragSrcId(null); + }; + + const commitRename = () => { + const n = nameInput.trim(); + if (n && onRename) { + const ok = onRename(n); + if (ok !== false) setEditingName(false); // keep editor open if rejected + } else { + setEditingName(false); + } + }; + + const showHeader = group.name !== null; + const isCollapsed = collapsed && showHeader && !isUngrouped; + // Selected count in this group + const selectedCount = groupDevices.filter(d => selectedDevices.includes(d.id)).length; + const allSelected = groupDevices.length > 0 && selectedCount === groupDevices.length; + const someSelected = selectedCount > 0 && !allSelected; + + const handleGroupCheckbox = (e) => { + if (onSelectAll) onSelectAll(groupDevices.map(d => d.id), e.target.checked); + }; + + return ( +
+ {showHeader && ( +
+ {/* Group select-all checkbox */} + {groupDevices.length > 0 && ( + { if (el) el.indeterminate = someSelected; }} + onChange={handleGroupCheckbox} + title={allSelected ? "Deselect all in group" : "Select all in group"} + style={{ width: 15, height: 15, accentColor: "var(--accent)", cursor: "pointer", flexShrink: 0 }} + /> + )} + + {/* Collapse toggle chevron */} + {!isUngrouped && onToggleCollapse && ( + + )} + + {/* Drag-drop hint icon */} + ⊞ + + {editingName ? ( + setNameInput(e.target.value)} + onKeyDown={e => { if (e.key === "Enter") commitRename(); if (e.key === "Escape") setEditingName(false); }} + onBlur={commitRename} + style={{ ...inputStyle, width: 180, padding: "4px 8px", fontSize: 12, display: "inline" }} + /> + ) : ( + { if (onRename) { setNameInput(group.name); setEditingName(true); } }} + title={onRename ? "Double-click to rename" : undefined} + > + {group.name} + + )} + + + {groupDevices.length} device{groupDevices.length !== 1 ? "s" : ""} + {isCollapsed && selectedCount > 0 && ( + · {selectedCount} selected + )} + + + {dragOver && ( + + Drop to {isUngrouped ? "ungroup" : "add"} + + )} + + {onDelete && ( + + )} +
+ )} + + {!isCollapsed && ( + groupDevices.length === 0 && showHeader ? ( +
+ Drag device cards here +
+ ) : ( +
+ {groupDevices.map(d => ( +
{ e.preventDefault(); e.stopPropagation(); setDragOverId(d.id); }} + onDragLeave={(e) => { e.stopPropagation(); setDragOverId(null); }} + style={{ + outline: dragOverId === d.id && dragSrcId !== d.id + ? "2px dashed var(--accent)" + : "2px solid transparent", + borderRadius: 14, + opacity: dragSrcId === d.id ? 0.45 : 1, + transition: "opacity 0.15s, outline 0.1s", + }} + > + onSelect(d.id, checked)} + onInfo={() => onInfo(d)} + onAutoCollectionSave={onAutoCollectionSave} + onDragStart={() => { setDragSrcId(d.id); }} + onDragEnd={() => { setDragSrcId(null); setDragOverId(null); }} + srcGroupId={group.id} + addToast={addToast} + /> +
+ ))} +
+ ) + )} +
+ ); +} + +// ── DEVICE CARD ─────────────────────────────────────────────────────────────── +function DeviceCard({ device, selected, onSelect, onInfo, onAutoCollectionSave, onDragStart, onDragEnd, srcGroupId, addToast }) { + const [hovered, setHovered] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const [autoEnabled, setAutoEnabled] = useState(device.autoCollectionEnabled ?? false); + const [intervalHours, setIntervalHours] = useState(device.autoCollectionInterval ?? 1); + const [saving, setSaving] = useState(false); + + // Sync if device prop changes (e.g. after a poll refresh) + useEffect(() => { + setAutoEnabled(device.autoCollectionEnabled ?? false); + setIntervalHours(device.autoCollectionInterval ?? 1); + }, [device.autoCollectionEnabled, device.autoCollectionInterval]); + + const saveAutoCollection = async () => { + setSaving(true); + try { + await apiFetch("/api/settings/auto-collection", { + method: "POST", + body: JSON.stringify({ enabled: autoEnabled, interval_hours: intervalHours, device_ids: [device.id] }), + }); + onAutoCollectionSave?.(device.id, autoEnabled, intervalHours); + addToast?.( + autoEnabled + ? `Auto-collection on "${device.name}" β€” every ${intervalHours}h.` + : `Auto-collection disabled for "${device.name}".`, + "success" + ); + } catch (e) { + addToast?.(`Failed to save: ${e.message}`); + } finally { + setSaving(false); + } + }; + + const INTERVALS = [1, 2, 4, 6, 12, 24]; + + return ( +
{ + e.dataTransfer.setData("deviceId", device.id); + e.dataTransfer.setData("srcGroupId", srcGroupId || ""); + e.dataTransfer.effectAllowed = "move"; + onDragStart?.(); + }} + onDragEnd={() => onDragEnd?.()} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + position: "relative", + width: 220, + borderRadius: 12, + background: selected ? "var(--accent-dim)" : "var(--card-bg)", + border: `1px solid ${selected ? "var(--accent)" : hovered ? "var(--accent-border)" : "var(--border)"}`, + transition: "all 0.2s", + cursor: "grab", + boxShadow: selected ? "0 0 20px rgba(129,140,248,0.1)" : "none", + overflow: "hidden", + }} + > + {/* ── Card header ── */} +
+ onSelect(e.target.checked)} + style={{ position: "absolute", top: 12, left: 12, width: 16, height: 16, accentColor: "var(--accent)", cursor: "pointer" }} + /> + + {/* Info + Settings toggle buttons */} +
+ {/* Pcap collection ongoing indicator β€” shown left of the info button + whenever this device is collecting AND has a packets_capture_config. + Rendered as an animated triangular dorsal fin (as seen breaking + the water's surface) evoking Wireshark. */} + {device.collecting && device.config?.packets_capture_config && ( + + + + + + + + + + + + + + + + )} + + +
+ +
+ {device.name} +
+ +
+ + + +
+ + {/* Auto-collection active badge */} + {autoEnabled && ( +
+ + + Auto collection - {intervalHours}h + +
+ )} + + {/* Network capture indicator now lives solely next to the info button above. */} +
+ + {/* ── Auto-collection settings panel ── */} + {settingsOpen && ( +
+ {/* Enable toggle */} +
+ Auto-collection + +
+ + {/* Interval grid */} +
+
+ Interval +
+
+ {INTERVALS.map(h => { + const active = intervalHours === h; + return ( + + ); + })} +
+
+ + {/* Save button */} + +
+ )} +
+ ); +} + +function StatusRow({ label, ok, pulseWhenTrue }) { + return ( +
+ + {label} + {ok ? "βœ”" : "βœ–"} +
+ ); +} + +// ── SNAPSHOTS TABLE ─────────────────────────────────────────────────────────── +// ── SNAPSHOTS PAGINATION ────────────────────────────────────────────────────── +function SnapshotsPagination({ page, totalPages, total, pageSize, onPage }) { + const from = total === 0 ? 0 : (page - 1) * pageSize + 1; + const to = Math.min(page * pageSize, total); + + const btnStyle = (active, disabled) => ({ + display: "inline-flex", alignItems: "center", justifyContent: "center", + minWidth: 32, height: 28, padding: "0 8px", + background: active ? "rgba(129,140,248,0.18)" : "transparent", + border: `1px solid ${active ? "rgba(129,140,248,0.45)" : "var(--border)"}`, + borderRadius: 6, + color: active ? "var(--accent)" : disabled ? "var(--muted)" : "var(--text)", + fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: active ? 700 : 400, + cursor: disabled ? "not-allowed" : "pointer", + opacity: disabled ? 0.4 : 1, + transition: "all 0.12s", + }); + + // Build page list: always first/last + a window around current, with ellipsis + const WINDOW = 2; + const pages = []; + for (let i = 1; i <= totalPages; i++) { + if (i === 1 || i === totalPages || (i >= page - WINDOW && i <= page + WINDOW)) { + pages.push(i); + } else if (pages[pages.length - 1] !== "...") { + pages.push("..."); + } + } + + return ( +
+ {total === 0 ? "No results" : `${from}–${to} of ${total}`} +
+ + {pages.map((p, i) => + p === "..." ? ( + … + ) : ( + + ) + )} + +
+
+ ); +} + +// Strips a trailing ".123456"-style fractional-seconds suffix from a +// "YYYY-MM-DD HH:MM:SS.ffffff"-style timestamp string so it displays +// without decimal places. Non-string / already-plain values pass through. +function trimFractionalSeconds(ts) { + if (typeof ts !== "string") return ts; + return ts.split(".")[0]; +} + +function SnapshotsTable({ snapshots, selected, onSelect, onView }) { + if (snapshots.length === 0) { + return ( +
+ β€” no log snapshots β€” +
+ ); + } + const cols = ["", "Device", "Log Name", "Started", "Finished", "Duration", "Size", "Session ID", "Scenario", ""]; + return ( +
+ + + + {cols.map((c, i) => ( + + ))} + + + + {snapshots.map((s) => ( + (e.currentTarget.style.background = "rgba(255,255,255,0.03)")} + onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")} + > + + + + + + + + + + + + ))} + +
+ {c} +
+ onSelect(s.id, e.target.checked)} + style={{ accentColor: "var(--accent)", width: 14, height: 14 }} + /> + {s.deviceName}{s.logName}{trimFractionalSeconds(s.startTime)}{trimFractionalSeconds(s.finishTime)}{Math.round(Number(s.duration) || 0)}s{s.sizeKb} kB + + {s.sessionId} + + + {s.sessionScenario ? ( + {s.sessionScenario} + ) : ( + β€” + )} + + onView([s])}>View +
+
+ ); +} + +// ── SPINNER ─────────────────────────────────────────────────────────────────── +function Spinner() { + return ( +
+ + + + Loading… +
+ ); +} + +// ── REST API DOCS ───────────────────────────────────────────────────────────── +function ApiDocs() { + const [active, setActive] = useState(0); + const [copied, setCopied] = useState(null); + + const copy = (text, key) => { + navigator.clipboard.writeText(text).then(() => { + setCopied(key); + setTimeout(() => setCopied(null), 1600); + }); + }; + + const pyCode = `import requests + +BASE = "${API_BASE}" + +# Start collection +r = requests.post(f"{BASE}/api/start-logs-collection", + json={"selected_devices": ["device_1", "device_2"], "session_scenario": "example_test_scenario"}) +session_id = r.json()["session_id"] + +# Stop collection +requests.post(f"{BASE}/api/stop-logs-collection", + json={"selected_devices": ["device_1"], "session_id": session_id})`; + + const endpoints = [ + { + method: "GET", path: "/api/devices", + desc: "Return the list of all managed devices with their current statuses.", + req: null, + res: `[\n {\n "id": "abc123",\n "name": "Router-Alpha",\n "connection": true,\n "logAccess": true,\n "collecting": false\n }\n]`, + }, + { + method: "POST", path: "/api/devices", + desc: "Add a new device from a base64-encoded JSON config file.", + req: `{\n "contents": "data:application/json;base64,"\n}`, + res: `{\n "device": { "id": "abc123", "name": "Router-Alpha" }\n}`, + }, + { + method: "DELETE", path: "/api/devices/:id", + desc: "Remove a device and terminate its watchdog process.", + req: null, + res: `204 No Content`, + }, + { + method: "GET", path: "/api/snapshots", + desc: "List log snapshots. Supports optional filtering via query parameters: search_param (Device | Log Name | Session ID), search_value, log_type (text | chart).", + req: null, + res: `[\n {\n "id": 1,\n "deviceName": "Router-Alpha",\n "logName": "syslog",\n "startTime": "2024-01-01 10:00:00",\n "sessionId": "8cd7112719ac",\n "isChart": false\n }\n]`, + }, + { + method: "GET", path: "/api/snapshots/:id/content", + desc: "Retrieve full log content rows for a single snapshot.", + req: null, + res: `{\n "rows": [\n {\n "time": "2024-01-01 10:00:01",\n "log_name": "syslog",\n "content": "INFO kernel: started"\n }\n ]\n}`, + }, + { + method: "DELETE", path: "/api/snapshots", + desc: "Remove one or more snapshots and delete their underlying log files.", + req: `{\n "snapshot_ids": ["snap-001", "snap-002"],\n "log_type": "text"\n}`, + res: `{\n "removed": ["snap-001", "snap-002"],\n "not_found": []\n}`, + }, + { + method: "POST", path: "/api/start-logs-collection", + desc: "Start log collection on the specified devices.", + req: `{\n "selected_devices": ["device_1", "device_2"]\n}`, + res: `{\n "status": "logs collection started",\n "session_id": "8cd7112719ac"\n}`, + }, + { + method: "POST", path: "/api/stop-logs-collection", + desc: "Stop log collection and persist the collected snapshots.", + req: `{\n "selected_devices": ["device_1"],\n "session_id": "8cd7112719ac"\n}`, + res: `{\n "status": "logs collection stopped",\n "session_id": "8cd7112719ac",\n "text_logs_url": "http://...",\n "chart_logs_url": "http://..."\n}`, + }, + ]; + + const METHOD_COLORS = { + GET: { bg: "rgba(74,222,128,0.12)", text: "#4ade80", border: "rgba(74,222,128,0.25)" }, + POST: { bg: "rgba(129,140,248,0.12)", text: "#818cf8", border: "rgba(129,140,248,0.25)" }, + DELETE: { bg: "rgba(248,113,113,0.12)", text: "#f87171", border: "rgba(248,113,113,0.25)" }, + }; + + const ep = active < endpoints.length ? endpoints[active] : null; + const mc = ep ? METHOD_COLORS[ep.method] || METHOD_COLORS.GET : METHOD_COLORS.GET; + + return ( +
+ + {/* ── Sidebar ── */} +
+ {endpoints.map((e, i) => { + const mc2 = METHOD_COLORS[e.method] || METHOD_COLORS.GET; + const isActive = i === active; + return ( + + ); + })} + + {/* Python example link */} + +
+ + {/* ── Detail panel ── */} +
+ + {active < endpoints.length ? ( + <> + {/* Endpoint title */} +
+ {ep.method} + + {ep.path} + + +
+ + {/* Description */} +

+ {ep.desc} +

+ + {/* Request body */} + {ep.req && ( +
+
+ Request Body + +
+
{ep.req}
+
+ )} + + {/* Response */} +
+
+ Response + +
+
{ep.res}
+
+ + {/* Base URL reference */} +
+ Base URL: {API_BASE} +
+ + ) : ( + /* Python example panel */ + <> +
+ 🐍 + Python Quick-Start + +
+

+ Install requests via pip, then use the snippet below to start and stop log collection programmatically. +

+
{pyCode}
+ + )} +
+
+ ); +} + +// ── DEVICE DETAILS ──────────────────────────────────────────────────────────── +function DeviceDetails({ device, isAdmin, onRequestLogin }) { + const [configVisible, setConfigVisible] = useState(false); + const [errors, setErrors] = useState(null); + const [errorsLoading, setErrorsLoading] = useState(false); + const [errorsLoadError, setErrorsLoadError] = useState(null); + const [errorsVisible, setErrorsVisible] = useState(false); + + const handleShowConfig = () => { + if (!isAdmin) { onRequestLogin(); return; } + setConfigVisible((v) => !v); + }; + + const fetchErrors = async () => { + setErrorsLoading(true); + setErrorsLoadError(null); + try { + const url = `/api/devices/${encodeURIComponent(device.id)}/errors`; + const data = await apiFetch(url); + setErrors(data.errors || []); + } catch (e) { + // e.message is "Failed to fetch" for network errors, or the server's error string + setErrorsLoadError(e.message || "Failed to load error log"); + } finally { + setErrorsLoading(false); + } + }; + + const handleToggleErrors = () => { + if (!errorsVisible) { + setErrorsVisible(true); + if (errors === null) fetchErrors(); + } else { + setErrorsVisible(false); + } + }; + + + return ( +
+
+ {[ + ["Name", device.name], + ["Connection", device.connection ? "βœ… Online" : "❌ Offline"], + ["Log Access", device.logAccess ? "βœ… Yes" : "❌ No"], + ["Collecting", device.collecting ? "🟒 Active" : "🟑 Idle"], + ].map(([k, v]) => ( +
+
{k}
+
{v}
+
+ ))} +
+ + {/* Config section β€” guarded by admin role */} +
+
+

+ JSON Configuration +

+ + {isAdmin + ? configVisible ? "πŸ™ˆ Hide" : "πŸ‘ Show" + : "πŸ” Admin only"} + +
+ + {!isAdmin && ( +
+ πŸ”’ +
+
Configuration is restricted
+
Sign in as admin to view the raw device JSON configuration.
+
+ + Sign In + +
+ )} + + {isAdmin && configVisible && device.config && ( +
+            {JSON.stringify(device.config, null, 2)}
+          
+ )} + + {isAdmin && configVisible && !device.config && ( +

No configuration data available.

+ )} +
+ + {/* Error Logs section */} +
+
+

+ Error Logs +

+ {errors !== null && errors.length > 0 && ( + + {errors.length} + + )} + + {errorsVisible ? "Hide" : "Show"} + + {errorsVisible && ( + + {errorsLoading ? "..." : "Refresh"} + + )} +
+ + {errorsVisible && ( +
+ {errorsLoading && } + + {!errorsLoading && errorsLoadError && ( +
+ Failed to load: {errorsLoadError} +
+ )} + + {!errorsLoading && !errorsLoadError && errors !== null && errors.length === 0 && ( +
+ No errors recorded for this device. +
+ )} + + {!errorsLoading && !errorsLoadError && errors !== null && errors.length > 0 && ( +
+ + + + + + + + + {errors.map((err, i) => ( + + + + + ))} + +
+ Time + + Error +
+ {err.time} + + {err.error_info} +
+
+ )} +
+ )} +
+
+ ); +} + +// ── UPLOAD BUTTON ───────────────────────────────────────────────────────────── +// ── CONFIG BUILDER ──────────────────────────────────────────────────────────── + +const EMPTY_LOG_ENTRY = () => ({ + _id: Math.random().toString(36).slice(2), + log_name: "", + log_file_cmd: "", + data_extraction_regex: "", + log_activation_cmd: "", + log_deactivation_cmd: "", + custom_shell_prompt: "", + log_type: "text", + data_unit: "", + description: "", +}); + +const FIELD_LABEL = { + fontSize: 10, color: "var(--muted)", textTransform: "uppercase", + letterSpacing: "0.09em", fontFamily: "var(--font-mono)", marginBottom: 5, +}; + +const CARD = { + background: "var(--card-bg)", border: "1px solid var(--border)", + borderRadius: 12, padding: "18px 22px", marginBottom: 16, +}; + +// Convert Python-style (?P...) named groups to JS (?...) syntax +function pyRegexToJs(pattern) { + return pattern.replace(/\(\?P a.start - b.start); + + if (spans.length === 0) return ""; + + let pattern = "^"; + let cursor = 0; + + for (const span of spans) { + // Literal prefix between cursor and span start + if (span.start > cursor) { + pattern += escapeForRegex(line.slice(cursor, span.start)); + } + // Generalise the captured text into a pattern + const captured = line.slice(span.start, span.end); + const inner = generaliseCapture(captured); + pattern += `(?P<${span.group}>${inner})`; + cursor = span.end; + } + + // If the last span is ENTRY and it goes to end-of-line, anchor with .* + const lastSpan = spans[spans.length - 1]; + if (lastSpan.group === "ENTRY" && lastSpan.end >= line.length) { + // already captured to end + } else if (cursor < line.length) { + // literal suffix β€” omit for flexibility, just don't anchor end + } + + return pattern; +} + +function generaliseCapture(text) { + // Pure digits (and separators like : - /) β†’ timestamp-like pattern + if (/^[\d:.\-/ ]+$/.test(text)) { + // Build a pattern that allows digits, colons, dashes, slashes, dots, spaces + return "[\\d:.\\/\\- ]+"; + } + // Pure word characters β†’ \w+ + if (/^\w+$/.test(text)) return "\\w+"; + // Mix of word + common separators β†’ \S+ + if (!/\s/.test(text)) return "\\S+"; + // Has whitespace inside (e.g. "Jan 12 10:00:00") β†’ collapse runs + return text + .split(/(\s+)/) + .map(tok => tok.match(/^\s+$/) ? "\\s+" : (tok ? generaliseCapture(tok) : "")) + .join(""); +} + +// ── TERMINAL OUTPUT WITH REGEX HIGHLIGHTING ─────────────────────────────────── +function TerminalOutput({ lines, regex, regexError, onMarkSpan, markMode }) { + // markMode: null | "TIME" | "ENTRY" + // onMarkSpan(lineIdx, start, end, text) + + const handleMouseUp = (lineIdx, lineText) => { + if (!markMode) return; + const sel = window.getSelection(); + if (!sel || sel.isCollapsed) return; + const selected = sel.toString(); + if (!selected) return; + // Find the start offset within the line text + const start = lineText.indexOf(selected); + if (start === -1) return; + onMarkSpan(lineIdx, start, start + selected.length, selected); + sel.removeAllRanges(); + }; + + let re = null; + try { if (regex && !regexError) re = new RegExp(pyRegexToJs(regex)); } catch {} + + return ( +
+ {lines.map((line, i) => { + if (!line) return
; + if (!re) { + return ( +
handleMouseUp(i, line)} + style={{ color: "#94a3b8", padding: "1px 0" }}> + {line} +
+ ); + } + const m = re.exec(line); + if (!m) { + return ( +
handleMouseUp(i, line)} + style={{ color: "#374151", borderLeft: "2px solid rgba(255,255,255,0.04)", paddingLeft: 8, padding: "1px 0 1px 8px" }}> + {line} +
+ ); + } + // Render matched line with TIME/ENTRY segments highlighted + const groups = m.groups || {}; + const TIME = groups.TIME !== undefined ? groups.TIME : null; + const ENTRY = groups.ENTRY !== undefined ? groups.ENTRY : null; + const timeIdx = TIME != null ? line.indexOf(TIME) : -1; + const entryIdx = ENTRY != null ? line.indexOf(ENTRY, timeIdx >= 0 ? timeIdx + TIME.length : 0) : -1; + + const parts = []; + let cur = 0; + const addPart = (end, color, label) => { + if (cur < end) { + parts.push({ text: line.slice(cur, end), color, label }); + cur = end; + } + }; + // Build parts in order + const segs = []; + if (TIME != null && timeIdx >= 0) segs.push({ start: timeIdx, end: timeIdx + TIME.length, color: "#f59e0b", label: "TIME" }); + if (ENTRY != null && entryIdx >= 0) segs.push({ start: entryIdx, end: entryIdx + ENTRY.length, color: "#86efac", label: "ENTRY" }); + segs.sort((a, b) => a.start - b.start); + for (const seg of segs) { + if (seg.start > cur) parts.push({ text: line.slice(cur, seg.start), color: "#4b5563", label: null }); + parts.push({ text: line.slice(seg.start, seg.end), color: seg.color, label: seg.label }); + cur = seg.end; + } + if (cur < line.length) parts.push({ text: line.slice(cur), color: "#4b5563", label: null }); + + return ( +
handleMouseUp(i, line)} + style={{ display: "flex", flexWrap: "wrap", gap: 0, alignItems: "baseline", padding: "1px 0" }}> + {parts.map((p, pi) => ( + {p.text} + ))} +
+ ); + })} +
+ ); +} + +// ── LOG ENTRY EDITOR ────────────────────────────────────────────────────────── +function LogEntryEditor({ entry, conn, index, onChange, onRemove, onDuplicate }) { + const [expanded, setExpanded] = useState(index === 0); + const [activeTab, setActiveTab] = useState("collect"); // "collect" | "activate" | "deactivate" + + // Per-command terminal state + const [outputs, setOutputs] = useState({ collect: null, activate: null, deactivate: null }); + const [errors, setErrors] = useState({ collect: "", activate: "", deactivate: "" }); + const [running, setRunning] = useState(null); // null | "collect" | "activate" | "deactivate" + + // Regex builder state + const [markMode, setMarkMode] = useState(null); // null | "TIME" | "ENTRY" + const [markedLine, setMarkedLine] = useState(null); // index of line used for regex building + const [timeSpan, setTimeSpan] = useState(null); // { start, end, text } + const [entrySpan, setEntrySpan] = useState(null); // { start, end, text } + + // Local editable command strings (so the user can edit without committing on every keystroke) + const set = (k, v) => onChange({ ...entry, [k]: v }); + + const execCmd = async (tab) => { + const cmdMap = { collect: entry.log_file_cmd, activate: entry.log_activation_cmd, deactivate: entry.log_deactivation_cmd }; + const cmd = cmdMap[tab]; + if (!conn.ip_address || !conn.user) { + setErrors(p => ({ ...p, [tab]: "Fill in connection details first (Step 1)." })); + return; + } + if (!cmd || !cmd.trim()) { + setErrors(p => ({ ...p, [tab]: "No command entered." })); + return; + } + setRunning(tab); + setOutputs(p => ({ ...p, [tab]: null })); + setErrors(p => ({ ...p, [tab]: "" })); + try { + const payload = { + ip_address: conn.ip_address, port: Number(conn.port || 22), + user: conn.user, password: conn.password, + ssh_key_string: conn.ssh_key_string || "", + gateways: conn.gateways || [], command: cmd, + }; + if (entry.custom_shell_prompt?.trim()) payload.custom_shell_prompt = entry.custom_shell_prompt.trim(); + const res = await apiFetch("/api/devices/exec-command", { method: "POST", body: JSON.stringify(payload) }); + const out = res.stdout || res.stderr || "(empty output)"; + setOutputs(p => ({ ...p, [tab]: out })); + if (res.exit_code !== 0 && res.stderr) { + setErrors(p => ({ ...p, [tab]: `exit ${res.exit_code}: ${res.stderr.slice(0, 200)}` })); + } + } catch (e) { + setErrors(p => ({ ...p, [tab]: e.message })); + } finally { + setRunning(null); + } + }; + + // When user marks a span in the terminal, update timeSpan/entrySpan and rebuild regex + const handleMarkSpan = (lineIdx, start, end, text) => { + if (!markMode) return; + const span = { start, end, text }; + let newTime = timeSpan, newEntry = entrySpan; + if (markMode === "TIME") { newTime = span; setTimeSpan(span); setMarkedLine(lineIdx); } + if (markMode === "ENTRY") { newEntry = span; setEntrySpan(span); setMarkedLine(lineIdx); } + setMarkMode(null); + // Rebuild regex from the line + const lines = (outputs.collect || "").split("\n").filter(l => l.trim()); + const line = lines[markedLine != null && markMode === "ENTRY" ? markedLine : lineIdx] || ""; + const built = buildRegexFromSpans(line, markMode === "TIME" ? span : newTime, markMode === "ENTRY" ? span : newEntry); + if (built) set("data_extraction_regex", built); + }; + + const clearSpans = () => { setTimeSpan(null); setEntrySpan(null); setMarkedLine(null); setMarkMode(null); }; + + // Regex validation + let reErr = ""; + try { if (entry.data_extraction_regex) new RegExp(pyRegexToJs(entry.data_extraction_regex)); } + catch (e) { reErr = e.message; } + + const collectLines = (outputs.collect || "").split("\n"); + const matchCount = (() => { + if (!entry.data_extraction_regex || reErr) return null; + try { + const re = new RegExp(pyRegexToJs(entry.data_extraction_regex)); + const nonEmpty = collectLines.filter(l => l.trim()); + return { matched: nonEmpty.filter(l => re.test(l)).length, total: nonEmpty.length }; + } catch { return null; } + })(); + + // Tab config + const TABS = [ + { key: "collect", icon: "β–Ά", label: "collect", cmdKey: "log_file_cmd", hint: "Command that fetches log data" }, + { key: "activate", icon: "⚑", label: "activate", cmdKey: "log_activation_cmd", hint: "Runs before collection. Must exit 0 to enable. Use `true` to always enable." }, + { key: "deactivate", icon: "β›”", label: "deactivate", cmdKey: "log_deactivation_cmd", hint: "Runs on collection stop to disable the log source. Optional." }, + ]; + const currentTab = TABS.find(t => t.key === activeTab); + + // Styles + const S = { + terminal: { + background: "#0a0d12", border: "1px solid rgba(255,255,255,0.08)", + borderRadius: 8, overflow: "hidden", + }, + termBar: { + background: "#111520", borderBottom: "1px solid rgba(255,255,255,0.07)", + display: "flex", alignItems: "center", padding: "0 0 0 14px", gap: 0, + }, + termTab: (active, hasOutput, hasErr) => ({ + display: "flex", alignItems: "center", gap: 5, + fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: active ? 700 : 400, + padding: "7px 14px", cursor: "pointer", border: "none", + borderBottom: active ? "2px solid var(--accent)" : "2px solid transparent", + background: active ? "rgba(129,140,248,0.08)" : "transparent", + color: active ? "var(--accent)" : (hasErr ? "#f87171" : hasOutput ? "#4ade80" : "var(--muted)"), + transition: "all 0.12s", whiteSpace: "nowrap", + }), + promptLine: { + display: "flex", alignItems: "center", gap: 8, + padding: "8px 14px", borderTop: "1px solid rgba(255,255,255,0.05)", + background: "rgba(0,0,0,0.2)", + }, + runBtn: (busy) => ({ + display: "flex", alignItems: "center", gap: 6, + background: busy ? "rgba(129,140,248,0.06)" : "rgba(129,140,248,0.12)", + border: "1px solid rgba(129,140,248,0.3)", borderRadius: 5, + color: "var(--accent)", fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 700, + padding: "4px 12px", cursor: busy ? "not-allowed" : "pointer", + opacity: busy ? 0.6 : 1, flexShrink: 0, whiteSpace: "nowrap", + }), + cmdInput: { + flex: 1, background: "transparent", border: "none", outline: "none", + color: "#e2e8f0", fontFamily: "var(--font-mono)", fontSize: 11, + padding: 0, caretColor: "var(--accent)", + }, + markBtn: (active, color) => ({ + display: "flex", alignItems: "center", gap: 4, + background: active ? `${color}22` : "rgba(255,255,255,0.04)", + border: `1px solid ${active ? color : "rgba(255,255,255,0.1)"}`, + borderRadius: 4, color: active ? color : "var(--muted)", + fontFamily: "var(--font-mono)", fontSize: 10, fontWeight: 700, + padding: "3px 8px", cursor: "pointer", transition: "all 0.12s", whiteSpace: "nowrap", + }), + }; + + const logTypeColors = { text: "cyan", chart: "violet" }; + + return ( +
+ {/* ── Header ── */} +
setExpanded(v => !v)} + style={{ + display: "flex", alignItems: "center", gap: 10, padding: "10px 14px", + cursor: "pointer", background: expanded ? "rgba(129,140,248,0.04)" : "transparent", + borderBottom: expanded ? "1px solid rgba(255,255,255,0.07)" : "none", + }} + > + {/* index pill */} + {String(index + 1).padStart(2, "0")} + + {/* log name */} + + {entry.log_name || unnamed entry} + + + {/* badges */} + {entry.log_type} + {outputs.collect && collect βœ”} + {outputs.activate && activate βœ”} + {outputs.deactivate && deactivate βœ”} + {entry.data_extraction_regex && !reErr && matchCount && ( + 0 ? "green" : "red"}> + {matchCount.matched}/{matchCount.total} lines + + )} + + + + {expanded ? "β–²" : "β–Ό"} +
+ + {expanded && ( +
+ + {/* ── Row 1: Log Name / Type / Data Unit ── */} +
+
+
Log Name *
+ set("log_name", e.target.value)} + placeholder="e.g. syslog, cpu_usage" style={inputStyle} /> +
+
+
Type
+
+ {["text", "chart"].map(t => ( + + ))} +
+
+ {entry.log_type === "chart" && ( +
+
Unit
+ set("data_unit", e.target.value)} + placeholder="%, Β°C, ms…" style={{ ...inputStyle, width: 90 }} /> +
+ )} +
+ + {/* ── Description ── */} +
+
+ Description * +
+