mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
release-2.9.0 (#1532)
* fix(macos): skip single-instance lock in Mac App Store builds (#1454)
fix(macos): skip single-instance lock in Mac App Store builds
* Fix/backend optional sharp (#1455)
Fix/backend optional sharp
* feat: issue #1218 (#1218)
https://github.com/Termix-SSH/Support/issues/1218
* feat: Add option to use saved global custom theme under Connection Defaults (#1209)
https://github.com/Termix-SSH/Support/issues/1209
* feat: Add Fleet Sharing functionality to the UI (#1228)
https://github.com/Termix-SSH/Support/issues/1228
* feat: Add a list view to the Docker management page (#1237)
https://github.com/Termix-SSH/Support/issues/1237
* feat: issue #1264 (#1264)
https://github.com/Termix-SSH/Support/issues/1264
* feat: 2FA With FortiToken (#1288)
https://github.com/Termix-SSH/Support/issues/1288
* feat: Allow to disable showing paths to folders (#1274)
https://github.com/Termix-SSH/Support/issues/1274
* feat: Sync Network Graph between Desktop and Remote Server (#1245)
https://github.com/Termix-SSH/Support/issues/1245
* feat: Version Number on offline servers (#1291)
https://github.com/Termix-SSH/Support/issues/1291
* feat: Support OrbStack Docker socket path on macOS / Fix "Docker is not installed" on mac... (#1302)
https://github.com/Termix-SSH/Support/issues/1302
* feat: Sidebar host click should focus existing tab instead of opening a new connection (#1289)
https://github.com/Termix-SSH/Support/issues/1289
* fix: Search in side bar shows overlapping hosts when grouped by tags (#1303)
https://github.com/Termix-SSH/Support/issues/1303
* fix: I can't send a file on its own. (#1304)
https://github.com/Termix-SSH/Support/issues/1304
* fix: sudo password not autofilling (#1248)
https://github.com/Termix-SSH/Support/issues/1248
* fix: node-MainThread (#1300)
https://github.com/Termix-SSH/Support/issues/1300
* fix: Tunnel authentication with shared credentials (#1295)
https://github.com/Termix-SSH/Support/issues/1295
* fix: host key updates not syncing, metrics 404 race on first connect
Host key writes never bumped updatedAt so sync never picked them up.
Also retry the first metrics fetch briefly instead of failing right away.
* chore: increment ver
* feat: add category and icon fields to plugin manifest schema
* ci(deps): bump the github-actions group with 2 updates (#1458)
Bumps the github-actions group with 2 updates: [crowdin/github-action](https://github.com/crowdin/github-action) and [actions/github-script](https://github.com/actions/github-script).
Updates `crowdin/github-action` from 2 to 3
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/v2...v3)
Updates `actions/github-script` from 7 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v9)
---
updated-dependencies:
- dependency-name: crowdin/github-action
dependency-version: '3'
dependency-type: direct:production
update-type: version-update:semver-major
dependency-group: github-actions
- dependency-name: actions/github-script
dependency-version: '9'
dependency-type: direct:production
update-type: version-update:semver-major
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps-dev): bump the dev-patch-updates group with 13 updates (#1459)
Bumps the dev-patch-updates group with 13 updates:
| Package | From | To |
| --- | --- | --- |
| [@codemirror/commands](https://github.com/codemirror/commands) | `6.11.0` | `6.11.1` |
| [@codemirror/search](https://github.com/codemirror/search) | `6.7.1` | `6.7.2` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.43.9` | `6.43.12` |
| [@testing-library/dom](https://github.com/testing-library/dom-testing-library) | `10.4.1` | `10.4.2` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.2` | `16.3.3` |
| [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.5` | `14.6.7` |
| [@types/ssh2](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ssh2) | `1.15.5` | `1.15.6` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.1.0` | `6.1.1` |
| [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.34.1` | `3.34.3` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.4` | `0.5.7` |
| [i18next](https://github.com/i18next/i18next) | `26.4.0` | `26.4.2` |
| [prettier](https://github.com/prettier/prettier) | `3.9.6` | `3.9.8` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.12` | `17.0.14` |
Updates `@codemirror/commands` from 6.11.0 to 6.11.1
- [Changelog](https://github.com/codemirror/commands/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/commands/commits)
Updates `@codemirror/search` from 6.7.1 to 6.7.2
- [Changelog](https://github.com/codemirror/search/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/search/commits)
Updates `@codemirror/view` from 6.43.9 to 6.43.12
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)
Updates `@testing-library/dom` from 10.4.1 to 10.4.2
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v10.4.1...v10.4.2)
Updates `@testing-library/react` from 16.3.2 to 16.3.3
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.2...v16.3.3)
Updates `@testing-library/user-event` from 14.6.5 to 14.6.7
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.6.5...v14.6.7)
Updates `@types/ssh2` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ssh2)
Updates `@vitejs/plugin-react` from 6.1.0 to 6.1.1
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.1/packages/plugin-react)
Updates `cytoscape` from 3.34.1 to 3.34.3
- [Release notes](https://github.com/cytoscape/cytoscape.js/releases)
- [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.34.1...v3.34.3)
Updates `eslint-plugin-react-refresh` from 0.5.4 to 0.5.7
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.4...v0.5.7)
Updates `i18next` from 26.4.0 to 26.4.2
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.4.0...v26.4.2)
Updates `prettier` from 3.9.6 to 3.9.8
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.8)
Updates `react-i18next` from 17.0.12 to 17.0.14
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.12...v17.0.14)
---
updated-dependencies:
- dependency-name: "@codemirror/commands"
dependency-version: 6.11.1
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@codemirror/search"
dependency-version: 6.7.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@codemirror/view"
dependency-version: 6.43.12
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@testing-library/dom"
dependency-version: 10.4.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@testing-library/react"
dependency-version: 16.3.3
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@testing-library/user-event"
dependency-version: 14.6.7
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@types/ssh2"
dependency-version: 1.15.6
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
dependency-version: 6.1.1
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: cytoscape
dependency-version: 3.34.3
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: eslint-plugin-react-refresh
dependency-version: 0.5.7
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: i18next
dependency-version: 26.4.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: prettier
dependency-version: 3.9.8
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: react-i18next
dependency-version: 17.0.14
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump the prod-patch-updates group with 6 updates (#1461)
Bumps the prod-patch-updates group with 6 updates:
| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) | `3.14.10` | `3.14.13` |
| [compression](https://github.com/expressjs/compression) | `1.8.1` | `1.8.2` |
| [jose](https://github.com/panva/jose) | `6.2.9` | `6.2.12` |
| [jszip](https://github.com/Stuk/jszip) | `3.10.1` | `3.10.2` |
| [socks](https://github.com/JoshGlazebrook/socks) | `2.8.9` | `2.8.10` |
| [undici](https://github.com/nodejs/undici) | `8.10.0` | `8.10.2` |
Updates `@tanstack/react-virtual` from 3.14.10 to 3.14.13
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.14.13/packages/react-virtual)
Updates `compression` from 1.8.1 to 1.8.2
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/v1.8.1...v1.8.2)
Updates `jose` from 6.2.9 to 6.2.12
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v6.2.9...v6.2.12)
Updates `jszip` from 3.10.1 to 3.10.2
- [Changelog](https://github.com/Stuk/jszip/blob/main/CHANGES.md)
- [Commits](https://github.com/Stuk/jszip/compare/v3.10.1...v3.10.2)
Updates `socks` from 2.8.9 to 2.8.10
- [Release notes](https://github.com/JoshGlazebrook/socks/releases)
- [Commits](https://github.com/JoshGlazebrook/socks/compare/2.8.9...2.8.10)
Updates `undici` from 8.10.0 to 8.10.2
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.10.0...v8.10.2)
---
updated-dependencies:
- dependency-name: "@tanstack/react-virtual"
dependency-version: 3.14.13
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: compression
dependency-version: 1.8.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: jose
dependency-version: 6.2.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: jszip
dependency-version: 3.10.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: socks
dependency-version: 2.8.10
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: undici
dependency-version: 8.10.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat: add plugin worker protocol, bootstrap and plugin_storage table
Worker-side ctx proxy plus the message envelope it talks over, and the
per-plugin key/value table behind ctx.storage with migrations for all
three dialects. Rows cascade with the plugin so uninstalling leaves
nothing behind.
* feat: add allowlisted host view for plugins
An allowlist rather than a list of fields to strip. A blocklist rots:
the moment a new secret-bearing column lands on SSHHost it starts
leaking silently. With an allowlist a new field stays invisible to
plugins until someone adds it here on purpose.
* feat: add plugin loader with manifest validation and crash restart
Validates an unpacked plugin, spawns a worker_threads worker and manages
activate/deactivate/crash-restart with backoff, disabling after three
tries.
The crash counter clears only after a plugin stays up past a stability
window, not on activation. Clearing it on activation let a plugin that
activates cleanly and then dies restart forever, because every attempt
looked like the first.
loader.ts documents what the worker boundary does and does not buy you.
Node's permission model is process-wide, not per-worker, so it cannot be
enabled for plugin workers alone and is not claimed here.
* feat: route internal events through a shared plugin event bus
Formalises three ad-hoc publish paths that already existed:
automation-events.ts, metrics/automation-bridge.ts and the
hostSessionStatus singleton, now the host.session.status topic.
Both invariants the old code relied on are preserved. Publishing stays
fire-and-forget, so a failing subscriber cannot disturb a metrics poll or
a host delete. The automations engine subscribes to the bus at scheduler
start instead of being imported ad hoc, which keeps the edge
one-directional: automations may import repositories, hosts modules must
not import automations.
* feat: add plugin ctx broker with permission gate and audit
Every ctx call a plugin makes is checked against its granted
capabilities, performed by the main thread, then audited. A capability
must be both declared in the manifest and granted in the database, so
widening a plugin's reach always needs a new manifest the user can see,
never just a database row.
Audit attribution is set by the broker, not the plugin. The plugin
supplies only the details, so it cannot forge the actor.
fix: stop plugin workers seeing plaintext credentials in error messages
A failing ctx.ssh.connect handed the worker the raw error from deep in
the SSH stack, which embeds the connect config. The worker received, in
full:
Authentication failed for root using password <SECRET>
where <SECRET> was the host's real plaintext password. Any plugin with
ssh.exec could read it by catching the error, defeating the whole point
of the handle-based ssh API.
Errors crossing the boundary are now sanitised. Only PluginFacingError
and PluginPermissionError, both authored here, reach a plugin verbatim;
anything else becomes a generic message and the real one goes to the
log. This uses a marker class rather than matching on message text,
because matching would start leaking again the moment an upstream error
happened to contain a familiar phrase.
Covered by credential-isolation.test.ts, which plants a sentinel in
every secret-bearing field and asserts it appears in no message the
worker ever receives.
* feat: dispatch plugin HTTP routes and wire the runtime into startup
ctx.http.route now registers a real router on the /plugin-api/:pluginId
dispatcher, replacing its stub 404. A plugin never touches the Express
req/res: it gets a plain summary and returns a plain object, so sockets
and session cookies stay on this side of the boundary. Authorization and
cookie headers are not forwarded.
Plugins load last in the start-up sequence, after the server is fully
wired, and are terminated before the database on shutdown so none can
outlive it. A plugin that fails to load cannot stop the backend.
fix: correct the nginx plugin-api location regex
The pattern was ^/plugin-api/(/.*)?$, which needs a double slash and so
never matched /plugin-api/<id>/<route>. Every other block in the file
uses ^/prefix(/.*)?$. Left alone, plugin routes would work in dev and
404 only behind Docker.
* fix: update stale SFTP transfer test for single file destination paths
The test still asserted transferToHost was called with the destination
directory "/srv". Commit 7727fa09 (#1304) deliberately changed a single
file transfer to send the full destination path including the filename,
so the correct expectation is "/srv/remote-1.txt". The code was right
and the test was left behind.
* feat: ship the ssh terminal as a first-party in-process plugin
Adds a hardcoded process:transport-owner tier so the terminal can own its
WebSocket server and ssh2 clients, which cannot cross the worker boundary.
* feat: ship docker as a first-party in-process plugin
Moves docker backend and frontend into plugins/docker/, alongside
ssh-terminal. Adds a host editor tab seam, a dashboard card registry,
and a build step to compile plugin backends into dist.
* feat: ship host metrics as a first-party in-process plugin
* fix: harden plugin auth and add per-request identity for ctx calls
Require auth on /plugin-api, gate plugin enable/grant/revoke routes
behind admin.plugins.manage, and thread the calling user through
worker ctx.hosts/ctx.ssh calls instead of always using the install
owner. Also adds the plugin permissions grant/revoke UI.
* feat: ship the AI assistant as a first-party in-process plugin
Moves src/backend/ai and its routes into plugins/ai, mounted through a
small /ai dispatcher instead of its own port. Moves the ai-enabled and
ai-private-endpoints settings routes out of user-settings-routes.ts
into the plugin too, so it has no leftover core dependents.
* fix: remove compiled plugin JS left behind in plugins/ after build
copy-bundled-plugins.cjs compiled and rewrote plugin backend TS in
place, copied it to dist/plugins, but never deleted the .js it had
just emitted next to the .ts sources. Those files shadowed the real
source for vitest/tsx and had import paths rewritten for the dist
layout, so tests failed with "Cannot find module" once a local build
had run. Affects docker, host-metrics and ai, not just this one.
* feat: add a permission-gated backend service registry for plugins
* feat: add a permission-gated UI action registry and action slots for plugins
Moves the terminal toolbar's hardwired AI button into a slot the ssh-terminal
plugin offers and the ai plugin fills, gated on ai.services.use.
* feat: add cross-plugin secret references gated by the provider's RBAC permission
* feat: move proxmox discovery/import/sync into a first-party plugin
* feat: move remote desktop (RDP/VNC/Telnet) into a first-party plugin
Relocates guacamole backend/frontend source and tests into
plugins/remote-desktop, following the ssh-terminal/docker pattern.
* feat: wire remote-desktop plugin into core and rename i18n keys
Repoints core consumers (dispatcher, collab, session-sharing, tab
utils) at the plugin, adds isTabTypeAvailable gating for rdp/vnc/
telnet, widens hostCapability to accept an array, and renames the
guacamole i18n namespace to remoteDesktop.
* feat: move fleets into a first-party plugin
* feat: move automations into a first-party plugin
* feat: remove legacy alert rules, superseded by automations
Alert rules already auto-migrate into automations on boot and the old
engine stands down after. Deletes the rule/firing tables, routes, UI,
and AI tools; keeps notification channels since automations still use
them. Also fixes automations' notify step, which still called the
deleted repository.
* feat: move network topology into a first-party plugin
* feat: move tailscale device discovery and settings into a plugin
Adds plugins/tailscale/ with a devices sidebar tab, admin settings
section, and re-auth dialog. Host Metrics tailscale manager card and
SSH re-auth banner parsing stay in core.
* feat: move workspaces into a first-party plugin
* feat: move web endpoint into a first-party plugin
* feat: collapse plugin runtime to one tier behind a plugin SDK
* feat: make each plugin an npm workspace built by a termix-plugin CLI
* feat: give plugins their own tables, migrations and sync entities
* feat: serve plugin http and websockets from one mount instead of ports
* feat: namespace plugin permissions and keep them valid when a plugin is off
* feat: give plugins their own settings and one settings screen
* feat: load plugin frontends at runtime and remove plugin ids from the shell
* fix: mock the ai status fetch in its activate test
* feat: add auth extension points and one ssh connect pipeline
* feat: rebuild workspaces as the reference plugin
* fix: gate second factors after SSO/LDAP behind an admin setting
* feat: convert network-topology to a full plugin (B1)
* feat: move snippets backend and tables into a plugin (B2)
* feat: point core frontend snippets pickers at the plugin (B2)
* feat: add the snippets plugin (B2)
* feat: wire ai, automations, fleets and ssh-terminal to snippets (B2)
* feat: finish the tailscale plugin, including SSH auth (B3)
* feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)
* chore: update proxmox and host-metrics locale files for B5
* chore: regenerate drizzle migration journals for B5
* feat: add ctx.hosts create/update to the SDK, host-metrics drops proxmox
* feat: move proxmox node stats and collectors into the proxmox plugin
* feat: drop proxmox host columns, use plugin settings instead
* feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)
* feat: convert file manager into a plugin
* feat: convert ssh tunnels into a plugin
* feat: finish the web-endpoint plugin, add ctx.desktop (B8)
* feat: move the terminal fully into the ssh-terminal plugin (B9)
* feat: convert tmux monitoring into a plugin (B10)
* feat: convert serial console into the serial plugin
* feat: move session sharing and collab rooms into a plugin (B12)
* feat: turn session recording into a plugin (B13)
* feat: finish the remote-desktop plugin (B14)
* feat: finish the host-metrics plugin, move host status into core (B16)
* feat: finish the docker plugin (B15)
* feat: finish the automations plugin (B17)
* feat: finish the ai plugin (B18)
* feat: finish the homepage plugin, move service links and canvas out of core (B19)
* feat: convert wake-on-lan into a plugin (B20)
* feat: convert secret-sources into a plugin (B20)
* feat: move totp and passkeys into plugins (C1)
* feat: move sso and ldap logins into plugins (C2)
* feat: move opkssh and warpgate into plugins (C3)
* feat: move step-ca into a plugin (C4)
* feat: move vault ssh signing into a plugin (C5)
* feat: move termix identity into a plugin (C6)
* feat: move acme certificate automation into a plugin (C7)
* fix: finish 2.9.0 plugin moves and get every check green
* chore: lock the plugin boundary and remove 2.9.0 leftovers
* fix: harden the plugin runtime after the 2.9.0 security review
* feat: add plugin packing, signing, pinned trust and tmxplug bundling
* test: prove 2.8 to 2.9.0 upgrade is lossless and fix what it caught
* fix: give bundled plugin backends a real require for CJS deps
* feat: improve UI loading time and jitter
* feat: show credential sharing inline in the sidebar like host sharing
* feat: tailscale add host and copy ip, fix outbound fetch hang and plugin ESM warning
* feat: unify alerts into one plugin, channels, rules and inbox
* feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling
* fix: set blacksmith builder cache-key to stop broken gha cache fallback
* fix: link plugin-sdk workspace before npm ci in docker deps stage
* fix: link every plugin workspace before npm ci in docker deps stage
* fix(host-metrics): exclude image and firmware filesystems (#1469)
* fix(host-metrics): exclude image and firmware filesystems
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(hosts): normalize imported jump host references (#1470)
* fix(hosts): normalize imported jump host references
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(ci): build plugin SDK and repair MySQL settings migration (#1471)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): keep inline rename consistent (#1476)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): keep inline rename consistent
* fix(dashboard): reuse translated host status labels (#1475)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(dashboard): reuse translated host status labels
* docs(api): clarify local and remote version fields (#1474)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* docs(api): clarify local and remote version fields
* fix(file-manager): preserve selected backgrounds on hover (#1473)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): preserve selection feedback on hover
* test(ui): await dialog focus cleanup before teardown
* fix(file-manager): restore Ctrl/Cmd+F file search (#1472)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): focus file search with Ctrl or Cmd F
* fix: keep rounded-full circular in production builds
* fix(file-manager): cancel uploads and stop remote writes (#1477)
* fix(file-manager): cancel uploads and stop remote writes
* test(css): exercise the configured PostCSS plugin chain
* fix(file-manager): apply compact density to directory tree (#1478)
* fix(file-manager): apply compact density to directory tree
* test(css): exercise the configured PostCSS plugin chain
* fix(file-manager): preserve font sizes in compact mode (#1480)
* fix(file-manager): preserve font sizes in compact mode
* test(css): exercise the configured PostCSS plugin chain
* fix(file-manager): prioritize file names in narrow lists (#1483)
* fix(file-manager): prioritize file names in narrow lists
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* test(remote-desktop): always mutate ciphertext in tamper test
* fix(remote-desktop): probe the saved host guacd endpoint (#1481)
* fix(remote-desktop): probe the saved host guacd endpoint
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* fix(homepage): check allowlisted private service reachability (#1479)
* fix(homepage): check allowlisted private service reachability
* test(css): exercise the configured PostCSS plugin chain
* fix(remote-desktop): stop unsolicited clipboard read prompts (#1482)
* fix(remote-desktop): read clipboard only on explicit paste
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* test(remote-desktop): always mutate ciphertext in tamper test
* chore: simplify plugin readmes, changelogs and manifest descriptions
* fix: keep plugins loaded after signing in from the login screen
* fix(file-manager): accept OS file drops over existing rows (#1484)
* fix(file-manager): accept OS file drops over existing rows
* test(file-manager): target the file label in drop tests
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* test(remote-desktop): always mutate ciphertext in tamper test
* fix(file-manager): pipeline SFTP download reads (#1485)
* feat: add plugin sdk build to beta release
* fix: make link-store secret check immune to random base64 matches
* fix(ldap): preserve the directory-provided bind DN (#1486)
* fix: compute beta changes across dev branches and move the beta tag
* chore: untrack plugin sdk ARCHITECTURE.md
* chore: remove references to plugin sdk ARCHITECTURE.md
* feat: merge host editor terminal tabs into one owned by ssh-terminal
* feat: add descriptions to plugin host enable toggles
* fix(file-manager): show download progress in file viewers (#1487)
* fix(electron): signal backend readiness over IPC (#1488)
* fix(split-screen): allow layout creation without randomUUID (#1490)
* fix(split-screen): allow layout creation without randomUUID
* test(credentials): avoid flaky Ed25519 key generation
* fix(sidebar): remeasure visible rows after density changes (#1489)
* fix(sidebar): remeasure visible rows after density changes
* test(credentials): avoid flaky Ed25519 key generation
* feat(split-screen): redesign split screen with free-form panes and pane picker
* feat(sidebar): add host click behavior setting, default to always opening a new tab
* feat(snippets): redesign panel to match the host list
* fix(session-recording): stop session logs panel from refetching in a loop
* feat(snippets): bring back sharing, target hosts and drag reorder
* feat(ssh-terminal): add toolbar position, start state, display and fade host settings
* feat(ssh-terminal): add host setting to turn off password prompt auto-fill
* fix(ai): stop streams getting cut off and keep tool steps across messages
* feat: improve quick connect system using plugin api
* fix: proxmox guest sync, startup snippet sync, drop dead cloudflare ssh transport
* feat: move terminal settings, look and ssh tools into ssh-terminal plugin
* feat: move snippets, keybinding actions and macros out of core into plugins
* feat: store rdp, vnc and telnet logins in a generic per-protocol table
* feat: type the plugin host record, move plugin strings out of core and add lint checks
* feat: replace core homepage widget registry with generic extension points
* feat: plugins only see their own host settings in the browser
* feat: harden plugin api for separate repos, signed updates and generic external login
* fix: keep plugin css below core utilities and version plugin assets by their css
* fix: load and save plugin host settings in the editor and tighten settings row spacing
* feat: redesign user host defaults system
* fix(remote-desktop): release held keys on focus loss (#1498)
* fix(session-sharing): replace local presentation after takeover (#1500)
* fix(file-manager): refresh disk usage with directory listing (#1503)
* fix(ssh-terminal): preserve input order during local echo reconciliation (#1505)
* feat(remote-desktop): expose display zoom controls for RDP (#1514)
* fix(file-manager): restore text selection and copying in Markdown previews (#1518)
* fix(sidebar): keep compact host addresses visible (#1524)
* fix(hosts): preserve plugin settings when reopening the editor (#1499)
* fix(ssh-terminal): close owner sockets when presentations unmount (#1501)
* fix(session-sharing): synchronize shared SSH terminal dimensions (#1502)
* fix(session-sharing): show participant presence in member terminals (#1504)
* fix(ai): sync provider configuration and keys across linked devices (#1506)
* feat(ssh-terminal): add a duplicate tab action (#1510)
* feat(file-manager): allow disabling move-to-trash confirmation (#1511)
* feat(file-manager): default to the remote login directory (#1512)
* feat(homepage): synchronize canvas layouts across devices (#1515)
* fix(shortcuts): prevent accidental double-Shift palette activation (#1519)
* feat(tmux-monitor): add a unified multi-host view (#1520)
* fix(electron): allow explicit quit with active connections (#1522)
* feat: reconnect all disconnected terminal tabs (#1523)
* fix(hosts): preserve recording and sharing settings in the dev editor (#1509)
* fix(database): preserve RDP domain during startup backfill (#1497)
* feat(sidebar): add keyboard type-ahead search for hosts (#1516)
* feat(automations): add per-host maintenance mode and recurring schedules (#1521)
* feat(tmux): add a per-host mouse toggle and preserve remote configuration (#1513)
* fix(session-sharing): use the linked server for desktop meetings and links (#1507)
* fix(tunnels): support TOTP for client tunnels and jump hosts (#1508)
* fix: type errors and lint failures from merged plugin prs
* feat(installer): make the Windows desktop shortcut optional (#1517)
* fix: ship the plugin sdk and serialport in desktop builds
* fix: prefer ipv4 when resolving ssh host names (#1365)
* fix: readable terminal selection with the light theme (#1319)
* fix: build desktop session share links on the linked server (#1350)
* fix: show meeting guests to members and hide the roster from guests (#1353)
* fix: reliable file manager saves with sudo prompt and no base64 guessing (#1335)
* fix: show disk usage for the mount being browsed (#1347)
* fix: load the linux tray icon reliably and log tray failures (#1332)
* fix: cover the gnome close path in the desktop quit test (#1332)
* fix: prompt for jump host TOTP in terminal, file manager and metrics (#1312)
* feat: optional auto reconnect for dropped ssh sessions (#1321)
* feat: tighter compact host rows with inline tags (#1325)
* feat: bring back the alert feed homepage widget
* feat: pick target terminals for snippets again
* feat: share button on connection tabs again
* feat: recent servers list when linking the desktop app
* feat: list the fixed app shortcuts in keyboard shortcuts
* fix: translate hardcoded toasts and drop em dashes from ui text
* fix: translate leftover hardcoded ui text
* fix: toolbar files button opens at the shell directory again
* fix: show the remote desktop note under connection origin again
* chore: openapi docs for host metrics manager routes
* chore: openapi docs for undocumented routes and fix two broken doc blocks
* fix: declare terminal.listSessions use for snippet targets
* revert: drop the alert feed widget, the notification center replaces it
* feat: restore the alert feed homepage widget
* fix: quick connect fills the host address from plugin auth editors like tailscale
* fix: docker container logs use the base background color
* chore: fix dependabot alerts and apply safe npm upgrades
* fix: proxmox stats tab reads its switch from plugin host settings
* fix: count plugin ssh logins toward host status and stop showing reachable as a warning
* fix: hide the built-in source pill in the host editor
* fix: host status shows online when the port answers and only warns on a failed login
* fix: drop the top border on the split layout menu
* fix: host defaults tabs sit where the host editor puts them and skip plugins with nothing to default
* fix: remote desktop auth method labels and notes for vnc and telnet
* fix: add description to the web endpoint enable switch
* fix: crowdin sync and pretranslate for plugin locales, run plugin tests on release
* feat: move telemetry out of core into a usage statistics plugin
* fix: recent activity card no longer cuts off host names early
* fix: remove duplicate top border on split screen menu
* fix: redesign session log player controls, fix tooltip overlap and drop dot separators
* fix: file manager and sftp text showing raw i18n keys
* fix: onboarding rerunning for existing users after upgrading
* fix: fleet detail buttons overflowing the sidebar
* fix: session log player scrolling off screen horizontally
* fix: network graph lag when adding hosts and on every render
* fix: maintenance index name, shared host read only view and badge polling
* fix: meeting tabs, workspace tab merging, rail panels as tabs and general ui lag
* chore: update release notes
* fix: remote desktop tabs reconnecting on every tab switch
* fix: build plugin sdk before linting in release workflow
* chore: lint, format, and bump version to 2.9.0
* chore: skip crowdin pre-translate in release for now
* chore: sync Crowdin translations for 2.9.0
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Koi <38512047+datlt4@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
4405 files changed
+1341026
-289722
No files matched your search
@@ -0,0 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0
|
||||
|
||||
First release, bundled with Termix 2.9.0.
|
||||
|
||||
Finds the VMs and containers on a Proxmox VE node through an SSH host and imports them as Termix hosts. Can keep them in sync on a schedule and shows node stats.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Proxmox
|
||||
|
||||
Find Proxmox VE guests through an SSH host, import them as hosts and keep them in sync.
|
||||
|
||||
## Features
|
||||
|
||||
- Find the VMs and containers on a Proxmox VE node through an SSH host you already have.
|
||||
- Import them as Termix hosts with the login type you choose.
|
||||
- Keep imported hosts in sync on a schedule.
|
||||
- Show node and guest stats in the Proxmox tab.
|
||||
|
||||
## Settings
|
||||
|
||||
### Host
|
||||
|
||||
- Proxmox: mark this host as a Proxmox node and set how guests are imported.
|
||||
- Default Auth Type: the login type given to imported guests.
|
||||
- Auto sync guests and Sync interval (minutes): check the node for changes on a schedule. The minimum is 5 minutes.
|
||||
- Proxmox stats: show the stats tab for this host.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run build # build into dist/
|
||||
npm run test # run this plugin's tests
|
||||
npm run typecheck # type-check this plugin
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox-statistieke"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox-statistieke",
|
||||
"cpuUsage": "SVE-gebruik",
|
||||
"memoryUsage": "Geheuegebruik",
|
||||
"diskUsage": "Skyfgebruik",
|
||||
"networkInterfaces": "Netwerkkoppelvlakke",
|
||||
"uptime": "Optyd",
|
||||
"systemInfo": "Stelselinligting",
|
||||
"guestsSummary": "Gaste",
|
||||
"storagePools": "Bergingspoele",
|
||||
"clusterHealth": "Klustergesondheid",
|
||||
"cpuCores_one": "{{count}} Kern",
|
||||
"cpuCores_other": "{{count}} Kerne",
|
||||
"used": "Gebruik",
|
||||
"total": "Totaal",
|
||||
"hostname": "Gasheernaam",
|
||||
"kernel": "Kern",
|
||||
"pveVersion": "PVE-weergawe",
|
||||
"name": "Naam",
|
||||
"status": "Status",
|
||||
"type": "Tipe",
|
||||
"cpu": "SVE",
|
||||
"mem": "Mem",
|
||||
"disk": "Skyf",
|
||||
"running": "Hardloop",
|
||||
"stopped": "Gestop",
|
||||
"guestCounts": "{{running}}/{{total}} hardloop",
|
||||
"inactive": "Onaktief",
|
||||
"notClustered": "Losstaande nodus (nie deel van 'n groep nie)",
|
||||
"quorate": "Kworate",
|
||||
"notQuorate": "Nie kworum nie",
|
||||
"nodeOnline": "Aanlyn",
|
||||
"nodeOffline": "Vanlyn",
|
||||
"noHostSelected": "Geen gasheer gekies nie",
|
||||
"pveshNotFound": "pvesh nie gevind nie - is dit 'n Proxmox-node?",
|
||||
"nodeDetectionFailed": "Kan nie 'n geldige Proxmox-nodusnaam bepaal nie",
|
||||
"connecting": "Verbind...",
|
||||
"connectionFailed": "Kon nie aan die Proxmox-nodus koppel nie",
|
||||
"refresh": "Verfris",
|
||||
"searchGuests": "Soek gaste...",
|
||||
"filterAll": "Alles",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Geen VM'e of LXC-houers is op hierdie node gevind nie.",
|
||||
"noGuestsMatch": "Geen gaste stem ooreen met die huidige filters nie."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Aktiveer Proxmox-statistieke",
|
||||
"enableProxmoxStatsDesc": "Wys 'n Proxmox Statistiek-oortjie vir hierdie gasheer met lewendige nodus-, gas-, bergings- en groepstatistieke wat oor SSH versamel is.",
|
||||
"proxmoxStatsPollInterval": "Stembusinterval (sekondes)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 sekondes. Hoe gereeld om Proxmox-nodusstatistieke te verfris.",
|
||||
"proxmoxStatsNodeOverride": "Nodenaam-oorskrywing",
|
||||
"proxmoxStatsNodeOverrideDesc": "Ignoreer outomatiese opsporing van die Proxmox-nodusnaam (standaard opgespoor via gasheernaam).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Outomaties opgespoor vanaf gasheernaam",
|
||||
"authTypeStepca": "Stap CA",
|
||||
"proxmoxIntegration": "Proxmox-integrasie",
|
||||
"enableProxmox": "Aktiveer Proxmox",
|
||||
"enableProxmoxDesc": "Merk hierdie gasheer as 'n Proxmox-nodus. Aktiveer gasontdekking en invoer direk vanaf hierdie gasheer.",
|
||||
"proxmoxDefaultAuthType": "Standaardmagtigingtipe",
|
||||
"proxmoxDefaultAuthTypeDesc": "Verifikasiemetode toegepas op ingevoerde gashere. Kies die verifikasietipe wat ooreenstem met hoe jy met jou gaste skakel.",
|
||||
"authTypePassword": "Wagwoord",
|
||||
"authTypeKey": "SSH-sleutel",
|
||||
"authTypeCredential": "Geloofsbrief",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Geen",
|
||||
"proxmoxDefaultCredential": "Standaard geloofsbriewe",
|
||||
"proxmoxDefaultCredentialDesc": "Geloofsbrief wat gebruik word vir ingevoerde gashere wanneer magtigingstipe op Geloofsbrief gestel is.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP-opsporing",
|
||||
"proxmoxWindowsDetectionDesc": "Komma-geskeide naampatrone wat RDP in plaas van SSH aktiveer (ongesensitief vir hoofletters)",
|
||||
"proxmoxDockerDetection": "Docker-opsporing",
|
||||
"proxmoxDockerDetectionDesc": "Komma-geskeide naampatrone wat Docker in staat stel om gaste te pas",
|
||||
"proxmoxPreferredRanges": "Voorkeur-IP-reekse",
|
||||
"proxmoxPreferredRangesDesc": "Komma-geskeide voorvoegsels in prioriteitsvolgorde vir IP-keuse wanneer 'n gas verskeie koppelvlakke het",
|
||||
"proxmoxAutoSync": "Outomatiese sinkronisering van gaste",
|
||||
"proxmoxAutoSyncDesc": "Ontdek hierdie Proxmox-nodus gereeld en skep of werk ingevoerde gashere op terwyl jou sessie ontsluit is.",
|
||||
"proxmoxSyncInterval": "Sinkronisasie-interval (minute)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minute. Die skeduleerder slaan geslote gebruikerssessies oor.",
|
||||
"proxmoxMarkMissing": "Merk ontbrekende gaste",
|
||||
"proxmoxMarkMissingDesc": "Voeg 'n proxmox-missing-etiket by wanneer 'n voorheen ingevoerde gas verdwyn in plaas daarvan om dit te verwyder.",
|
||||
"proxmoxLastSync": "Laaste sinkronisasie",
|
||||
"proxmoxLastSyncNever": "Nog nie gesinkroniseer nie",
|
||||
"proxmoxLastSyncNoResult": "Geen resultaatbesonderhede nie",
|
||||
"proxmoxLastSyncSummary": "{{created}} geskep, {{updated}} opgedateer, {{markedMissing}} ontbreek, {{skipped}} oorgeslaan",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Misluk",
|
||||
"pending": "Hangende",
|
||||
"success": "Sukses"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Ontdek en invoer Proxmox-gaste",
|
||||
"proxmoxImportTitle": "Invoer vanaf Proxmox",
|
||||
"proxmoxSelectHost": "Kies 'n Proxmox-gasheer…",
|
||||
"proxmoxDiscover": "Ontdek",
|
||||
"proxmoxDiscovering": "Ontdek…",
|
||||
"proxmoxDiscoverGuests": "Ontdek gaste",
|
||||
"proxmoxSelectAll": "Kies alles",
|
||||
"proxmoxDeselectAll": "Deselekteer alles",
|
||||
"proxmoxNoGuests": "Geen gaste gevind op hierdie Proxmox-nodus nie.",
|
||||
"proxmoxResultImported": "{{count}} ingevoer",
|
||||
"proxmoxResultUpdated": "{{count}} opgedateer",
|
||||
"proxmoxResultFailed": "{{count}} het misluk",
|
||||
"proxmoxImportComplete": "Proxmox-invoer voltooi: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Ontdekking het misluk",
|
||||
"proxmoxImportFailed": "Invoer het misluk",
|
||||
"docsLink": "Bekyk dokumente",
|
||||
"hostNotFound": "Gasheer nie gevind nie",
|
||||
"loadingHost": "Laai gasheer...",
|
||||
"none": "Geen",
|
||||
"proxmoxGuestsSelected_one": "{{count}} gas, {{selected}} gekies",
|
||||
"proxmoxGuestsSelected_other": "{{count}} gaste, {{selected}} gekies",
|
||||
"proxmoxImportButton_one": "Voer {{count}} gasheer in",
|
||||
"proxmoxImportButton_other": "Voer {{count}} gashere in"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Verbonde"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "إحصائيات Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "إحصائيات Proxmox",
|
||||
"cpuUsage": "استخدام CPU",
|
||||
"memoryUsage": "استخدام الذاكرة",
|
||||
"diskUsage": "استخدام القرص",
|
||||
"networkInterfaces": "واجهات الشبكة",
|
||||
"uptime": "مدة التشغيل",
|
||||
"systemInfo": "معلومات النظام",
|
||||
"guestsSummary": "الضيوف",
|
||||
"storagePools": "أحواض التخزين",
|
||||
"clusterHealth": "الصحة العنقودية",
|
||||
"cpuCores_one": "{{count}} نواة",
|
||||
"cpuCores_other": "{{count}} نوى",
|
||||
"used": "مستخدم",
|
||||
"total": "المجموع",
|
||||
"hostname": "اسم المضيف",
|
||||
"kernel": "النواة",
|
||||
"pveVersion": "إصدار لاعب ضد البيئة",
|
||||
"name": "الاسم",
|
||||
"status": "الحالة",
|
||||
"type": "النوع",
|
||||
"cpu": "المعالج",
|
||||
"mem": "ميم",
|
||||
"disk": "القرص",
|
||||
"running": "قيد التشغيل",
|
||||
"stopped": "متوقف",
|
||||
"guestCounts": "{{running}}/{{total}} قيد التشغيل",
|
||||
"inactive": "غير نشط",
|
||||
"notClustered": "عقدة مستقلة (ليست جزءًا من مجموعة)",
|
||||
"quorate": "اقتبس",
|
||||
"notQuorate": "غير مكتمل النصاب",
|
||||
"nodeOnline": "متصل",
|
||||
"nodeOffline": "غير متصل",
|
||||
"noHostSelected": "لم يتم تحديد أي جهاز مضيف",
|
||||
"pveshNotFound": "لم يتم العثور على pvesh - هل هذه عقدة Proxmox؟",
|
||||
"nodeDetectionFailed": "تعذر تحديد اسم عقدة Proxmox صالح",
|
||||
"connecting": "جارٍ الاتصال...",
|
||||
"connectionFailed": "فشل الاتصال بعقدة Proxmox",
|
||||
"refresh": "تحديث",
|
||||
"searchGuests": "ابحث عن الضيوف...",
|
||||
"filterAll": "الكل",
|
||||
"filterVm": "آلة افتراضية",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "لم يتم العثور على أي أجهزة افتراضية أو حاويات LXC على هذه العقدة.",
|
||||
"noGuestsMatch": "لا يوجد ضيوف يطابقون المعايير الحالية."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "تفعيل إحصائيات Proxmox",
|
||||
"enableProxmoxStatsDesc": "اعرض علامة تبويب إحصائيات Proxmox لهذا المضيف مع إحصائيات العقدة والضيف والتخزين والمجموعة التي تم جمعها عبر SSH.",
|
||||
"proxmoxStatsPollInterval": "فترة الاستطلاع (بالثواني)",
|
||||
"proxmoxStatsPollIntervalDesc": "الحد الأدنى 15 ثانية. عدد مرات تحديث إحصائيات عقدة Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "تجاوز اسم العقدة",
|
||||
"proxmoxStatsNodeOverrideDesc": "تجاوز الكشف التلقائي عن اسم عقدة Proxmox (يتم الكشف عنه عبر اسم المضيف افتراضيًا).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "تم اكتشافه تلقائيًا من اسم المضيف",
|
||||
"authTypeStepca": "الخطوة ج",
|
||||
"proxmoxIntegration": "تكامل Proxmox",
|
||||
"enableProxmox": "تمكين Proxmox",
|
||||
"enableProxmoxDesc": "تعليم هذا المضيف كعقدة Proxmox. يُمكّن اكتشاف الضيوف واستيرادهم مباشرة من هذا المضيف.",
|
||||
"proxmoxDefaultAuthType": "نوع المصادقة الافتراضي",
|
||||
"proxmoxDefaultAuthTypeDesc": "طريقة المصادقة المطبقة على المضيفين الضيوف المستوردين. اختر نوع المصادقة الذي يتوافق مع طريقة اتصالك بالضيوف.",
|
||||
"authTypePassword": "كلمة مرور",
|
||||
"authTypeKey": "مفتاح SSH",
|
||||
"authTypeCredential": "بيانات اعتماد",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "بلا",
|
||||
"proxmoxDefaultCredential": "بيانات الاعتماد الافتراضية",
|
||||
"proxmoxDefaultCredentialDesc": "بيانات الاعتماد المُستخدمة للأجهزة الضيفة المستوردة عند ضبط نوع المصادقة على 'بيانات الاعتماد'.",
|
||||
"proxmoxWindowsDetection": "اكتشاف Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "أنماط الأسماء مفصولة بفواصل تُشغّل RDP بدلاً من SSH (غير حسّاسة لحالة الأحرف)",
|
||||
"proxmoxDockerDetection": "اكتشاف Docker",
|
||||
"proxmoxDockerDetectionDesc": "أنماط الأسماء مفصولة بفواصل تُمكّن Docker للأجهزة الضيفة المطابقة",
|
||||
"proxmoxPreferredRanges": "نطاقات IP المُفضّلة",
|
||||
"proxmoxPreferredRangesDesc": "بادئات مفصولة بفواصل بترتيب الأولوية لاختيار IP عندما يكون للجهاز الضيف واجهات متعددة",
|
||||
"proxmoxAutoSync": "مزامنة تلقائية للأجهزة الضيفة",
|
||||
"proxmoxAutoSyncDesc": "اكتشاف عقدة Proxmox هذه بشكل دوري وإنشاء أو تحديث الأجهزة الضيفة المستوردة أثناء فتح جلستك.",
|
||||
"proxmoxSyncInterval": "فترة المزامنة (بالدقائق)",
|
||||
"proxmoxSyncIntervalDesc": "الحد الأدنى 5 دقائق. يتخطّى المجدول جلسات المستخدم المقفلة.",
|
||||
"proxmoxMarkMissing": "تعليم الأجهزة الضيفة المفقودة",
|
||||
"proxmoxMarkMissingDesc": "أضف وسم proxmox-missing عندما يختفي جهاز ضيف تم استيراده مسبقاً بدلاً من حذفه.",
|
||||
"proxmoxLastSync": "آخر مزامنة",
|
||||
"proxmoxLastSyncNever": "لم تتم المزامنة بعد",
|
||||
"proxmoxLastSyncNoResult": "لا توجد تفاصيل للنتائج",
|
||||
"proxmoxLastSyncSummary": "{{created}} مُنشأ، {{updated}} مُحدَّث، {{markedMissing}} مفقود، {{skipped}} مُتخطّى",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "فشل",
|
||||
"pending": "قيد الانتظار",
|
||||
"success": "نجاح"
|
||||
},
|
||||
"proxmoxDiscoverAction": "اكتشاف واستيراد أجهزة Proxmox الضيفة",
|
||||
"proxmoxImportTitle": "استيراد من Proxmox",
|
||||
"proxmoxSelectHost": "اختر مضيف Proxmox…",
|
||||
"proxmoxDiscover": "اكتشف",
|
||||
"proxmoxDiscovering": "جارٍ الاكتشاف…",
|
||||
"proxmoxDiscoverGuests": "اكتشف الأجهزة الضيفة",
|
||||
"proxmoxSelectAll": "تحديد الكل",
|
||||
"proxmoxDeselectAll": "إلغاء تحديد الكل",
|
||||
"proxmoxNoGuests": "لم يتم العثور على أجهزة ضيفة على عقدة Proxmox هذه.",
|
||||
"proxmoxResultImported": "{{count}} تم استيرادها",
|
||||
"proxmoxResultUpdated": "{{count}} تم تحديثها",
|
||||
"proxmoxResultFailed": "{{count}} فشلت",
|
||||
"proxmoxImportComplete": "اكتمل استيراد Proxmox: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "فشل الاكتشاف",
|
||||
"proxmoxImportFailed": "فشل الاستيراد",
|
||||
"docsLink": "عرض المستندات",
|
||||
"hostNotFound": "المضيف غير موجود",
|
||||
"loadingHost": "جارٍ تحميل المضيف...",
|
||||
"none": "لا يوجد",
|
||||
"proxmoxGuestsSelected_one": "{{count}} ضيف، {{selected}} مُختار",
|
||||
"proxmoxGuestsSelected_other": "{{count}} ضيوف، {{selected}} مختارون",
|
||||
"proxmoxImportButton_one": "استيراد {{count}} مضيف",
|
||||
"proxmoxImportButton_other": "استيراد {{count}} مضيفين"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "متصل"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Статистика на Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Статистика на Proxmox",
|
||||
"cpuUsage": "Използване на процесора",
|
||||
"memoryUsage": "Използване на паметта",
|
||||
"diskUsage": "Използване на диска",
|
||||
"networkInterfaces": "Мрежови интерфейси",
|
||||
"uptime": "Време на работа",
|
||||
"systemInfo": "Системна информация",
|
||||
"guestsSummary": "Гости",
|
||||
"storagePools": "Пулове за съхранение",
|
||||
"clusterHealth": "Здраве на клъстера",
|
||||
"cpuCores_one": "{{count}} Ядро",
|
||||
"cpuCores_other": "{{count}} Ядра",
|
||||
"used": "Използван",
|
||||
"total": "Общо",
|
||||
"hostname": "Име на хост",
|
||||
"kernel": "Ядро",
|
||||
"pveVersion": "PVE версия",
|
||||
"name": "Име",
|
||||
"status": "Статус",
|
||||
"type": "Тип",
|
||||
"cpu": "Процесор",
|
||||
"mem": "Мем",
|
||||
"disk": "Диск",
|
||||
"running": "Бягане",
|
||||
"stopped": "Спряно",
|
||||
"guestCounts": "{{running}}/{{total}} бягане",
|
||||
"inactive": "Неактивен",
|
||||
"notClustered": "Самостоятелен възел (не е част от клъстер)",
|
||||
"quorate": "Кворат",
|
||||
"notQuorate": "Няма кворум",
|
||||
"nodeOnline": "Онлайн",
|
||||
"nodeOffline": "Офлайн",
|
||||
"noHostSelected": "Няма избран хост",
|
||||
"pveshNotFound": "pvesh не е намерен - това Proxmox възел ли е?",
|
||||
"nodeDetectionFailed": "Не може да се определи валидно име на възел Proxmox",
|
||||
"connecting": "Свързване...",
|
||||
"connectionFailed": "Неуспешно свързване с възел Proxmox",
|
||||
"refresh": "Обновяване",
|
||||
"searchGuests": "Търсене на гости...",
|
||||
"filterAll": "Всички",
|
||||
"filterVm": "ВМ",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "На този възел не са намерени виртуални машини или LXC контейнери.",
|
||||
"noGuestsMatch": "Няма гости, които да отговарят на текущите филтри."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Проксмокс",
|
||||
"enableProxmoxStats": "Активиране на Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Покажете раздела „Статистика на Proxmox“ за този хост с актуални статистически данни за възлите, госта, хранилището и клъстера, събрани през SSH.",
|
||||
"proxmoxStatsPollInterval": "Интервал на анкетата (секунди)",
|
||||
"proxmoxStatsPollIntervalDesc": "Минимум 15 секунди. Колко често да се обновяват статистическите данни за възлите на Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Замяна на името на възела",
|
||||
"proxmoxStatsNodeOverrideDesc": "Игнориране на автоматичното откриване на името на възела Proxmox (по подразбиране се открива чрез името на хоста).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Автоматично разпознато от името на хоста",
|
||||
"authTypeStepca": "Стъпка CA",
|
||||
"proxmoxIntegration": "Интеграция с Proxmox",
|
||||
"enableProxmox": "Активиране на Proxmox",
|
||||
"enableProxmoxDesc": "Маркирайте този хост като Proxmox възел. Активирайте откриването на гост и импортирането директно от този хост.",
|
||||
"proxmoxDefaultAuthType": "Тип удостоверяване по подразбиране",
|
||||
"proxmoxDefaultAuthTypeDesc": "Методът за удостоверяване е приложен към импортираните гост-хостове. Изберете типа удостоверяване, който съответства на начина, по който се свързвате с гостите си.",
|
||||
"authTypePassword": "Парола",
|
||||
"authTypeKey": "SSH ключ",
|
||||
"authTypeCredential": "Пълномощия",
|
||||
"authTypeOpkssh": "ОПКСШ",
|
||||
"authTypeNone": "Няма",
|
||||
"proxmoxDefaultCredential": "Подразбиращи се идентификационни данни",
|
||||
"proxmoxDefaultCredentialDesc": "Идентификационни данни, използвани за импортирани гост хостове, когато типът удостоверяване е зададен на Идентификационни данни.",
|
||||
"proxmoxWindowsDetection": "Откриване на Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Модели на имена, разделени със запетаи, които задействат RDP вместо SSH (без значение от главни и малки букви)",
|
||||
"proxmoxDockerDetection": "Откриване на Докер",
|
||||
"proxmoxDockerDetectionDesc": "Модели за имена, разделени със запетаи, които позволяват на Docker да съпоставя гости",
|
||||
"proxmoxPreferredRanges": "Предпочитани IP диапазони",
|
||||
"proxmoxPreferredRangesDesc": "Префикси, разделени със запетаи, подредени по приоритет за избор на IP адрес, когато гост има множество интерфейси",
|
||||
"proxmoxAutoSync": "Автоматично синхронизиране на гости",
|
||||
"proxmoxAutoSyncDesc": "Периодично откривайте този Proxmox възел и създавайте или актуализирайте импортирани гост хостове, докато сесията ви е отключена.",
|
||||
"proxmoxSyncInterval": "Интервал на синхронизиране (минути)",
|
||||
"proxmoxSyncIntervalDesc": "Минимум 5 минути. Планировчикът пропуска заключените потребителски сесии.",
|
||||
"proxmoxMarkMissing": "Маркиране на липсващи гости",
|
||||
"proxmoxMarkMissingDesc": "Добавете етикет proxmox-missing, когато предварително импортиран гост изчезне, вместо да го изтриете.",
|
||||
"proxmoxLastSync": "Последна синхронизация",
|
||||
"proxmoxLastSyncNever": "Все още не е синхронизирано",
|
||||
"proxmoxLastSyncNoResult": "Няма подробности за резултата",
|
||||
"proxmoxLastSyncSummary": "{{created}} създадено, {{updated}} актуализирано, {{markedMissing}} липсващо, {{skipped}} пропуснато",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Неуспешно",
|
||||
"pending": "В очакване",
|
||||
"success": "Успех"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Откриване и импортиране на гости на Proxmox",
|
||||
"proxmoxImportTitle": "Импортиране от Proxmox",
|
||||
"proxmoxSelectHost": "Изберете хост на Proxmox…",
|
||||
"proxmoxDiscover": "Открийте",
|
||||
"proxmoxDiscovering": "Откриване…",
|
||||
"proxmoxDiscoverGuests": "Открийте гостите",
|
||||
"proxmoxSelectAll": "Избери всички",
|
||||
"proxmoxDeselectAll": "Премахване на избора от всички",
|
||||
"proxmoxNoGuests": "Няма намерени гости на този Proxmox възел.",
|
||||
"proxmoxResultImported": "{{count}} импортирано",
|
||||
"proxmoxResultUpdated": "{{count}} актуализирано",
|
||||
"proxmoxResultFailed": "{{count}} неуспешно",
|
||||
"proxmoxImportComplete": "Импортирането на Proxmox е завършено: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Откриването не беше успешно",
|
||||
"proxmoxImportFailed": "Импортирането не бе успешно",
|
||||
"docsLink": "Преглед на документи",
|
||||
"hostNotFound": "Хостът не е намерен",
|
||||
"loadingHost": "Зареждане на хоста...",
|
||||
"none": "Няма",
|
||||
"proxmoxGuestsSelected_one": "{{count}} гост, {{selected}} избран",
|
||||
"proxmoxGuestsSelected_other": "{{count}} гости, {{selected}} избрани",
|
||||
"proxmoxImportButton_one": "Импортиране на хост {{count}}",
|
||||
"proxmoxImportButton_other": "Импортиране на {{count}} хостове"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Свързан"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Estadístiques de Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Estadístiques de Proxmox",
|
||||
"cpuUsage": "Ús de la CPU",
|
||||
"memoryUsage": "Ús de memòria",
|
||||
"diskUsage": "Ús del disc",
|
||||
"networkInterfaces": "Interfícies de xarxa",
|
||||
"uptime": "Temps de funcionament",
|
||||
"systemInfo": "Informació del sistema",
|
||||
"guestsSummary": "Convidats",
|
||||
"storagePools": "Piscines d'emmagatzematge",
|
||||
"clusterHealth": "Salut del clúster",
|
||||
"cpuCores_one": "{{count}} Nucli",
|
||||
"cpuCores_other": "{{count}} Nuclis",
|
||||
"used": "Usat",
|
||||
"total": "Total",
|
||||
"hostname": "Nom d'amfitrió",
|
||||
"kernel": "Nucli",
|
||||
"pveVersion": "Versió PVE",
|
||||
"name": "Nom",
|
||||
"status": "Estat",
|
||||
"type": "Tipus",
|
||||
"cpu": "CPU",
|
||||
"mem": "Memòria",
|
||||
"disk": "Disc",
|
||||
"running": "Córrer",
|
||||
"stopped": "Aturat",
|
||||
"guestCounts": "{{running}}/{{total}} en execució",
|
||||
"inactive": "Inactiu",
|
||||
"notClustered": "Node autònom (no forma part d'un clúster)",
|
||||
"quorate": "Quota",
|
||||
"notQuorate": "Sense quòrum",
|
||||
"nodeOnline": "En línia",
|
||||
"nodeOffline": "Fora de línia",
|
||||
"noHostSelected": "No s'ha seleccionat cap amfitrió",
|
||||
"pveshNotFound": "No s'ha trobat pvesh: és un node de Proxmox?",
|
||||
"nodeDetectionFailed": "No s'ha pogut determinar un nom de node Proxmox vàlid",
|
||||
"connecting": "Connectant...",
|
||||
"connectionFailed": "No s'ha pogut connectar al node de Proxmox.",
|
||||
"refresh": "Actualitza",
|
||||
"searchGuests": "Cerca convidats...",
|
||||
"filterAll": "Tot",
|
||||
"filterVm": "màquina virtual",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No s'han trobat màquines virtuals ni contenidors LXC en aquest node.",
|
||||
"noGuestsMatch": "No hi ha convidats que coincideixin amb els filtres actuals."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Activa les estadístiques de Proxmox",
|
||||
"enableProxmoxStatsDesc": "Mostra una pestanya Estadístiques de Proxmox per a aquest amfitrió amb estadístiques en directe de node, convidat, emmagatzematge i clúster recollides a través de SSH.",
|
||||
"proxmoxStatsPollInterval": "Interval de sondeig (segons)",
|
||||
"proxmoxStatsPollIntervalDesc": "Mínim 15 segons. Amb quina freqüència s'han d'actualitzar les estadístiques del node Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Substitució del nom del node",
|
||||
"proxmoxStatsNodeOverrideDesc": "Anul·la la detecció automàtica del nom del node Proxmox (detectat per defecte mitjançant el nom de l'amfitrió).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Detectat automàticament des del nom d'amfitrió",
|
||||
"authTypeStepca": "Pas CA",
|
||||
"proxmoxIntegration": "Integració de Proxmox",
|
||||
"enableProxmox": "Activa Proxmox",
|
||||
"enableProxmoxDesc": "Marca aquest amfitrió com a node de Proxmox. Activa la detecció de convidats i la importació directament des d'aquest amfitrió.",
|
||||
"proxmoxDefaultAuthType": "Tipus d'autorització per defecte",
|
||||
"proxmoxDefaultAuthTypeDesc": "Mètode d'autenticació aplicat als amfitrions convidats importats. Trieu el tipus d'autenticació que coincideixi amb la manera com us connecteu als vostres convidats.",
|
||||
"authTypePassword": "Contrasenya",
|
||||
"authTypeKey": "Clau SSH",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Cap",
|
||||
"proxmoxDefaultCredential": "Credencial predeterminada",
|
||||
"proxmoxDefaultCredentialDesc": "Credencial utilitzada per als amfitrions convidats importats quan el tipus d'autenticació està definit com a Credencial.",
|
||||
"proxmoxWindowsDetection": "Detecció de Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Patrons de noms separats per comes que activen RDP en lloc de SSH (sense distinció entre majúscules i minúscules)",
|
||||
"proxmoxDockerDetection": "Detecció de Docker",
|
||||
"proxmoxDockerDetectionDesc": "Patrons de noms separats per comes que permeten a Docker utilitzar convidats coincidents",
|
||||
"proxmoxPreferredRanges": "Intervals d'IP preferits",
|
||||
"proxmoxPreferredRangesDesc": "Prefixos separats per comes en ordre de prioritat per a la selecció d'IP quan un convidat té diverses interfícies",
|
||||
"proxmoxAutoSync": "Sincronitza automàticament els convidats",
|
||||
"proxmoxAutoSyncDesc": "Descobreix periòdicament aquest node de Proxmox i crea o actualitza els hosts convidats importats mentre la sessió està desbloquejada.",
|
||||
"proxmoxSyncInterval": "Interval de sincronització (minuts)",
|
||||
"proxmoxSyncIntervalDesc": "Mínim 5 minuts. El planificador omet les sessions d'usuari bloquejades.",
|
||||
"proxmoxMarkMissing": "Marca els convidats que falten",
|
||||
"proxmoxMarkMissingDesc": "Afegeix una etiqueta proxmox-missing quan un convidat importat prèviament desapareix en comptes d'eliminar-lo.",
|
||||
"proxmoxLastSync": "Darrera sincronització",
|
||||
"proxmoxLastSyncNever": "Encara no sincronitzat",
|
||||
"proxmoxLastSyncNoResult": "Sense detalls del resultat",
|
||||
"proxmoxLastSyncSummary": "{{created}} creat, {{updated}} actualitzat, {{markedMissing}} no hi és, {{skipped}} omès",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Fallat",
|
||||
"pending": "Pendent",
|
||||
"success": "Èxit"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Descobreix i importa convidats de Proxmox",
|
||||
"proxmoxImportTitle": "Importar des de Proxmox",
|
||||
"proxmoxSelectHost": "Seleccioneu un amfitrió Proxmox…",
|
||||
"proxmoxDiscover": "Descobreix",
|
||||
"proxmoxDiscovering": "Descobrint…",
|
||||
"proxmoxDiscoverGuests": "Descobreix convidats",
|
||||
"proxmoxSelectAll": "Selecciona-ho tot",
|
||||
"proxmoxDeselectAll": "Desselecciona-ho tot",
|
||||
"proxmoxNoGuests": "No s'han trobat convidats en aquest node de Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} importat",
|
||||
"proxmoxResultUpdated": "{{count}} actualitzat",
|
||||
"proxmoxResultFailed": "{{count}} ha fallat",
|
||||
"proxmoxImportComplete": "Importació de Proxmox completada: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "El descobriment ha fallat",
|
||||
"proxmoxImportFailed": "La importació ha fallat",
|
||||
"docsLink": "Veure documents",
|
||||
"hostNotFound": "No s'ha trobat l'amfitrió",
|
||||
"loadingHost": "S'està carregant l'amfitrió...",
|
||||
"none": "Cap",
|
||||
"proxmoxGuestsSelected_one": "{{count}} convidat, {{selected}} seleccionat",
|
||||
"proxmoxGuestsSelected_other": "{{count}} convidats, {{selected}} seleccionats",
|
||||
"proxmoxImportButton_one": "Importa l'amfitrió {{count}}",
|
||||
"proxmoxImportButton_other": "Importa els amfitrions {{count}}"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connectat"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Statistiky Proxmoxu"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Statistiky Proxmoxu",
|
||||
"cpuUsage": "Využití CPU",
|
||||
"memoryUsage": "Využití paměti",
|
||||
"diskUsage": "Využití disku",
|
||||
"networkInterfaces": "Síťová rozhraní",
|
||||
"uptime": "Doba provozuschopnosti",
|
||||
"systemInfo": "Informace o systému",
|
||||
"guestsSummary": "Hosté",
|
||||
"storagePools": "Úložné fondy",
|
||||
"clusterHealth": "Zdraví klastru",
|
||||
"cpuCores_one": "{{count}} Jádro",
|
||||
"cpuCores_other": "{{count}} Jádra",
|
||||
"used": "Použitý",
|
||||
"total": "Celkový",
|
||||
"hostname": "Název hostitele",
|
||||
"kernel": "Jádro",
|
||||
"pveVersion": "PVE verze",
|
||||
"name": "Jméno",
|
||||
"status": "Postavení",
|
||||
"type": "Typ",
|
||||
"cpu": "Procesor",
|
||||
"mem": "Paměť",
|
||||
"disk": "Disk",
|
||||
"running": "Běh",
|
||||
"stopped": "Zastaveno",
|
||||
"guestCounts": "{{running}}/{{total}} běží",
|
||||
"inactive": "Neaktivní",
|
||||
"notClustered": "Samostatný uzel (není součástí clusteru)",
|
||||
"quorate": "Kvorát",
|
||||
"notQuorate": "Není usnášeníschopný",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Nebyl vybrán žádný hostitel",
|
||||
"pveshNotFound": "pvesh nenalezen - je to uzel Proxmox?",
|
||||
"nodeDetectionFailed": "Nelze určit platný název uzlu Proxmox.",
|
||||
"connecting": "Spojovací...",
|
||||
"connectionFailed": "Nepodařilo se připojit k uzlu Proxmox",
|
||||
"refresh": "Obnovit",
|
||||
"searchGuests": "Hledat hosty...",
|
||||
"filterAll": "Vše",
|
||||
"filterVm": "virtuální počítač",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Na tomto uzlu nebyly nalezeny žádné virtuální počítače ani kontejnery LXC.",
|
||||
"noGuestsMatch": "Aktuálním filtrům neodpovídají žádní hosté."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Povolit statistiky Proxmoxu",
|
||||
"enableProxmoxStatsDesc": "Zobrazit kartu Statistiky Proxmoxu pro tohoto hostitele s aktivními statistikami uzlů, hostů, úložišť a clusterů shromážděnými přes SSH.",
|
||||
"proxmoxStatsPollInterval": "Interval dotazování (sekundy)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimálně 15 sekund. Jak často aktualizovat statistiky uzlu Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Přepsání názvu uzlu",
|
||||
"proxmoxStatsNodeOverrideDesc": "Přepsat automatickou detekci názvu uzlu Proxmox (ve výchozím nastavení detekováno pomocí názvu hostitele).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automaticky detekováno z názvu hostitele",
|
||||
"authTypeStepca": "Krok CA",
|
||||
"proxmoxIntegration": "Integrace Proxmoxu",
|
||||
"enableProxmox": "Povolit Proxmox",
|
||||
"enableProxmoxDesc": "Označit tento hostitel jako uzel Proxmox. Umožňuje vyhledávání hostů a import přímo z tohoto hostitele.",
|
||||
"proxmoxDefaultAuthType": "Výchozí typ ověření",
|
||||
"proxmoxDefaultAuthTypeDesc": "Metoda ověřování použitá na importované hostující hostitele. Vyberte typ ověřování, který odpovídá způsobu, jakým se připojujete k hostům.",
|
||||
"authTypePassword": "Heslo",
|
||||
"authTypeKey": "Klíč SSH",
|
||||
"authTypeCredential": "Přihlašovací údaj",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Žádný",
|
||||
"proxmoxDefaultCredential": "Výchozí přihlašovací údaje",
|
||||
"proxmoxDefaultCredentialDesc": "Přihlašovací údaje používané pro importované hostující hostitele, pokud je typ ověření nastaven na Přihlašovací údaje.",
|
||||
"proxmoxWindowsDetection": "Detekce Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Vzory názvů oddělené čárkami, které spouštějí RDP místo SSH (bez rozlišení velkých a malých písmen)",
|
||||
"proxmoxDockerDetection": "Detekce Dockeru",
|
||||
"proxmoxDockerDetectionDesc": "Vzory názvů oddělených čárkami, které Dockeru umožňují vyhledávání hostů",
|
||||
"proxmoxPreferredRanges": "Preferované rozsahy IP adres",
|
||||
"proxmoxPreferredRangesDesc": "Prefixy oddělené čárkami v pořadí priority pro výběr IP adresy, když má host více rozhraní",
|
||||
"proxmoxAutoSync": "Automatická synchronizace hostů",
|
||||
"proxmoxAutoSyncDesc": "Pravidelně vyhledávejte tento uzel Proxmox a vytvářejte nebo aktualizujte importované hostující hostitele, když je vaše relace odemčená.",
|
||||
"proxmoxSyncInterval": "Interval synchronizace (minuty)",
|
||||
"proxmoxSyncIntervalDesc": "Minimálně 5 minut. Plánovač přeskakuje uzamčené uživatelské relace.",
|
||||
"proxmoxMarkMissing": "Označit chybějící hosty",
|
||||
"proxmoxMarkMissingDesc": "Přidat tag proxmox-missing, když dříve importovaný host zmizí, místo aby byl smazán.",
|
||||
"proxmoxLastSync": "Poslední synchronizace",
|
||||
"proxmoxLastSyncNever": "Zatím nesynchronizováno",
|
||||
"proxmoxLastSyncNoResult": "Žádné podrobnosti o výsledcích",
|
||||
"proxmoxLastSyncSummary": "{{created}} vytvořeno, {{updated}} aktualizováno, {{markedMissing}} chybí, {{skipped}} přeskočeno",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Selhalo",
|
||||
"pending": "Čeká na vyřízení",
|
||||
"success": "Úspěch"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Objevte a importujte hosty Proxmoxu",
|
||||
"proxmoxImportTitle": "Import z Proxmoxu",
|
||||
"proxmoxSelectHost": "Vyberte hostitele Proxmox…",
|
||||
"proxmoxDiscover": "Objevit",
|
||||
"proxmoxDiscovering": "Objevování…",
|
||||
"proxmoxDiscoverGuests": "Objevte hosty",
|
||||
"proxmoxSelectAll": "Vybrat vše",
|
||||
"proxmoxDeselectAll": "Zrušit výběr všeho",
|
||||
"proxmoxNoGuests": "Na tomto uzlu Proxmox nebyli nalezeni žádní hosté.",
|
||||
"proxmoxResultImported": "{{count}} importováno",
|
||||
"proxmoxResultUpdated": "{{count}} aktualizováno",
|
||||
"proxmoxResultFailed": "{{count}} selhalo",
|
||||
"proxmoxImportComplete": "Import Proxmoxu dokončen: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Objev selhal",
|
||||
"proxmoxImportFailed": "Import se nezdařil",
|
||||
"docsLink": "Zobrazit dokumenty",
|
||||
"hostNotFound": "Hostitel nenalezen",
|
||||
"loadingHost": "Načítání hostitele...",
|
||||
"none": "Žádný",
|
||||
"proxmoxGuestsSelected_one": "{{count}} host, {{selected}} vybráno",
|
||||
"proxmoxGuestsSelected_other": "{{count}} hostů, {{selected}} vybráno",
|
||||
"proxmoxImportButton_one": "Importovat hostitele {{count}}",
|
||||
"proxmoxImportButton_other": "Importovat hostitele {{count}}"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Připojeno"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox-statistik"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox-statistik",
|
||||
"cpuUsage": "CPU-forbrug",
|
||||
"memoryUsage": "Hukommelsesforbrug",
|
||||
"diskUsage": "Diskforbrug",
|
||||
"networkInterfaces": "Netværksgrænseflader",
|
||||
"uptime": "Oppetid",
|
||||
"systemInfo": "Systemoplysninger",
|
||||
"guestsSummary": "Gæster",
|
||||
"storagePools": "Opbevaringsbassiner",
|
||||
"clusterHealth": "Klyngesundhed",
|
||||
"cpuCores_one": "{{count}} Kerne",
|
||||
"cpuCores_other": "{{count}} Kerner",
|
||||
"used": "Brugt",
|
||||
"total": "Total",
|
||||
"hostname": "Værtsnavn",
|
||||
"kernel": "Kernen",
|
||||
"pveVersion": "PVE-version",
|
||||
"name": "Navn",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Løb",
|
||||
"stopped": "Stoppet",
|
||||
"guestCounts": "{{running}}/{{total}} løber",
|
||||
"inactive": "Inaktiv",
|
||||
"notClustered": "Selvstændig node (ikke en del af en klynge)",
|
||||
"quorate": "Kvorat",
|
||||
"notQuorate": "Ikke quorable",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Ingen vært valgt",
|
||||
"pveshNotFound": "pvesh ikke fundet - er dette en Proxmox-node?",
|
||||
"nodeDetectionFailed": "Kan ikke bestemme et gyldigt Proxmox-nodenavn",
|
||||
"connecting": "Forbinder...",
|
||||
"connectionFailed": "Kunne ikke oprette forbindelse til Proxmox-noden",
|
||||
"refresh": "Opfriske",
|
||||
"searchGuests": "Søg gæster...",
|
||||
"filterAll": "Alle",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Der blev ikke fundet nogen VM'er eller LXC-containere på denne node.",
|
||||
"noGuestsMatch": "Ingen gæster matcher de nuværende filtre."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Aktivér Proxmox-statistik",
|
||||
"enableProxmoxStatsDesc": "Vis en Proxmox Stats-faneblad for denne vært med live node-, gæst-, lager- og klyngestatistikker indsamlet via SSH.",
|
||||
"proxmoxStatsPollInterval": "Afstemningsinterval (sekunder)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 sekunder. Hvor ofte skal Proxmox-nodestatistikker opdateres.",
|
||||
"proxmoxStatsNodeOverride": "Tilsidesættelse af nodenavn",
|
||||
"proxmoxStatsNodeOverrideDesc": "Tilsidesæt automatisk detektion af Proxmox-nodenavnet (registreres som standard via værtsnavn).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automatisk registreret fra værtsnavn",
|
||||
"authTypeStepca": "Trin CA",
|
||||
"proxmoxIntegration": "Proxmox-integration",
|
||||
"enableProxmox": "Aktivér Proxmox",
|
||||
"enableProxmoxDesc": "Marker denne vært som en Proxmox-node. Aktiverer gæsteopdagelse og import direkte fra denne vært.",
|
||||
"proxmoxDefaultAuthType": "Standardgodkendelsestype",
|
||||
"proxmoxDefaultAuthTypeDesc": "Godkendelsesmetode anvendt på importerede gæsteværter. Vælg den godkendelsestype, der matcher den måde, du opretter forbindelse til dine gæster.",
|
||||
"authTypePassword": "Adgangskode",
|
||||
"authTypeKey": "SSH-nøgle",
|
||||
"authTypeCredential": "Legitimationsoplysninger",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Ingen",
|
||||
"proxmoxDefaultCredential": "Standardlegitimationsoplysninger",
|
||||
"proxmoxDefaultCredentialDesc": "Legitimationsoplysninger, der bruges til importerede gæsteværter, når godkendelsestypen er indstillet til Legitimationsoplysninger.",
|
||||
"proxmoxWindowsDetection": "Windows/RDP-detektion",
|
||||
"proxmoxWindowsDetectionDesc": "Kommaseparerede navnemønstre, der udløser RDP i stedet for SSH (ikke-sanskel på store og små bogstaver)",
|
||||
"proxmoxDockerDetection": "Docker-detektion",
|
||||
"proxmoxDockerDetectionDesc": "Kommaseparerede navnemønstre, der gør det muligt for Docker at matche gæster",
|
||||
"proxmoxPreferredRanges": "Foretrukne IP-intervaller",
|
||||
"proxmoxPreferredRangesDesc": "Kommaseparerede præfikser i prioriteret rækkefølge for IP-valg, når en gæst har flere grænseflader",
|
||||
"proxmoxAutoSync": "Automatisk synkronisering af gæster",
|
||||
"proxmoxAutoSyncDesc": "Opdag periodisk denne Proxmox-node og opret eller opdater importerede gæsteværter, mens din session er låst op.",
|
||||
"proxmoxSyncInterval": "Synkroniseringsinterval (minutter)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutter. Planlæggeren springer låste brugersessioner over.",
|
||||
"proxmoxMarkMissing": "Markér manglende gæster",
|
||||
"proxmoxMarkMissingDesc": "Tilføj et proxmox-missing-tag, når en tidligere importeret gæst forsvinder, i stedet for at slette den.",
|
||||
"proxmoxLastSync": "Sidste synkronisering",
|
||||
"proxmoxLastSyncNever": "Ikke synkroniseret endnu",
|
||||
"proxmoxLastSyncNoResult": "Ingen resultatdetaljer",
|
||||
"proxmoxLastSyncSummary": "{{created}} oprettet, {{updated}} opdateret, {{markedMissing}} mangler, {{skipped}} sprunget over",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Mislykkedes",
|
||||
"pending": "Indtil",
|
||||
"success": "Succes"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Opdag og importer Proxmox-gæster",
|
||||
"proxmoxImportTitle": "Importer fra Proxmox",
|
||||
"proxmoxSelectHost": "Vælg en Proxmox-vært…",
|
||||
"proxmoxDiscover": "Opdage",
|
||||
"proxmoxDiscovering": "Opdager…",
|
||||
"proxmoxDiscoverGuests": "Opdag gæster",
|
||||
"proxmoxSelectAll": "Vælg alle",
|
||||
"proxmoxDeselectAll": "Fravælg alle",
|
||||
"proxmoxNoGuests": "Ingen gæster fundet på denne Proxmox-node.",
|
||||
"proxmoxResultImported": "{{count}} importeret",
|
||||
"proxmoxResultUpdated": "{{count}} opdateret",
|
||||
"proxmoxResultFailed": "{{count}} mislykkedes",
|
||||
"proxmoxImportComplete": "Proxmox-import fuldført: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Opdagelsen mislykkedes",
|
||||
"proxmoxImportFailed": "Importen mislykkedes",
|
||||
"docsLink": "Se dokumenter",
|
||||
"hostNotFound": "Værten blev ikke fundet",
|
||||
"loadingHost": "Indlæser vært...",
|
||||
"none": "Ingen",
|
||||
"proxmoxGuestsSelected_one": "{{count}} gæst, {{selected}} valgt",
|
||||
"proxmoxGuestsSelected_other": "{{count}} gæster, {{selected}} valgt",
|
||||
"proxmoxImportButton_one": "Importér {{count}} vært",
|
||||
"proxmoxImportButton_other": "Importér {{count}} værter"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Forbundet"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox-Statistiken"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox-Statistiken",
|
||||
"cpuUsage": "CPU-Auslastung",
|
||||
"memoryUsage": "Speicherauslastung",
|
||||
"diskUsage": "Festplattenauslastung",
|
||||
"networkInterfaces": "Netzwerkschnittstellen",
|
||||
"uptime": "Betriebszeit",
|
||||
"systemInfo": "Systeminformationen",
|
||||
"guestsSummary": "Gäste",
|
||||
"storagePools": "Speicherbecken",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Kern",
|
||||
"cpuCores_other": "{{count}} Kerne",
|
||||
"used": "Belegt",
|
||||
"total": "Gesamt",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE-Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Typ",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mitglied",
|
||||
"disk": "Festplatte",
|
||||
"running": "Läuft",
|
||||
"stopped": "Gestoppt",
|
||||
"guestCounts": "{{running}}/{{total}} läuft",
|
||||
"inactive": "Inaktiv",
|
||||
"notClustered": "Eigenständiger Knoten (nicht Teil eines Clusters)",
|
||||
"quorate": "Quorum",
|
||||
"notQuorate": "Nicht beschlussfähig",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Kein Host ausgewählt",
|
||||
"pveshNotFound": "pvesh nicht gefunden - handelt es sich um einen Proxmox-Knoten?",
|
||||
"nodeDetectionFailed": "Es konnte kein gültiger Proxmox-Knotenname ermittelt werden.",
|
||||
"connecting": "Verbindung wird hergestellt...",
|
||||
"connectionFailed": "Verbindung zum Proxmox-Knoten fehlgeschlagen.",
|
||||
"refresh": "Aktualisieren",
|
||||
"searchGuests": "Gäste suchen...",
|
||||
"filterAll": "Alle",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Auf diesem Knoten wurden keine VMs oder LXC-Container gefunden.",
|
||||
"noGuestsMatch": "Es gibt keine Gäste, die den aktuellen Filtern entsprechen."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Proxmox-Statistiken aktivieren",
|
||||
"enableProxmoxStatsDesc": "Zeigen Sie für diesen Host eine Registerkarte „Proxmox Stats“ mit Live-Statistiken zu Knoten, Gastsystemen, Speicher und Cluster an, die über SSH erfasst werden.",
|
||||
"proxmoxStatsPollInterval": "Abfrageintervall (Sekunden)",
|
||||
"proxmoxStatsPollIntervalDesc": "Mindestens 15 Sekunden. Wie oft sollen die Proxmox-Knotenstatistiken aktualisiert werden?",
|
||||
"proxmoxStatsNodeOverride": "Knotennamen überschreiben",
|
||||
"proxmoxStatsNodeOverrideDesc": "Die automatische Erkennung des Proxmox-Knotennamens (standardmäßig über den Hostnamen ermittelt) kann deaktiviert werden.",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automatisch anhand des Hostnamens erkannt",
|
||||
"authTypeStepca": "Schritt CA",
|
||||
"proxmoxIntegration": "Proxmox-Integration",
|
||||
"enableProxmox": "Proxmox aktivieren",
|
||||
"enableProxmoxDesc": "Diesen Host als Proxmox-Knoten markieren. Ermöglicht die Erkennung und den Import von Gästen direkt von diesem Host.",
|
||||
"proxmoxDefaultAuthType": "Standard-Authentifizierungstyp",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentifizierungsmethode, die auf importierte Gast-Hosts angewendet wird. Wählen Sie den Authentifizierungstyp, der Ihrer Verbindungsmethode zu den Gästen entspricht.",
|
||||
"authTypePassword": "Passwort",
|
||||
"authTypeKey": "SSH-Schlüssel",
|
||||
"authTypeCredential": "Anmeldedaten",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Keine",
|
||||
"proxmoxDefaultCredential": "Standard-Anmeldedaten",
|
||||
"proxmoxDefaultCredentialDesc": "Anmeldedaten, die für importierte Gastsysteme verwendet werden, wenn der Authentifizierungstyp auf Anmeldedaten gesetzt ist.",
|
||||
"proxmoxWindowsDetection": "Windows-/RDP-Erkennung",
|
||||
"proxmoxWindowsDetectionDesc": "Kommagetrennte Namensmuster, die RDP statt SSH auslösen (Groß-/Kleinschreibung wird nicht beachtet).",
|
||||
"proxmoxDockerDetection": "Docker-Erkennung",
|
||||
"proxmoxDockerDetectionDesc": "Kommagetrennte Namensmuster, die Docker für passende Gastsysteme aktivieren.",
|
||||
"proxmoxPreferredRanges": "Bevorzugte IP-Bereiche",
|
||||
"proxmoxPreferredRangesDesc": "Kommagetrennte Präfixe in Prioritätsreihenfolge für die IP-Auswahl, wenn ein Gastsystem mehrere Schnittstellen hat.",
|
||||
"proxmoxAutoSync": "Gastsysteme automatisch synchronisieren",
|
||||
"proxmoxAutoSyncDesc": "Diesen Proxmox-Knoten periodisch erkennen und importierte Gastsysteme erstellen oder aktualisieren, während Ihre Sitzung entsperrt ist.",
|
||||
"proxmoxSyncInterval": "Synchronisierungsintervall (Minuten)",
|
||||
"proxmoxSyncIntervalDesc": "Mindestens 5 Minuten. Der Scheduler überspringt gesperrte Benutzersitzungen.",
|
||||
"proxmoxMarkMissing": "Fehlende Gastsysteme markieren",
|
||||
"proxmoxMarkMissingDesc": "Ein proxmox-missing-Tag hinzufügen, wenn ein zuvor importiertes Gastsystem verschwindet, anstatt es zu löschen.",
|
||||
"proxmoxLastSync": "Letzte Synchronisierung",
|
||||
"proxmoxLastSyncNever": "Noch nicht synchronisiert",
|
||||
"proxmoxLastSyncNoResult": "Keine Ergebnisdetails",
|
||||
"proxmoxLastSyncSummary": "{{created}} erstellt, {{updated}} aktualisiert, {{markedMissing}} fehlend, {{skipped}} übersprungen",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Fehlgeschlagen",
|
||||
"pending": "Ausstehend",
|
||||
"success": "Erfolg"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Proxmox-Gastsysteme erkennen & importieren",
|
||||
"proxmoxImportTitle": "Von Proxmox importieren",
|
||||
"proxmoxSelectHost": "Proxmox-Host auswählen…",
|
||||
"proxmoxDiscover": "Erkennen",
|
||||
"proxmoxDiscovering": "Erkennen…",
|
||||
"proxmoxDiscoverGuests": "Gastsysteme erkennen",
|
||||
"proxmoxSelectAll": "Alle auswählen",
|
||||
"proxmoxDeselectAll": "Alle abwählen",
|
||||
"proxmoxNoGuests": "Keine Gastsysteme auf diesem Proxmox-Knoten gefunden.",
|
||||
"proxmoxResultImported": "{{count}} importiert",
|
||||
"proxmoxResultUpdated": "{{count}} aktualisiert",
|
||||
"proxmoxResultFailed": "{{count}} fehlgeschlagen",
|
||||
"proxmoxImportComplete": "Proxmox-Import abgeschlossen: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Erkennung fehlgeschlagen",
|
||||
"proxmoxImportFailed": "Import fehlgeschlagen",
|
||||
"docsLink": "Dokumentation anzeigen",
|
||||
"hostNotFound": "Host nicht gefunden",
|
||||
"loadingHost": "Host wird geladen...",
|
||||
"none": "Keine",
|
||||
"proxmoxGuestsSelected_one": "{{count}} Gast, {{selected}} ausgewählt",
|
||||
"proxmoxGuestsSelected_other": "{{count}} Gäste, {{selected}} ausgewählt",
|
||||
"proxmoxImportButton_one": "{{count}} Host importieren",
|
||||
"proxmoxImportButton_other": "{{count}} Hosts importieren"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Verbunden"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Στατιστικά Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Στατιστικά Proxmox",
|
||||
"cpuUsage": "Χρήση CPU",
|
||||
"memoryUsage": "Χρήση μνήμης",
|
||||
"diskUsage": "Χρήση δίσκου",
|
||||
"networkInterfaces": "Διεπαφές δικτύου",
|
||||
"uptime": "Χρόνος λειτουργίας",
|
||||
"systemInfo": "Πληροφορίες συστήματος",
|
||||
"guestsSummary": "Επισκέπτες",
|
||||
"storagePools": "Πισίνες αποθήκευσης",
|
||||
"clusterHealth": "Υγεία συμπλέγματος",
|
||||
"cpuCores_one": "{{count}} Πυρήνας",
|
||||
"cpuCores_other": "{{count}} Πυρήνες",
|
||||
"used": "Μεταχειρισμένος",
|
||||
"total": "Σύνολο",
|
||||
"hostname": "Όνομα κεντρικού υπολογιστή",
|
||||
"kernel": "Πυρήνας",
|
||||
"pveVersion": "Έκδοση PVE",
|
||||
"name": "Ονομα",
|
||||
"status": "Κατάσταση",
|
||||
"type": "Τύπος",
|
||||
"cpu": "ΚΜΕ",
|
||||
"mem": "Μνήμη",
|
||||
"disk": "Δίσκος",
|
||||
"running": "Τρέξιμο",
|
||||
"stopped": "Σταμάτησε",
|
||||
"guestCounts": "{{running}}/{{total}} σε λειτουργία",
|
||||
"inactive": "Αδρανής",
|
||||
"notClustered": "Αυτόνομος κόμβος (δεν αποτελεί μέρος συμπλέγματος)",
|
||||
"quorate": "Απαρτίζω",
|
||||
"notQuorate": "Δεν υπάρχει απαρτία",
|
||||
"nodeOnline": "Διαδικτυακά",
|
||||
"nodeOffline": "Εκτός σύνδεσης",
|
||||
"noHostSelected": "Δεν έχει επιλεγεί κεντρικός υπολογιστής",
|
||||
"pveshNotFound": "Το pvesh δεν βρέθηκε - είναι αυτός κόμβος Proxmox;",
|
||||
"nodeDetectionFailed": "Δεν είναι δυνατός ο προσδιορισμός ενός έγκυρου ονόματος κόμβου Proxmox",
|
||||
"connecting": "Συνδετικός...",
|
||||
"connectionFailed": "Αποτυχία σύνδεσης με τον κόμβο Proxmox",
|
||||
"refresh": "Φρεσκάρω",
|
||||
"searchGuests": "Αναζήτηση επισκεπτών...",
|
||||
"filterAll": "Ολοι",
|
||||
"filterVm": "Εικονική Μηχανή",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Δεν βρέθηκαν εικονικές μηχανές ή κοντέινερ LXC σε αυτόν τον κόμβο.",
|
||||
"noGuestsMatch": "Δεν υπάρχουν επισκέπτες που να αντιστοιχούν στα τρέχοντα φίλτρα."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Ενεργοποίηση στατιστικών Proxmox",
|
||||
"enableProxmoxStatsDesc": "Εμφάνιση μιας καρτέλας Proxmox Stats για αυτόν τον κεντρικό υπολογιστή με στατιστικά στοιχεία ζωντανού κόμβου, επισκέπτη, χώρου αποθήκευσης και συμπλέγματος που συλλέγονται μέσω SSH.",
|
||||
"proxmoxStatsPollInterval": "Διάστημα δημοσκόπησης (δευτερόλεπτα)",
|
||||
"proxmoxStatsPollIntervalDesc": "Ελάχιστο 15 δευτερόλεπτα. Πόσο συχνά θα ανανεώνονται τα στατιστικά στοιχεία του κόμβου Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Παράκαμψη ονόματος κόμβου",
|
||||
"proxmoxStatsNodeOverrideDesc": "Παράκαμψη αυτόματης ανίχνευσης του ονόματος κόμβου Proxmox (εντοπίζεται μέσω του ονόματος κεντρικού υπολογιστή από προεπιλογή).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Αυτόματη ανίχνευση από το όνομα κεντρικού υπολογιστή",
|
||||
"authTypeStepca": "Βήμα CA",
|
||||
"proxmoxIntegration": "Ενσωμάτωση Proxmox",
|
||||
"enableProxmox": "Ενεργοποίηση Proxmox",
|
||||
"enableProxmoxDesc": "Σημειώστε αυτόν τον κεντρικό υπολογιστή ως κόμβο Proxmox. Επιτρέπει την ανακάλυψη επισκεπτών και την απευθείας εισαγωγή από αυτόν τον κεντρικό υπολογιστή.",
|
||||
"proxmoxDefaultAuthType": "Προεπιλεγμένος τύπος εξουσιοδότησης",
|
||||
"proxmoxDefaultAuthTypeDesc": "Μέθοδος ελέγχου ταυτότητας που εφαρμόστηκε στους εισαγόμενους φιλοξενούμενους οικοδεσπότες. Επιλέξτε τον τύπο ελέγχου ταυτότητας που ταιριάζει με τον τρόπο που συνδέεστε με τους επισκέπτες σας.",
|
||||
"authTypePassword": "Σύνθημα",
|
||||
"authTypeKey": "Κλειδί SSH",
|
||||
"authTypeCredential": "Πιστοποιητικό",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Κανένας",
|
||||
"proxmoxDefaultCredential": "Προεπιλεγμένα διαπιστευτήρια",
|
||||
"proxmoxDefaultCredentialDesc": "Διαπιστευτήριο που χρησιμοποιείται για εισαγόμενους φιλοξενούμενους οικοδεσπότες όταν ο τύπος εξουσιοδότησης έχει οριστεί σε Διαπιστευτήριο.",
|
||||
"proxmoxWindowsDetection": "Ανίχνευση Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Μοτίβα ονομάτων διαχωρισμένα με κόμματα που ενεργοποιούν το RDP αντί για SSH (χωρίς διάκριση πεζών-κεφαλαίων)",
|
||||
"proxmoxDockerDetection": "Ανίχνευση Docker",
|
||||
"proxmoxDockerDetectionDesc": "Μοτίβα ονομάτων διαχωρισμένα με κόμματα που επιτρέπουν στο Docker να αντιστοιχίζει επισκέπτες",
|
||||
"proxmoxPreferredRanges": "Προτιμώμενα εύρη IP",
|
||||
"proxmoxPreferredRangesDesc": "Προθέματα διαχωρισμένα με κόμμα σε σειρά προτεραιότητας για την επιλογή IP όταν ένας επισκέπτης έχει πολλαπλές διεπαφές",
|
||||
"proxmoxAutoSync": "Αυτόματος συγχρονισμός επισκεπτών",
|
||||
"proxmoxAutoSyncDesc": "Εντοπίζετε περιοδικά αυτόν τον κόμβο Proxmox και δημιουργείτε ή ενημερώνετε εισαγόμενους φιλοξενούμενους υπολογιστές όσο η συνεδρία σας είναι ξεκλείδωτη.",
|
||||
"proxmoxSyncInterval": "Διάστημα συγχρονισμού (λεπτά)",
|
||||
"proxmoxSyncIntervalDesc": "Ελάχιστο 5 λεπτά. Ο προγραμματιστής παραλείπει τις κλειδωμένες συνεδρίες χρήστη.",
|
||||
"proxmoxMarkMissing": "Ο Μαρκ λείπουν επισκέπτες.",
|
||||
"proxmoxMarkMissingDesc": "Προσθέστε μια ετικέτα proxmox-missing όταν εξαφανιστεί ένας προηγουμένως εισαγόμενος επισκέπτης αντί να τον διαγράψετε.",
|
||||
"proxmoxLastSync": "Τελευταίος συγχρονισμός",
|
||||
"proxmoxLastSyncNever": "Δεν έχει συγχρονιστεί ακόμα",
|
||||
"proxmoxLastSyncNoResult": "Δεν υπάρχουν λεπτομέρειες για το αποτέλεσμα",
|
||||
"proxmoxLastSyncSummary": "{{created}} δημιουργήθηκε, {{updated}} ενημερώθηκε, {{markedMissing}} λείπει, {{skipped}} παραλείφθηκε",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Αποτυχημένος",
|
||||
"pending": "Εκκρεμής",
|
||||
"success": "Επιτυχία"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Ανακάλυψη και εισαγωγή επισκεπτών Proxmox",
|
||||
"proxmoxImportTitle": "Εισαγωγή από το Proxmox",
|
||||
"proxmoxSelectHost": "Επιλέξτε έναν κεντρικό υπολογιστή Proxmox…",
|
||||
"proxmoxDiscover": "Ανακαλύπτω",
|
||||
"proxmoxDiscovering": "Ανακαλύπτοντας…",
|
||||
"proxmoxDiscoverGuests": "Ανακαλύψτε επισκέπτες",
|
||||
"proxmoxSelectAll": "Επιλογή όλων",
|
||||
"proxmoxDeselectAll": "Αποεπιλογή όλων",
|
||||
"proxmoxNoGuests": "Δεν βρέθηκαν επισκέπτες σε αυτόν τον κόμβο Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} εισήχθη",
|
||||
"proxmoxResultUpdated": "{{count}} ενημερώθηκε",
|
||||
"proxmoxResultFailed": "{{count}} απέτυχε",
|
||||
"proxmoxImportComplete": "Η εισαγωγή Proxmox ολοκληρώθηκε: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Η ανακάλυψη απέτυχε",
|
||||
"proxmoxImportFailed": "Η εισαγωγή απέτυχε.",
|
||||
"docsLink": "Προβολή εγγράφων",
|
||||
"hostNotFound": "Δεν βρέθηκε κεντρικός υπολογιστής",
|
||||
"loadingHost": "Φόρτωση κεντρικού υπολογιστή...",
|
||||
"none": "Κανένας",
|
||||
"proxmoxGuestsSelected_one": "{{count}} επισκέπτης, {{selected}} επιλεγμένος",
|
||||
"proxmoxGuestsSelected_other": "{{count}} επισκέπτες, {{selected}} επιλεγμένοι",
|
||||
"proxmoxImportButton_one": "Εισαγωγή κεντρικού υπολογιστή {{count}}",
|
||||
"proxmoxImportButton_other": "Εισαγωγή {{count}} κεντρικών υπολογιστών"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Συνδεδεμένος"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Estadísticas de Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Estadísticas de Proxmox",
|
||||
"cpuUsage": "Uso de CPU",
|
||||
"memoryUsage": "Uso de memoria",
|
||||
"diskUsage": "Uso del disco",
|
||||
"networkInterfaces": "Interfaces de red",
|
||||
"uptime": "Tiempo de actividad",
|
||||
"systemInfo": "Información del sistema",
|
||||
"guestsSummary": "Huéspedes",
|
||||
"storagePools": "Piscinas de almacenamiento",
|
||||
"clusterHealth": "Salud en grupo",
|
||||
"cpuCores_one": "{{count}} núcleo",
|
||||
"cpuCores_other": "{{count}} núcleos",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"hostname": "Nombre de host",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "Versión PVE",
|
||||
"name": "Nombre",
|
||||
"status": "Estado",
|
||||
"type": "Tipo",
|
||||
"cpu": "CPU",
|
||||
"mem": "Memoria",
|
||||
"disk": "Disco",
|
||||
"running": "En ejecución",
|
||||
"stopped": "Detenido",
|
||||
"guestCounts": "{{running}}/{{total}} corriendo",
|
||||
"inactive": "Inactivo",
|
||||
"notClustered": "Nodo independiente (no forma parte de un clúster)",
|
||||
"quorate": "Con quórum",
|
||||
"notQuorate": "No hay quórum",
|
||||
"nodeOnline": "En línea",
|
||||
"nodeOffline": "Desconectado",
|
||||
"noHostSelected": "Ningún host seleccionado",
|
||||
"pveshNotFound": "pvesh no encontrado - ¿es este un nodo Proxmox?",
|
||||
"nodeDetectionFailed": "No se pudo determinar un nombre de nodo Proxmox válido.",
|
||||
"connecting": "Conectando...",
|
||||
"connectionFailed": "No se pudo conectar con el nodo Proxmox.",
|
||||
"refresh": "Actualizar",
|
||||
"searchGuests": "Buscar huéspedes...",
|
||||
"filterAll": "Revocar todo",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No se encontraron máquinas virtuales ni contenedores LXC en este nodo.",
|
||||
"noGuestsMatch": "No hay huéspedes que coincidan con los filtros actuales."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Habilitar estadísticas de Proxmox",
|
||||
"enableProxmoxStatsDesc": "Muestra una pestaña de estadísticas de Proxmox para este host con estadísticas en tiempo real del nodo, invitado, almacenamiento y clúster recopiladas a través de SSH.",
|
||||
"proxmoxStatsPollInterval": "Intervalo de sondeo (segundos)",
|
||||
"proxmoxStatsPollIntervalDesc": "Mínimo 15 segundos. Con qué frecuencia se actualizan las estadísticas del nodo Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Anulación del nombre del nodo",
|
||||
"proxmoxStatsNodeOverrideDesc": "Anular la detección automática del nombre del nodo Proxmox (detectado por defecto mediante el nombre de host).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Detectado automáticamente a partir del nombre de host.",
|
||||
"authTypeStepca": "Paso CA",
|
||||
"proxmoxIntegration": "Integración con Proxmox",
|
||||
"enableProxmox": "Activar Proxmox",
|
||||
"enableProxmoxDesc": "Marcar este host como un nodo Proxmox. Permite la detección e importación de invitados directamente desde este host.",
|
||||
"proxmoxDefaultAuthType": "Tipo de autenticación predeterminado",
|
||||
"proxmoxDefaultAuthTypeDesc": "Método de autenticación aplicado a los hosts invitados importados. Elija el tipo de autenticación que coincida con cómo se conecta a sus invitados.",
|
||||
"authTypePassword": "Contraseña",
|
||||
"authTypeKey": "Clave SSH",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Ninguno",
|
||||
"proxmoxDefaultCredential": "Credencial predeterminada",
|
||||
"proxmoxDefaultCredentialDesc": "Credencial utilizada para los hosts invitados importados cuando el tipo de autenticación está configurado como Credencial.",
|
||||
"proxmoxWindowsDetection": "Detección de Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Patrones de nombres separados por comas que activan RDP en lugar de SSH (sin distinción de mayúsculas/minúsculas)",
|
||||
"proxmoxDockerDetection": "Detección de Docker",
|
||||
"proxmoxDockerDetectionDesc": "Patrones de nombres separados por comas que habilitan Docker para los invitados coincidentes",
|
||||
"proxmoxPreferredRanges": "Rangos de IP preferidos",
|
||||
"proxmoxPreferredRangesDesc": "Prefijos separados por comas en orden de prioridad para la selección de IP cuando un invitado tiene múltiples interfaces",
|
||||
"proxmoxAutoSync": "Sincronización automática de invitados",
|
||||
"proxmoxAutoSyncDesc": "Descubrir periódicamente este nodo Proxmox y crear o actualizar los hosts invitados importados mientras la sesión está desbloqueada.",
|
||||
"proxmoxSyncInterval": "Intervalo de sincronización (minutos)",
|
||||
"proxmoxSyncIntervalDesc": "Mínimo 5 minutos. El planificador omite las sesiones de usuario bloqueadas.",
|
||||
"proxmoxMarkMissing": "Marcar invitados faltantes",
|
||||
"proxmoxMarkMissingDesc": "Añadir una etiqueta proxmox-missing cuando un invitado previamente importado desaparece en lugar de eliminarlo.",
|
||||
"proxmoxLastSync": "Última sincronización",
|
||||
"proxmoxLastSyncNever": "Aún no sincronizado",
|
||||
"proxmoxLastSyncNoResult": "Sin detalles del resultado",
|
||||
"proxmoxLastSyncSummary": "{{created}} creados, {{updated}} actualizados, {{markedMissing}} faltantes, {{skipped}} omitidos",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Fallido",
|
||||
"pending": "Pendiente",
|
||||
"success": "Correcto"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Descubrir e importar invitados Proxmox",
|
||||
"proxmoxImportTitle": "Importar desde Proxmox",
|
||||
"proxmoxSelectHost": "Seleccionar un host Proxmox…",
|
||||
"proxmoxDiscover": "Descubrir",
|
||||
"proxmoxDiscovering": "Descubriendo…",
|
||||
"proxmoxDiscoverGuests": "Descubrir invitados",
|
||||
"proxmoxSelectAll": "Seleccionar todos",
|
||||
"proxmoxDeselectAll": "Deseleccionar todos",
|
||||
"proxmoxNoGuests": "No se encontraron invitados en este nodo Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} importados",
|
||||
"proxmoxResultUpdated": "{{count}} actualizados",
|
||||
"proxmoxResultFailed": "{{count}} fallidos",
|
||||
"proxmoxImportComplete": "Importación de Proxmox completada: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Descubrimiento fallido",
|
||||
"proxmoxImportFailed": "Importación fallida",
|
||||
"docsLink": "Ver documentación",
|
||||
"hostNotFound": "Host no encontrado",
|
||||
"loadingHost": "Cargando host...",
|
||||
"none": "Ninguno",
|
||||
"proxmoxGuestsSelected_one": "{{count}} invitado, {{selected}} seleccionado",
|
||||
"proxmoxGuestsSelected_other": "{{count}} invitados, {{selected}} seleccionados",
|
||||
"proxmoxImportButton_one": "Importar {{count}} host",
|
||||
"proxmoxImportButton_other": "Importar {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Conectado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox-tilastot"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox-tilastot",
|
||||
"cpuUsage": "Suorittimen käyttö",
|
||||
"memoryUsage": "Muistin käyttö",
|
||||
"diskUsage": "Levyn käyttö",
|
||||
"networkInterfaces": "Verkkoliitännät",
|
||||
"uptime": "Käyttöaika",
|
||||
"systemInfo": "Järjestelmätiedot",
|
||||
"guestsSummary": "Vieraat",
|
||||
"storagePools": "Varastoaltaat",
|
||||
"clusterHealth": "Klusterin terveys",
|
||||
"cpuCores_one": "{{count}} Ydin",
|
||||
"cpuCores_other": "{{count}} Ytimet",
|
||||
"used": "Käytetty",
|
||||
"total": "Kokonais",
|
||||
"hostname": "Isäntänimi",
|
||||
"kernel": "Ydin",
|
||||
"pveVersion": "PVE-versio",
|
||||
"name": "Nimi",
|
||||
"status": "Status",
|
||||
"type": "Tyyppi",
|
||||
"cpu": "Suoritin",
|
||||
"mem": "Mem",
|
||||
"disk": "Levy",
|
||||
"running": "Juoksu",
|
||||
"stopped": "Pysähtynyt",
|
||||
"guestCounts": "{{running}}/{{total}} käynnissä",
|
||||
"inactive": "Passiivinen",
|
||||
"notClustered": "Itsenäinen solmu (ei osa klusteria)",
|
||||
"quorate": "Päätösvaltainen",
|
||||
"notQuorate": "Ei päätöslauselma",
|
||||
"nodeOnline": "Verkossa",
|
||||
"nodeOffline": "Offline-tilassa",
|
||||
"noHostSelected": "Ei valittua isäntää",
|
||||
"pveshNotFound": "pvesh-solmua ei löytynyt - onko tämä Proxmox-solmu?",
|
||||
"nodeDetectionFailed": "Kelvollista Proxmox-solmun nimeä ei voitu määrittää.",
|
||||
"connecting": "Yhdistetään...",
|
||||
"connectionFailed": "Yhteyden muodostaminen Proxmox-solmuun epäonnistui",
|
||||
"refresh": "Päivitä",
|
||||
"searchGuests": "Hae vieraita...",
|
||||
"filterAll": "Kaikki",
|
||||
"filterVm": "Virtuaalikone",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Tältä solmulta ei löytynyt virtuaalikoneita tai LXC-säilöjä.",
|
||||
"noGuestsMatch": "Yksikään vieras ei vastaa nykyisiä suodattimia."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Ota Proxmox-tilastot käyttöön",
|
||||
"enableProxmoxStatsDesc": "Näytä tämän isännän Proxmox-tilastot-välilehti, joka sisältää SSH:n kautta kerätyt reaaliaikaiset solmu-, vieras-, tallennus- ja klusteritilastot.",
|
||||
"proxmoxStatsPollInterval": "Kyselyväli (sekuntia)",
|
||||
"proxmoxStatsPollIntervalDesc": "Vähintään 15 sekuntia. Kuinka usein Proxmox-solmun tilastot päivitetään.",
|
||||
"proxmoxStatsNodeOverride": "Solmun nimen ohitus",
|
||||
"proxmoxStatsNodeOverrideDesc": "Ohita Proxmox-solmun nimen automaattinen tunnistus (tunnistetaan oletusarvoisesti isäntänimen kautta).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automaattisesti tunnistettu isäntänimestä",
|
||||
"authTypeStepca": "Vaihe CA",
|
||||
"proxmoxIntegration": "Proxmox-integraatio",
|
||||
"enableProxmox": "Ota Proxmox käyttöön",
|
||||
"enableProxmoxDesc": "Merkitse tämä isäntä Proxmox-solmuksi. Mahdollistaa vieraslaitteiden etsinnän ja tuonnin suoraan tästä isännästä.",
|
||||
"proxmoxDefaultAuthType": "Oletustodennustyyppi",
|
||||
"proxmoxDefaultAuthTypeDesc": "Tuotuihin vieraspalvelimiin sovellettu todennustapa. Valitse todennustyyppi, joka vastaa tapaa, jolla muodostat yhteyden vieraisiin palvelimiin.",
|
||||
"authTypePassword": "Salasana",
|
||||
"authTypeKey": "SSH-avain",
|
||||
"authTypeCredential": "Valtakirja",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Ei mitään",
|
||||
"proxmoxDefaultCredential": "Oletustunnistetiedot",
|
||||
"proxmoxDefaultCredentialDesc": "Tuotujen vieraspalvelimien tunnistetiedot, kun todennustyypiksi on asetettu Tunnistetiedot.",
|
||||
"proxmoxWindowsDetection": "Windowsin / RDP-tunnistus",
|
||||
"proxmoxWindowsDetectionDesc": "Pilkuilla erotetut nimimallit, jotka käynnistävät RDP:n SSH:n sijaan (kirjainkokoa ei erotella)",
|
||||
"proxmoxDockerDetection": "Docker-tunnistus",
|
||||
"proxmoxDockerDetectionDesc": "Pilkuilla erotetut nimimallit, joiden avulla Docker löytää vieraat yhteensopiviksi",
|
||||
"proxmoxPreferredRanges": "Suositellut IP-alueet",
|
||||
"proxmoxPreferredRangesDesc": "Pilkuilla erotetut etuliitteet prioriteettijärjestyksessä IP-osoitteen valinnassa, kun vieraalla on useita rajapintoja",
|
||||
"proxmoxAutoSync": "Synkronoi vieraat automaattisesti",
|
||||
"proxmoxAutoSyncDesc": "Etsi tämä Proxmox-solmu säännöllisesti ja luo tai päivitä tuotuja vieraskomanteja istunnon ollessa lukitsematon.",
|
||||
"proxmoxSyncInterval": "Synkronointiväli (minuuttia)",
|
||||
"proxmoxSyncIntervalDesc": "Vähintään 5 minuuttia. Ajastin ohittaa lukitut käyttäjäistunnot.",
|
||||
"proxmoxMarkMissing": "Merkitse puuttuvat vieraat",
|
||||
"proxmoxMarkMissingDesc": "Lisää proxmox-missing-tunniste, kun aiemmin tuotu vieras katoaa sen poistamisen sijaan.",
|
||||
"proxmoxLastSync": "Viimeisin synkronointi",
|
||||
"proxmoxLastSyncNever": "Ei vielä synkronoitu",
|
||||
"proxmoxLastSyncNoResult": "Ei tulostietoja",
|
||||
"proxmoxLastSyncSummary": "{{created}} luotu, {{updated}} päivitetty, {{markedMissing}} puuttuu, {{skipped}} ohitettu",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Epäonnistui",
|
||||
"pending": "Odottaa",
|
||||
"success": "Menestys"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Löydä ja tuo Proxmox-vieraita",
|
||||
"proxmoxImportTitle": "Tuo Proxmoxista",
|
||||
"proxmoxSelectHost": "Valitse Proxmox-isäntä…",
|
||||
"proxmoxDiscover": "Tutustu",
|
||||
"proxmoxDiscovering": "Löytöretkellä…",
|
||||
"proxmoxDiscoverGuests": "Tutustu vieraisiin",
|
||||
"proxmoxSelectAll": "Valitse kaikki",
|
||||
"proxmoxDeselectAll": "Poista kaikkien valinta",
|
||||
"proxmoxNoGuests": "Tältä Proxmox-solmulta ei löytynyt vieraita.",
|
||||
"proxmoxResultImported": "{{count}} tuotu",
|
||||
"proxmoxResultUpdated": "{{count}} päivitetty",
|
||||
"proxmoxResultFailed": "{{count}} epäonnistui",
|
||||
"proxmoxImportComplete": "Proxmox-tuonti valmis: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Löytö epäonnistui",
|
||||
"proxmoxImportFailed": "Tuonti epäonnistui",
|
||||
"docsLink": "Näytä dokumentit",
|
||||
"hostNotFound": "Isäntää ei löydy",
|
||||
"loadingHost": "Ladataan isäntää...",
|
||||
"none": "Ei mitään",
|
||||
"proxmoxGuestsSelected_one": "{{count}} vieras, {{selected}} valittu",
|
||||
"proxmoxGuestsSelected_other": "{{count}} vierasta, {{selected}} valittu",
|
||||
"proxmoxImportButton_one": "Tuo {{count}} isäntä",
|
||||
"proxmoxImportButton_other": "Tuo {{count}} isäntiä"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Yhdistetty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Statistiques Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Statistiques Proxmox",
|
||||
"cpuUsage": "Utilisation CPU",
|
||||
"memoryUsage": "Utilisation mémoire",
|
||||
"diskUsage": "Utilisation disque",
|
||||
"networkInterfaces": "Interfaces réseau",
|
||||
"uptime": "Disponibilité",
|
||||
"systemInfo": "Informations système",
|
||||
"guestsSummary": "Les invités",
|
||||
"storagePools": "Piscines de stockage",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} cœur",
|
||||
"cpuCores_other": "{{count}} cœurs",
|
||||
"used": "Utilisé",
|
||||
"total": "Total",
|
||||
"hostname": "Nom d'hôte",
|
||||
"kernel": "Noyau",
|
||||
"pveVersion": "Version JcE",
|
||||
"name": "Nom",
|
||||
"status": "État",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disque",
|
||||
"running": "En cours d’exécution",
|
||||
"stopped": "Arrêté",
|
||||
"guestCounts": "{{running}}/{{total}} en cours",
|
||||
"inactive": "Inactif",
|
||||
"notClustered": "Nœud autonome (ne faisant pas partie d'un cluster)",
|
||||
"quorate": "Quaire",
|
||||
"notQuorate": "Pas de quorum",
|
||||
"nodeOnline": "En ligne",
|
||||
"nodeOffline": "Hors ligne",
|
||||
"noHostSelected": "Aucun hôte sélectionné",
|
||||
"pveshNotFound": "pvesh introuvable - s'agit-il d'un nœud Proxmox ?",
|
||||
"nodeDetectionFailed": "Impossible de déterminer un nom de nœud Proxmox valide",
|
||||
"connecting": "Connexion...",
|
||||
"connectionFailed": "Échec de la connexion au nœud Proxmox",
|
||||
"refresh": "Actualiser",
|
||||
"searchGuests": "Rechercher des invités...",
|
||||
"filterAll": "Tout",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Aucune machine virtuelle ni conteneur LXC n'a été trouvé sur ce nœud.",
|
||||
"noGuestsMatch": "Aucun invité ne correspond aux filtres actuels."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Activer les statistiques Proxmox",
|
||||
"enableProxmoxStatsDesc": "Afficher un onglet Statistiques Proxmox pour cet hôte avec des statistiques en direct sur les nœuds, les invités, le stockage et le cluster collectées via SSH.",
|
||||
"proxmoxStatsPollInterval": "Intervalle d'interrogation (secondes)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 secondes. Fréquence d'actualisation des statistiques du nœud Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Remplacement du nom du nœud",
|
||||
"proxmoxStatsNodeOverrideDesc": "Désactiver la détection automatique du nom du nœud Proxmox (détecté par défaut via le nom d'hôte).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Détection automatique à partir du nom d'hôte",
|
||||
"authTypeStepca": "Étape CA",
|
||||
"proxmoxIntegration": "Intégration Proxmox",
|
||||
"enableProxmox": "Activer Proxmox",
|
||||
"enableProxmoxDesc": "Marquer cet hôte comme un nœud Proxmox. Permet la découverte et l'importation des invités directement depuis cet hôte.",
|
||||
"proxmoxDefaultAuthType": "Type d'authentification par défaut",
|
||||
"proxmoxDefaultAuthTypeDesc": "Méthode d'authentification appliquée aux hôtes invités importés. Choisissez le type d'authentification correspondant à votre méthode de connexion aux invités.",
|
||||
"authTypePassword": "Mot de passe",
|
||||
"authTypeKey": "Clé SSH",
|
||||
"authTypeCredential": "Identifiants",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Aucun",
|
||||
"proxmoxDefaultCredential": "Identifiant par défaut",
|
||||
"proxmoxDefaultCredentialDesc": "Identifiant utilisé pour les hôtes invités importés lorsque le type d'authentification est défini sur Identifiant.",
|
||||
"proxmoxWindowsDetection": "Détection Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Liste de motifs de noms, séparés par des virgules, qui déclenchent RDP au lieu de SSH (insensible à la casse).",
|
||||
"proxmoxDockerDetection": "Détection Docker",
|
||||
"proxmoxDockerDetectionDesc": "Liste de motifs de noms, séparés par des virgules, qui activent Docker pour les invités correspondants.",
|
||||
"proxmoxPreferredRanges": "Plages IP préférées",
|
||||
"proxmoxPreferredRangesDesc": "Préfixes séparés par des virgules, par ordre de priorité, pour la sélection d'adresse IP lorsqu'un invité possède plusieurs interfaces.",
|
||||
"proxmoxAutoSync": "Synchronisation automatique des invités",
|
||||
"proxmoxAutoSyncDesc": "Découvre périodiquement ce nœud Proxmox et crée ou met à jour les hôtes invités importés tant que votre session est déverrouillée.",
|
||||
"proxmoxSyncInterval": "Intervalle de synchronisation (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. Le planificateur ignore les sessions utilisateur verrouillées.",
|
||||
"proxmoxMarkMissing": "Marquer les invités manquants",
|
||||
"proxmoxMarkMissingDesc": "Ajoute une étiquette proxmox-missing lorsqu'un invité précédemment importé disparaît, au lieu de le supprimer.",
|
||||
"proxmoxLastSync": "Dernière synchronisation",
|
||||
"proxmoxLastSyncNever": "Pas encore synchronisé",
|
||||
"proxmoxLastSyncNoResult": "Aucun détail de résultat",
|
||||
"proxmoxLastSyncSummary": "{{created}} créé(s), {{updated}} mis à jour, {{markedMissing}} manquant(s), {{skipped}} ignoré(s)",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Échec",
|
||||
"pending": "En attente",
|
||||
"success": "Succès"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Découvrir et importer les invités Proxmox",
|
||||
"proxmoxImportTitle": "Importer depuis Proxmox",
|
||||
"proxmoxSelectHost": "Sélectionner un hôte Proxmox…",
|
||||
"proxmoxDiscover": "Découvrir",
|
||||
"proxmoxDiscovering": "Découverte en cours…",
|
||||
"proxmoxDiscoverGuests": "Découvrir les invités",
|
||||
"proxmoxSelectAll": "Tout sélectionner",
|
||||
"proxmoxDeselectAll": "Tout désélectionner",
|
||||
"proxmoxNoGuests": "Aucun invité trouvé sur ce nœud Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} importés",
|
||||
"proxmoxResultUpdated": "{{count}} mis à jour",
|
||||
"proxmoxResultFailed": "{{count}} échoués",
|
||||
"proxmoxImportComplete": "Importation Proxmox terminée : {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Échec de la découverte",
|
||||
"proxmoxImportFailed": "Échec de l'importation",
|
||||
"docsLink": "Voir la documentation",
|
||||
"hostNotFound": "Hôte introuvable",
|
||||
"loadingHost": "Chargement de l'hôte...",
|
||||
"none": "Aucun",
|
||||
"proxmoxGuestsSelected_one": "{{count}} invité, {{selected}} sélectionné",
|
||||
"proxmoxGuestsSelected_other": "{{count}} invités, {{selected}} sélectionnés",
|
||||
"proxmoxImportButton_one": "Importer {{count}} hôte",
|
||||
"proxmoxImportButton_other": "Importer {{count}} hôtes"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connecté"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "סטטיסטיקות פרוקסמוקס"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "סטטיסטיקות פרוקסמוקס",
|
||||
"cpuUsage": "שימוש במעבד",
|
||||
"memoryUsage": "שימוש בזיכרון",
|
||||
"diskUsage": "שימוש בדיסק",
|
||||
"networkInterfaces": "ממשקי רשת",
|
||||
"uptime": "זמן פעולה",
|
||||
"systemInfo": "מידע מערכת",
|
||||
"guestsSummary": "אורחים",
|
||||
"storagePools": "בריכות אחסון",
|
||||
"clusterHealth": "בריאות אשכול",
|
||||
"cpuCores_one": "{{count}} ליבה",
|
||||
"cpuCores_other": "{{count}} ליבות",
|
||||
"used": "מְשׁוּמָשׁ",
|
||||
"total": "סַך הַכֹּל",
|
||||
"hostname": "שם מארח",
|
||||
"kernel": "גַרעִין",
|
||||
"pveVersion": "גרסת PVE",
|
||||
"name": "שֵׁם",
|
||||
"status": "סטָטוּס",
|
||||
"type": "סוּג",
|
||||
"cpu": "מעבד",
|
||||
"mem": "מ",
|
||||
"disk": "דִיסק",
|
||||
"running": "רִיצָה",
|
||||
"stopped": "נֶעצָר",
|
||||
"guestCounts": "ריצה {{running}}/ ריצה{{total}}",
|
||||
"inactive": "לֹא פָּעִיל",
|
||||
"notClustered": "צומת עצמאי (לא חלק מאשכול)",
|
||||
"quorate": "קוורט",
|
||||
"notQuorate": "לא נמצא מניין חוקי",
|
||||
"nodeOnline": "באינטרנט",
|
||||
"nodeOffline": "לא מקוון",
|
||||
"noHostSelected": "לא נבחר מארח",
|
||||
"pveshNotFound": "pvesh לא נמצא - האם זהו צומת Proxmox?",
|
||||
"nodeDetectionFailed": "לא ניתן לקבוע שם צומת Proxmox תקין",
|
||||
"connecting": "מְקַשֵׁר...",
|
||||
"connectionFailed": "נכשלה ההתחברות לצומת Proxmox",
|
||||
"refresh": "לְרַעֲנֵן",
|
||||
"searchGuests": "חיפוש אורחים...",
|
||||
"filterAll": "כֹּל",
|
||||
"filterVm": "מכונה וירטואלית",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "לא נמצאו מכונות וירטואליות או מכולות LXC בצומת זה.",
|
||||
"noGuestsMatch": "אין אורחים התואמים את המסננים הנוכחיים."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "פרוקסמוקס",
|
||||
"enableProxmoxStats": "הפעל סטטיסטיקות Proxmox",
|
||||
"enableProxmoxStatsDesc": "הצג כרטיסיית סטטיסטיקות Proxmox עבור מארח זה עם סטטיסטיקות של צומת, אורח, אחסון ואשכול שנאספו דרך SSH.",
|
||||
"proxmoxStatsPollInterval": "מרווח זמן בין סקר (שניות)",
|
||||
"proxmoxStatsPollIntervalDesc": "מינימום 15 שניות. באיזו תדירות לרענן את סטטיסטיקות הצומת של Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "עקיפת שם הצומת",
|
||||
"proxmoxStatsNodeOverrideDesc": "עקיפת זיהוי אוטומטי של שם הצומת Proxmox (מזוהה דרך שם המארח כברירת מחדל).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "זוהה אוטומטית משם המארח",
|
||||
"authTypeStepca": "שלב CA",
|
||||
"proxmoxIntegration": "אינטגרציה של פרוקסמוקס",
|
||||
"enableProxmox": "הפעל את Proxmox",
|
||||
"enableProxmoxDesc": "סמן מארח זה כצומת Proxmox. מאפשר גילוי אורחים וייבוא ישירות ממארח זה.",
|
||||
"proxmoxDefaultAuthType": "סוג אימות ברירת מחדל",
|
||||
"proxmoxDefaultAuthTypeDesc": "שיטת אימות הוחלת על מארחי אורחים מיובאים. בחר את סוג האימות שמתאים לאופן שבו אתה מתחבר לאורחים שלך.",
|
||||
"authTypePassword": "סִיסמָה",
|
||||
"authTypeKey": "מפתח SSH",
|
||||
"authTypeCredential": "תְעוּדָה",
|
||||
"authTypeOpkssh": "אופקש",
|
||||
"authTypeNone": "אַף לֹא אֶחָד",
|
||||
"proxmoxDefaultCredential": "אישור ברירת מחדל",
|
||||
"proxmoxDefaultCredentialDesc": "אישור המשמש עבור מארחים אורחים מיובאים כאשר סוג האימות מוגדר לאישור.",
|
||||
"proxmoxWindowsDetection": "זיהוי Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "תבניות שמות מופרדות בפסיקים המפעילות RDP במקום SSH (לא תלויות רישיות)",
|
||||
"proxmoxDockerDetection": "זיהוי דוקר",
|
||||
"proxmoxDockerDetectionDesc": "תבניות שמות מופרדות בפסיקים המאפשרות ל-Docker להתאים אורחים",
|
||||
"proxmoxPreferredRanges": "טווחי IP מועדפים",
|
||||
"proxmoxPreferredRangesDesc": "קידומות מופרדות בפסיקים בסדר עדיפות לבחירת IP כאשר לאורח יש ממשקים מרובים",
|
||||
"proxmoxAutoSync": "סנכרון אוטומטי של אורחים",
|
||||
"proxmoxAutoSyncDesc": "גלה מעת לעת את צומת Proxmox הזה וצור או עדכן מארחי אורחים מיובאים בזמן שההפעלה שלך אינה נעולה.",
|
||||
"proxmoxSyncInterval": "מרווח סנכרון (דקות)",
|
||||
"proxmoxSyncIntervalDesc": "מינימום 5 דקות. המתזמן מדלג על סשנים נעולים של משתמשים.",
|
||||
"proxmoxMarkMissing": "סמן אורחים חסרים",
|
||||
"proxmoxMarkMissingDesc": "הוסף תג proxmox-missing כאשר אורח שיובא בעבר נעלם במקום למחוק אותו.",
|
||||
"proxmoxLastSync": "סנכרון אחרון",
|
||||
"proxmoxLastSyncNever": "עדיין לא מסונכרן",
|
||||
"proxmoxLastSyncNoResult": "אין פרטי תוצאה",
|
||||
"proxmoxLastSyncSummary": "{{created}} נוצר, {{updated}} עודכן, {{markedMissing}} חסר, {{skipped}} דילג",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "נִכשָׁל",
|
||||
"pending": "תָלוּי וְעוֹמֵד",
|
||||
"success": "הַצלָחָה"
|
||||
},
|
||||
"proxmoxDiscoverAction": "גלה וייבא אורחי Proxmox",
|
||||
"proxmoxImportTitle": "ייבוא מ-Proxmox",
|
||||
"proxmoxSelectHost": "בחר מארח Proxmox…",
|
||||
"proxmoxDiscover": "לְגַלוֹת",
|
||||
"proxmoxDiscovering": "גילוי…",
|
||||
"proxmoxDiscoverGuests": "גלה אורחים",
|
||||
"proxmoxSelectAll": "בחר הכל",
|
||||
"proxmoxDeselectAll": "בטל את הבחירה של הכל",
|
||||
"proxmoxNoGuests": "לא נמצאו אורחים בצומת Proxmox זה.",
|
||||
"proxmoxResultImported": "{{count}} מיובא",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} נכשל",
|
||||
"proxmoxImportComplete": "ייבוא Proxmox הושלם: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "הגילוי נכשל",
|
||||
"proxmoxImportFailed": "הייבוא נכשל",
|
||||
"docsLink": "הצג מסמכים",
|
||||
"hostNotFound": "המארח לא נמצא",
|
||||
"loadingHost": "טוען מארח...",
|
||||
"none": "אַף לֹא אֶחָד",
|
||||
"proxmoxGuestsSelected_one": "{{count}} אורח, {{selected}} נבחר",
|
||||
"proxmoxGuestsSelected_other": "{{count}} אורחים, {{selected}} נבחרו",
|
||||
"proxmoxImportButton_one": "ייבוא מארח {{count}}",
|
||||
"proxmoxImportButton_other": "ייבוא מארחים {{count}}"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "מְחוּבָּר"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU उपयोग",
|
||||
"memoryUsage": "मेमोरी उपयोग",
|
||||
"diskUsage": "डिस्क उपयोग",
|
||||
"networkInterfaces": "नेटवर्क इंटरफ़ेस",
|
||||
"uptime": "अपटाइम",
|
||||
"systemInfo": "सिस्टम जानकारी",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} कोर",
|
||||
"cpuCores_other": "{{count}} कोर",
|
||||
"used": "प्रयुक्त",
|
||||
"total": "Total",
|
||||
"hostname": "होस्टनाम",
|
||||
"kernel": "कर्नेल",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "नाम",
|
||||
"status": "स्थिति",
|
||||
"type": "प्रकार",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "डिस्क",
|
||||
"running": "चालू",
|
||||
"stopped": "बंद",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "निष्क्रिय",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "ऑनलाइन",
|
||||
"nodeOffline": "ऑफलाइन",
|
||||
"noHostSelected": "कोई होस्ट चयनित नहीं है",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "कनेक्ट हो रहा है...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "ताज़ा करें",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "सभी",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox एकीकरण",
|
||||
"enableProxmox": "Proxmox सक्षम करें",
|
||||
"enableProxmoxDesc": "इस होस्ट को Proxmox नोड के रूप में चिह्नित करें। इस होस्ट से सीधे अतिथि खोज और आयात सक्षम करता है।",
|
||||
"proxmoxDefaultAuthType": "डिफ़ॉल्ट प्रमाणीकरण प्रकार",
|
||||
"proxmoxDefaultAuthTypeDesc": "आयातित अतिथि होस्ट पर लागू प्रमाणीकरण विधि। वह प्रमाणीकरण प्रकार चुनें जो आपके अतिथियों से कनेक्ट करने के तरीके से मेल खाता हो।",
|
||||
"authTypePassword": "पासवर्ड",
|
||||
"authTypeKey": "SSH कुंजी",
|
||||
"authTypeCredential": "क्रेडेंशियल",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "कोई नहीं",
|
||||
"proxmoxDefaultCredential": "डिफ़ॉल्ट क्रेडेंशियल",
|
||||
"proxmoxDefaultCredentialDesc": "जब प्रमाणीकरण प्रकार 'क्रेडेंशियल' पर सेट होता है तब आयातित अतिथि होस्ट के लिए उपयोग किया जाने वाला क्रेडेंशियल।",
|
||||
"proxmoxWindowsDetection": "Windows / RDP पहचान",
|
||||
"proxmoxWindowsDetectionDesc": "अल्पविराम से अलग नाम पैटर्न जो SSH के बजाय RDP ट्रिगर करते हैं (अक्षर-आकार असंवेदी)",
|
||||
"proxmoxDockerDetection": "Docker पहचान",
|
||||
"proxmoxDockerDetectionDesc": "अल्पविराम से अलग नाम पैटर्न जो मिलान वाले अतिथियों के लिए Docker सक्षम करते हैं",
|
||||
"proxmoxPreferredRanges": "पसंदीदा IP श्रेणियाँ",
|
||||
"proxmoxPreferredRangesDesc": "अतिथि के पास कई इंटरफ़ेस होने पर IP चयन के लिए प्राथमिकता क्रम में अल्पविराम से अलग उपसर्ग",
|
||||
"proxmoxAutoSync": "अतिथि स्वतः सिंक",
|
||||
"proxmoxAutoSyncDesc": "जब आपका सत्र अनलॉक हो तो समय-समय पर इस Proxmox नोड की खोज करें और आयातित अतिथि होस्ट बनाएँ या अपडेट करें।",
|
||||
"proxmoxSyncInterval": "सिंक अंतराल (मिनट)",
|
||||
"proxmoxSyncIntervalDesc": "न्यूनतम 5 मिनट। शेड्यूलर लॉक उपयोगकर्ता सत्रों को छोड़ देता है।",
|
||||
"proxmoxMarkMissing": "गायब अतिथियों को चिह्नित करें",
|
||||
"proxmoxMarkMissingDesc": "जब पहले से आयातित कोई अतिथि गायब हो जाए तो उसे हटाने के बजाय एक 'proxmox-missing' टैग जोड़ें।",
|
||||
"proxmoxLastSync": "अंतिम सिंक",
|
||||
"proxmoxLastSyncNever": "अभी तक सिंक नहीं हुआ",
|
||||
"proxmoxLastSyncNoResult": "कोई परिणाम विवरण नहीं",
|
||||
"proxmoxLastSyncSummary": "{{created}} बनाए गए, {{updated}} अपडेट किए गए, {{markedMissing}} गायब, {{skipped}} छोड़े गए",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "विफल",
|
||||
"pending": "लंबित",
|
||||
"success": "सफलता"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Proxmox अतिथियों की खोज करें और आयात करें",
|
||||
"proxmoxImportTitle": "Proxmox से आयात करें",
|
||||
"proxmoxSelectHost": "एक Proxmox होस्ट चुनें…",
|
||||
"proxmoxDiscover": "खोजें",
|
||||
"proxmoxDiscovering": "खोज रहा है…",
|
||||
"proxmoxDiscoverGuests": "अतिथियों की खोज करें",
|
||||
"proxmoxSelectAll": "सभी चुनें",
|
||||
"proxmoxDeselectAll": "सभी अचयनित करें",
|
||||
"proxmoxNoGuests": "इस Proxmox नोड पर कोई अतिथि नहीं मिला।",
|
||||
"proxmoxResultImported": "{{count}} आयातित",
|
||||
"proxmoxResultUpdated": "{{count}} अद्यतित",
|
||||
"proxmoxResultFailed": "{{count}} विफल",
|
||||
"proxmoxImportComplete": "Proxmox आयात पूर्ण: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "डिस्कवरी विफल",
|
||||
"proxmoxImportFailed": "आयात विफल",
|
||||
"docsLink": "दस्तावेज़ देखें",
|
||||
"hostNotFound": "होस्ट नहीं मिला",
|
||||
"loadingHost": "होस्ट लोड हो रहा है...",
|
||||
"none": "कोई नहीं",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "{{count}} होस्ट आयात करें",
|
||||
"proxmoxImportButton_other": "{{count}} होस्ट आयात करें"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "कनेक्टेड"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox statisztikák"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox statisztikák",
|
||||
"cpuUsage": "CPU-használat",
|
||||
"memoryUsage": "Memóriahasználat",
|
||||
"diskUsage": "Lemezhasználat",
|
||||
"networkInterfaces": "Hálózati interfészek",
|
||||
"uptime": "Üzemidő",
|
||||
"systemInfo": "Rendszerinformációk",
|
||||
"guestsSummary": "Vendégek",
|
||||
"storagePools": "Tárolómedencék",
|
||||
"clusterHealth": "Klaszter Egészségügy",
|
||||
"cpuCores_one": "{{count}} Mag",
|
||||
"cpuCores_other": "{{count}} Magok",
|
||||
"used": "Használt",
|
||||
"total": "Teljes",
|
||||
"hostname": "Gazdagépnév",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE verzió",
|
||||
"name": "Név",
|
||||
"status": "Állapot",
|
||||
"type": "Típus",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Korong",
|
||||
"running": "Futás",
|
||||
"stopped": "Leállítva",
|
||||
"guestCounts": "{{running}}/{{total}} fut",
|
||||
"inactive": "Inaktív",
|
||||
"notClustered": "Önálló csomópont (nem része fürtnek)",
|
||||
"quorate": "Kvórat",
|
||||
"notQuorate": "Nem határozatképes",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Nincs kiválasztva gazdagép",
|
||||
"pveshNotFound": "pvesh nem található - ez egy Proxmox csomópont?",
|
||||
"nodeDetectionFailed": "Nem sikerült érvényes Proxmox csomópontnevet meghatározni.",
|
||||
"connecting": "Kapcsolódás...",
|
||||
"connectionFailed": "Nem sikerült csatlakozni a Proxmox csomóponthoz",
|
||||
"refresh": "Frissítés",
|
||||
"searchGuests": "Vendégek keresése...",
|
||||
"filterAll": "Minden",
|
||||
"filterVm": "Virtuális gép",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Nem találhatók virtuális gépek vagy LXC konténerek ezen a csomóponton.",
|
||||
"noGuestsMatch": "Egyik vendég sem felel meg a jelenlegi szűrőknek."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Proxmox statisztikák engedélyezése",
|
||||
"enableProxmoxStatsDesc": "Proxmox Statisztika fül megjelenítése ehhez a gazdagéphez, élő csomópont-, vendég-, tároló- és fürtstatisztikákkal, SSH-n keresztül gyűjtött adatokkal.",
|
||||
"proxmoxStatsPollInterval": "Lekérdezési intervallum (másodperc)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 másodperc. A Proxmox csomópont statisztikáinak frissítési gyakorisága.",
|
||||
"proxmoxStatsNodeOverride": "Csomópont nevének felülbírálása",
|
||||
"proxmoxStatsNodeOverrideDesc": "A Proxmox csomópont nevének automatikus észlelésének felülbírálása (alapértelmezés szerint a gazdagépnév alapján észlelhető).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automatikusan felismerve a gazdagépnév alapján",
|
||||
"authTypeStepca": "CA lépés",
|
||||
"proxmoxIntegration": "Proxmox integráció",
|
||||
"enableProxmox": "Proxmox engedélyezése",
|
||||
"enableProxmoxDesc": "Jelölje meg ezt a gépet Proxmox csomópontként. Engedélyezi a vendégfelderítést és az importálást közvetlenül erről a gépről.",
|
||||
"proxmoxDefaultAuthType": "Alapértelmezett hitelesítési típus",
|
||||
"proxmoxDefaultAuthTypeDesc": "Hitelesítési módszer alkalmazva az importált vendéggazdagépekre. Válassza ki azt a hitelesítési típust, amelyik megfelel a vendéggazdagépekhez való csatlakozás módjának.",
|
||||
"authTypePassword": "Jelszó",
|
||||
"authTypeKey": "SSH-kulcs",
|
||||
"authTypeCredential": "Hitelesítő adat",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Egyik sem",
|
||||
"proxmoxDefaultCredential": "Alapértelmezett hitelesítő adat",
|
||||
"proxmoxDefaultCredentialDesc": "Az importált vendéggazdagépekhez használt hitelesítő adat, ha a hitelesítési típus Hitelesítő adatra van állítva.",
|
||||
"proxmoxWindowsDetection": "Windows/RDP-észlelés",
|
||||
"proxmoxWindowsDetectionDesc": "Vesszővel elválasztott névminták, amelyek RDP-t indítanak el SSH helyett (kis- és nagybetűket nem megkülönböztető)",
|
||||
"proxmoxDockerDetection": "Docker-észlelés",
|
||||
"proxmoxDockerDetectionDesc": "Vesszővel elválasztott névminták, amelyek lehetővé teszik a Docker számára a vendégek egyezését",
|
||||
"proxmoxPreferredRanges": "Előnyben részesített IP-tartományok",
|
||||
"proxmoxPreferredRangesDesc": "Vesszővel elválasztott előtagok prioritási sorrendben IP-cím kiválasztásához, ha egy vendégnek több interfésze van",
|
||||
"proxmoxAutoSync": "Vendégek automatikus szinkronizálása",
|
||||
"proxmoxAutoSyncDesc": "Rendszeresen fedezze fel ezt a Proxmox csomópontot, és hozzon létre vagy frissítsen importált vendéggazdagépeket, miközben a munkamenet fel van oldva.",
|
||||
"proxmoxSyncInterval": "Szinkronizálási időköz (perc)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 perc. Az ütemező kihagyja a zárolt felhasználói munkameneteket.",
|
||||
"proxmoxMarkMissing": "Jelöld meg az eltűnt vendégeket",
|
||||
"proxmoxMarkMissingDesc": "Hozzáad egy proxmox-missing címkét, amikor egy korábban importált vendég eltűnik, ahelyett, hogy törölné azt.",
|
||||
"proxmoxLastSync": "Utolsó szinkronizálás",
|
||||
"proxmoxLastSyncNever": "Még nincs szinkronizálva",
|
||||
"proxmoxLastSyncNoResult": "Nincsenek találati részletek",
|
||||
"proxmoxLastSyncSummary": "{{created}} létrehozva, {{updated}} frissítve, {{markedMissing}} hiányzik, {{skipped}} kihagyva",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Sikertelen",
|
||||
"pending": "Függőben levő",
|
||||
"success": "Siker"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Proxmox vendégek felfedezése és importálása",
|
||||
"proxmoxImportTitle": "Importálás a Proxmoxból",
|
||||
"proxmoxSelectHost": "Válasszon egy Proxmox hosztot…",
|
||||
"proxmoxDiscover": "Felfedezés",
|
||||
"proxmoxDiscovering": "Felfedezés…",
|
||||
"proxmoxDiscoverGuests": "Fedezze fel a vendégeket",
|
||||
"proxmoxSelectAll": "Összes kijelölése",
|
||||
"proxmoxDeselectAll": "Összes kijelölés törlése",
|
||||
"proxmoxNoGuests": "Nem találhatók vendégek ezen a Proxmox csomóponton.",
|
||||
"proxmoxResultImported": "{{count}} importálva",
|
||||
"proxmoxResultUpdated": "{{count}} frissítve",
|
||||
"proxmoxResultFailed": "{{count}} sikertelen",
|
||||
"proxmoxImportComplete": "Proxmox importálás kész: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "A felfedezés sikertelen",
|
||||
"proxmoxImportFailed": "Importálás sikertelen",
|
||||
"docsLink": "Dokumentumok megtekintése",
|
||||
"hostNotFound": "Nem található gazdagép",
|
||||
"loadingHost": "Gazdagép betöltése...",
|
||||
"none": "Egyik sem",
|
||||
"proxmoxGuestsSelected_one": "{{count}} vendég, {{selected}} kiválasztva",
|
||||
"proxmoxGuestsSelected_other": "{{count}} vendég, {{selected}} kiválasztva",
|
||||
"proxmoxImportButton_one": "Importálja a {{count}} gazdagépet",
|
||||
"proxmoxImportButton_other": "{{count}} hosztok importálása"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Csatlakoztatva"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Statistiche di Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Statistiche di Proxmox",
|
||||
"cpuUsage": "Utilizzo CPU",
|
||||
"memoryUsage": "Utilizzo memoria",
|
||||
"diskUsage": "Utilizzo disco",
|
||||
"networkInterfaces": "Interfacce di rete",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "Informazioni di sistema",
|
||||
"guestsSummary": "Ospiti",
|
||||
"storagePools": "Piscine di stoccaggio",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Core",
|
||||
"used": "Usato",
|
||||
"total": "Totale",
|
||||
"hostname": "Nome host",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "Versione PVE",
|
||||
"name": "Nome",
|
||||
"status": "Stato",
|
||||
"type": "Tipo",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disco",
|
||||
"running": "In esecuzione",
|
||||
"stopped": "Arrestato",
|
||||
"guestCounts": "{{running}}/{{total}} in esecuzione",
|
||||
"inactive": "Inattivo",
|
||||
"notClustered": "Nodo autonomo (non facente parte di un cluster)",
|
||||
"quorate": "Citazione",
|
||||
"notQuorate": "Non è stato raggiunto il quorum",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Nessun host selezionato",
|
||||
"pveshNotFound": "pvesh non trovato: si tratta di un nodo Proxmox?",
|
||||
"nodeDetectionFailed": "Impossibile determinare un nome nodo Proxmox valido",
|
||||
"connecting": "Connessione in corso...",
|
||||
"connectionFailed": "Impossibile connettersi al nodo Proxmox",
|
||||
"refresh": "Aggiorna",
|
||||
"searchGuests": "Cerca ospiti...",
|
||||
"filterAll": "Tutte",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Nessuna macchina virtuale o container LXC trovato su questo nodo.",
|
||||
"noGuestsMatch": "Nessun ospite corrisponde ai filtri attualmente attivi."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Abilita le statistiche di Proxmox",
|
||||
"enableProxmoxStatsDesc": "Mostra la scheda Statistiche di Proxmox per questo host con statistiche in tempo reale su nodi, guest, storage e cluster, raccolte tramite SSH.",
|
||||
"proxmoxStatsPollInterval": "Intervallo di polling (secondi)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimo 15 secondi. Frequenza di aggiornamento delle statistiche dei nodi Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Sovrascrittura del nome del nodo",
|
||||
"proxmoxStatsNodeOverrideDesc": "Ignora il rilevamento automatico del nome del nodo Proxmox (rilevato tramite nome host per impostazione predefinita).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Rilevato automaticamente dal nome host",
|
||||
"authTypeStepca": "Passo CA",
|
||||
"proxmoxIntegration": "Integrazione Proxmox",
|
||||
"enableProxmox": "Abilita Proxmox",
|
||||
"enableProxmoxDesc": "Contrassegna questo host come nodo Proxmox. Abilita il discovery e l'importazione di guest direttamente da questo host.",
|
||||
"proxmoxDefaultAuthType": "Tipo di autenticazione predefinito",
|
||||
"proxmoxDefaultAuthTypeDesc": "Metodo di autenticazione applicato agli host guest importati. Scegli il tipo di autenticazione corrispondente a come ti connetti ai tuoi guest.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "Chiave SSH",
|
||||
"authTypeCredential": "Credenziale",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Nessuno",
|
||||
"proxmoxDefaultCredential": "Credenziale predefinita",
|
||||
"proxmoxDefaultCredentialDesc": "Credenziale utilizzata per gli host guest importati quando il tipo di autenticazione è impostato su Credenziale.",
|
||||
"proxmoxWindowsDetection": "Rilevamento Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Elenco di pattern di nomi separati da virgole che attivano RDP invece di SSH (case‑insensitive)",
|
||||
"proxmoxDockerDetection": "Rilevamento Docker",
|
||||
"proxmoxDockerDetectionDesc": "Elenco di pattern di nomi separati da virgole che abilitano Docker per i guest corrispondenti",
|
||||
"proxmoxPreferredRanges": "Intervalli IP preferiti",
|
||||
"proxmoxPreferredRangesDesc": "Prefissi separati da virgole in ordine di priorità per la selezione dell'IP quando un guest ha più interfacce",
|
||||
"proxmoxAutoSync": "Sincronizzazione automatica dei guest",
|
||||
"proxmoxAutoSyncDesc": "Scopri periodicamente questo nodo Proxmox e crea o aggiorna gli host guest importati mentre la sessione è sbloccata.",
|
||||
"proxmoxSyncInterval": "Intervallo di sincronizzazione (minuti)",
|
||||
"proxmoxSyncIntervalDesc": "Minimo 5 minuti. La pianificazione salta le sessioni utente bloccate.",
|
||||
"proxmoxMarkMissing": "Segna i guest mancanti",
|
||||
"proxmoxMarkMissingDesc": "Aggiungi un tag proxmox-missing quando un guest precedentemente importato scompare invece di eliminarlo.",
|
||||
"proxmoxLastSync": "Ultima sincronizzazione",
|
||||
"proxmoxLastSyncNever": "Non ancora sincronizzato",
|
||||
"proxmoxLastSyncNoResult": "Nessun dettaglio del risultato",
|
||||
"proxmoxLastSyncSummary": "{{created}} creati, {{updated}} aggiornati, {{markedMissing}} mancanti, {{skipped}} saltati",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Fallita",
|
||||
"pending": "In attesa",
|
||||
"success": "Riuscita"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Scopri e importa guest Proxmox",
|
||||
"proxmoxImportTitle": "Importa da Proxmox",
|
||||
"proxmoxSelectHost": "Seleziona un host Proxmox…",
|
||||
"proxmoxDiscover": "Scopri",
|
||||
"proxmoxDiscovering": "Scoperta in corso…",
|
||||
"proxmoxDiscoverGuests": "Scopri guest",
|
||||
"proxmoxSelectAll": "Seleziona tutto",
|
||||
"proxmoxDeselectAll": "Deseleziona tutto",
|
||||
"proxmoxNoGuests": "Nessun guest trovato su questo nodo Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} importati",
|
||||
"proxmoxResultUpdated": "{{count}} aggiornati",
|
||||
"proxmoxResultFailed": "{{count}} falliti",
|
||||
"proxmoxImportComplete": "Importazione Proxmox completata: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Rilevamento non riuscito",
|
||||
"proxmoxImportFailed": "Importazione non riuscita",
|
||||
"docsLink": "Visualizza documentazione",
|
||||
"hostNotFound": "Host non trovato",
|
||||
"loadingHost": "Caricamento host...",
|
||||
"none": "Nessuna",
|
||||
"proxmoxGuestsSelected_one": "{{count}} ospite, {{selected}} selezionato",
|
||||
"proxmoxGuestsSelected_other": "{{count}} ospiti, {{selected}} selezionati",
|
||||
"proxmoxImportButton_one": "Importa {{count}} host",
|
||||
"proxmoxImportButton_other": "Importa {{count}} host"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connesso"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox統計情報"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox統計情報",
|
||||
"cpuUsage": "CPU使用率",
|
||||
"memoryUsage": "メモリ使用量",
|
||||
"diskUsage": "ディスク使用率",
|
||||
"networkInterfaces": "ネットワークインターフェース",
|
||||
"uptime": "アップタイム",
|
||||
"systemInfo": "システム情報",
|
||||
"guestsSummary": "ゲスト",
|
||||
"storagePools": "ストレージプール",
|
||||
"clusterHealth": "クラスターヘルス",
|
||||
"cpuCores_one": "{{count}} コア",
|
||||
"cpuCores_other": "{{count}} コア",
|
||||
"used": "使用量",
|
||||
"total": "合計",
|
||||
"hostname": "ホスト名",
|
||||
"kernel": "カーネル",
|
||||
"pveVersion": "PVEバージョン",
|
||||
"name": "名前",
|
||||
"status": "状態",
|
||||
"type": "タイプ",
|
||||
"cpu": "CPU",
|
||||
"mem": "メモリ",
|
||||
"disk": "ディスク",
|
||||
"running": "実行中",
|
||||
"stopped": "停止中",
|
||||
"guestCounts": "{{running}}/{{total}} 実行中",
|
||||
"inactive": "非アクティブ",
|
||||
"notClustered": "スタンドアロンノード(クラスタの一部ではない)",
|
||||
"quorate": "引用",
|
||||
"notQuorate": "定足数に満たない",
|
||||
"nodeOnline": "オンライン",
|
||||
"nodeOffline": "オフライン",
|
||||
"noHostSelected": "ホストが選択されていません",
|
||||
"pveshNotFound": "pveshが見つかりません - これはProxmoxノードですか?",
|
||||
"nodeDetectionFailed": "有効な Proxmox ノード名を特定できません",
|
||||
"connecting": "接続中...",
|
||||
"connectionFailed": "Proxmoxノードへの接続に失敗しました",
|
||||
"refresh": "再読み込み",
|
||||
"searchGuests": "ゲストを検索...",
|
||||
"filterAll": "すべて選択",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "このノード上にはVMまたはLXCコンテナは見つかりませんでした。",
|
||||
"noGuestsMatch": "現在の検索条件に一致するゲストはいません。"
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Proxmox Statsを有効にする",
|
||||
"enableProxmoxStatsDesc": "SSH経由で収集された、このホストのリアルタイムのノード、ゲスト、ストレージ、およびクラスタの統計情報を表示するProxmox統計タブを表示します。",
|
||||
"proxmoxStatsPollInterval": "ポーリング間隔(秒)",
|
||||
"proxmoxStatsPollIntervalDesc": "最低15秒。Proxmoxノードの統計情報を更新する頻度。",
|
||||
"proxmoxStatsNodeOverride": "ノード名の上書き",
|
||||
"proxmoxStatsNodeOverrideDesc": "Proxmoxノード名の自動検出(デフォルトではホスト名で検出)を上書きします。",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "ホスト名から自動検出",
|
||||
"authTypeStepca": "ステップCA",
|
||||
"proxmoxIntegration": "Proxmox統合",
|
||||
"enableProxmox": "Proxmoxを有効にする",
|
||||
"enableProxmoxDesc": "このホストをProxmoxノードとしてマークします。ゲストの検出とインポートをこのホストから直接行えるようにします。",
|
||||
"proxmoxDefaultAuthType": "デフォルト認証タイプ",
|
||||
"proxmoxDefaultAuthTypeDesc": "インポートされたゲストホストに適用される認証方式です。ゲストへの接続方法に合わせて認証タイプを選択してください。",
|
||||
"authTypePassword": "パスワード",
|
||||
"authTypeKey": "SSH鍵",
|
||||
"authTypeCredential": "クレデンシャル",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "なし",
|
||||
"proxmoxDefaultCredential": "デフォルト認証情報",
|
||||
"proxmoxDefaultCredentialDesc": "認証タイプが「Credential」に設定されている場合に、インポートされたゲストホストに使用される認証情報。",
|
||||
"proxmoxWindowsDetection": "Windows / RDP 検出",
|
||||
"proxmoxWindowsDetectionDesc": "SSH の代わりに RDP をトリガーする、カンマ区切りの名前パターン(大文字と小文字を区別しない)",
|
||||
"proxmoxDockerDetection": "Docker 検出",
|
||||
"proxmoxDockerDetectionDesc": "一致するゲストに対して Docker を有効にする、カンマ区切りの名前パターン",
|
||||
"proxmoxPreferredRanges": "優先 IP 範囲",
|
||||
"proxmoxPreferredRangesDesc": "ゲストが複数のインターフェースを持つ場合の IP 選択の優先順位で指定する、カンマ区切りのプレフィックス",
|
||||
"proxmoxAutoSync": "ゲストの自動同期",
|
||||
"proxmoxAutoSyncDesc": "セッションがロックされていないときに、この Proxmox ノードを定期的に検出し、インポートされたゲストホストを作成または更新します。",
|
||||
"proxmoxSyncInterval": "同期間隔(分)",
|
||||
"proxmoxSyncIntervalDesc": "最小 5 分。スケジューラーはロックされたユーザーセッションをスキップします。",
|
||||
"proxmoxMarkMissing": "欠落ゲストをマーク",
|
||||
"proxmoxMarkMissingDesc": "以前にインポートされたゲストが消えたときに、削除する代わりに proxmox-missing タグを追加します。",
|
||||
"proxmoxLastSync": "最終同期",
|
||||
"proxmoxLastSyncNever": "未同期",
|
||||
"proxmoxLastSyncNoResult": "結果の詳細なし",
|
||||
"proxmoxLastSyncSummary": "{{created}} 作成、{{updated}} 更新、{{markedMissing}} 欠落、{{skipped}} スキップ",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "失敗",
|
||||
"pending": "保留中",
|
||||
"success": "成功"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Proxmox ゲストの検出とインポート",
|
||||
"proxmoxImportTitle": "Proxmox からインポート",
|
||||
"proxmoxSelectHost": "Proxmox ホストを選択…",
|
||||
"proxmoxDiscover": "検出",
|
||||
"proxmoxDiscovering": "検出中…",
|
||||
"proxmoxDiscoverGuests": "ゲストを検出",
|
||||
"proxmoxSelectAll": "すべて選択",
|
||||
"proxmoxDeselectAll": "すべて選択解除",
|
||||
"proxmoxNoGuests": "この Proxmox ノードにゲストが見つかりません。",
|
||||
"proxmoxResultImported": "{{count}} インポート済み",
|
||||
"proxmoxResultUpdated": "{{count}} 更新済み",
|
||||
"proxmoxResultFailed": "{{count}} 失敗",
|
||||
"proxmoxImportComplete": "Proxmox インポート完了: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "検出に失敗しました",
|
||||
"proxmoxImportFailed": "インポートに失敗しました",
|
||||
"docsLink": "ドキュメントを表示",
|
||||
"hostNotFound": "ホストが見つかりません",
|
||||
"loadingHost": "ホストを読み込み中...",
|
||||
"none": "なし",
|
||||
"proxmoxGuestsSelected_one": "{{count}} ゲスト、 {{selected}} 選択済み",
|
||||
"proxmoxGuestsSelected_other": "{{count}} ゲスト、 {{selected}} 選択済み",
|
||||
"proxmoxImportButton_one": "{{count}} ホストをインポート",
|
||||
"proxmoxImportButton_other": "{{count}} 台のホストをインポート"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "接続中"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "프록스목스 통계"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "프록스목스 통계",
|
||||
"cpuUsage": "CPU 사용률",
|
||||
"memoryUsage": "메모리 사용률",
|
||||
"diskUsage": "디스크 사용률",
|
||||
"networkInterfaces": "네트워크 인터페이스",
|
||||
"uptime": "업타임",
|
||||
"systemInfo": "시스템 정보",
|
||||
"guestsSummary": "손님",
|
||||
"storagePools": "저장 풀",
|
||||
"clusterHealth": "클러스터 헬스",
|
||||
"cpuCores_one": "{{count}} 코어",
|
||||
"cpuCores_other": "{{count}} 코어",
|
||||
"used": "사용",
|
||||
"total": "총",
|
||||
"hostname": "호스트명",
|
||||
"kernel": "커널",
|
||||
"pveVersion": "PVE 버전",
|
||||
"name": "이름",
|
||||
"status": "상태",
|
||||
"type": "유형",
|
||||
"cpu": "CPU",
|
||||
"mem": "메모리",
|
||||
"disk": "디스크",
|
||||
"running": "실행 중",
|
||||
"stopped": "중지됨",
|
||||
"guestCounts": "{{running}}/{{total}} 실행 중",
|
||||
"inactive": "비활성",
|
||||
"notClustered": "독립형 노드(클러스터의 일부가 아님)",
|
||||
"quorate": "할당량",
|
||||
"notQuorate": "정족수 미달",
|
||||
"nodeOnline": "온라인",
|
||||
"nodeOffline": "오프라인",
|
||||
"noHostSelected": "선택된 호스트가 없습니다",
|
||||
"pveshNotFound": "pvesh를 찾을 수 없습니다. Proxmox 노드입니까?",
|
||||
"nodeDetectionFailed": "유효한 Proxmox 노드 이름을 확인할 수 없습니다.",
|
||||
"connecting": "연결 중...",
|
||||
"connectionFailed": "Proxmox 노드에 연결하지 못했습니다.",
|
||||
"refresh": "새로 고침",
|
||||
"searchGuests": "투숙객 검색...",
|
||||
"filterAll": "모두",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "이 노드에서 VM 또는 LXC 컨테이너를 찾을 수 없습니다.",
|
||||
"noGuestsMatch": "현재 필터 조건에 맞는 투숙객이 없습니다."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Proxmox 통계 활성화",
|
||||
"enableProxmoxStatsDesc": "SSH를 통해 수집된 실시간 노드, 게스트, 스토리지 및 클러스터 통계를 포함하는 이 호스트의 Proxmox 통계 탭을 표시합니다.",
|
||||
"proxmoxStatsPollInterval": "폴링 간격(초)",
|
||||
"proxmoxStatsPollIntervalDesc": "최소 15초. Proxmox 노드 통계를 새로 고치는 빈도.",
|
||||
"proxmoxStatsNodeOverride": "노드 이름 재정의",
|
||||
"proxmoxStatsNodeOverrideDesc": "Proxmox 노드 이름 자동 감지 기능을 재정의합니다(기본적으로 호스트 이름을 통해 감지됨).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "호스트 이름에서 자동 감지됨",
|
||||
"authTypeStepca": "스텝 CA",
|
||||
"proxmoxIntegration": "Proxmox 통합",
|
||||
"enableProxmox": "Proxmox 활성화",
|
||||
"enableProxmoxDesc": "이 호스트를 Proxmox 노드로 표시합니다. 이 호스트에서 직접 게스트를 검색하고 가져올 수 있도록 합니다.",
|
||||
"proxmoxDefaultAuthType": "기본 인증 유형",
|
||||
"proxmoxDefaultAuthTypeDesc": "가져온 게스트 호스트에 적용되는 인증 방법입니다. 게스트에 연결하는 방식에 맞는 인증 유형을 선택하세요.",
|
||||
"authTypePassword": "비밀번호",
|
||||
"authTypeKey": "SSH 키",
|
||||
"authTypeCredential": "자격 증명",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "없음",
|
||||
"proxmoxDefaultCredential": "기본 자격 증명",
|
||||
"proxmoxDefaultCredentialDesc": "인증 유형이 자격 증명으로 설정된 경우 가져온 게스트 호스트에 사용되는 자격 증명입니다.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP 감지",
|
||||
"proxmoxWindowsDetectionDesc": "SSH 대신 RDP를 트리거하는 쉼표로 구분된 이름 패턴(대소문자 구분 안 함)",
|
||||
"proxmoxDockerDetection": "Docker 감지",
|
||||
"proxmoxDockerDetectionDesc": "일치하는 게스트에 대해 Docker를 활성화하는 쉼표로 구분된 이름 패턴",
|
||||
"proxmoxPreferredRanges": "선호하는 IP 범위",
|
||||
"proxmoxPreferredRangesDesc": "게스트에 여러 인터페이스가 있을 때 IP 선택을 위한 우선 순위 순서의 쉼표로 구분된 접두사",
|
||||
"proxmoxAutoSync": "게스트 자동 동기화",
|
||||
"proxmoxAutoSyncDesc": "세션이 잠금 해제되어 있을 때 주기적으로 이 Proxmox 노드를 검색하고 가져온 게스트 호스트를 생성하거나 업데이트합니다.",
|
||||
"proxmoxSyncInterval": "동기화 간격(분)",
|
||||
"proxmoxSyncIntervalDesc": "최소 5분. 스케줄러는 잠긴 사용자 세션을 건너뜁니다.",
|
||||
"proxmoxMarkMissing": "누락된 게스트 표시",
|
||||
"proxmoxMarkMissingDesc": "이전에 가져온 게스트가 사라지면 삭제 대신 proxmox-missing 태그를 추가합니다.",
|
||||
"proxmoxLastSync": "마지막 동기화",
|
||||
"proxmoxLastSyncNever": "아직 동기화되지 않음",
|
||||
"proxmoxLastSyncNoResult": "결과 세부 정보 없음",
|
||||
"proxmoxLastSyncSummary": "생성 {{created}}개, 업데이트 {{updated}}개, 누락 {{markedMissing}}개, 건너뜀 {{skipped}}개",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "실패",
|
||||
"pending": "보류 중",
|
||||
"success": "성공"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Proxmox 게스트 검색 및 가져오기",
|
||||
"proxmoxImportTitle": "Proxmox에서 가져오기",
|
||||
"proxmoxSelectHost": "Proxmox 호스트 선택…",
|
||||
"proxmoxDiscover": "검색",
|
||||
"proxmoxDiscovering": "검색 중…",
|
||||
"proxmoxDiscoverGuests": "게스트 검색",
|
||||
"proxmoxSelectAll": "전체 선택",
|
||||
"proxmoxDeselectAll": "전체 선택 취소",
|
||||
"proxmoxNoGuests": "이 Proxmox 노드에서 게스트를 찾을 수 없습니다.",
|
||||
"proxmoxResultImported": "{{count}}개 가져옴",
|
||||
"proxmoxResultUpdated": "{{count}}개 업데이트됨",
|
||||
"proxmoxResultFailed": "{{count}}개 실패",
|
||||
"proxmoxImportComplete": "Proxmox 가져오기 완료: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "검색 실패",
|
||||
"proxmoxImportFailed": "가져오기 실패",
|
||||
"docsLink": "문서 보기",
|
||||
"hostNotFound": "호스트를 찾을 수 없습니다",
|
||||
"loadingHost": "호스트 로드 중...",
|
||||
"none": "없음",
|
||||
"proxmoxGuestsSelected_one": "{{count}} 게스트, {{selected}} 선택됨",
|
||||
"proxmoxGuestsSelected_other": "{{count}} 명의 손님, {{selected}} 명이 선택되었습니다.",
|
||||
"proxmoxImportButton_one": "호스트 {{count}}개 가져오기",
|
||||
"proxmoxImportButton_other": "{{count}}개 호스트 가져오기"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "연결됨"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox-statistieken"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox-statistieken",
|
||||
"cpuUsage": "CPU-gebruik",
|
||||
"memoryUsage": "Geheugengebruik",
|
||||
"diskUsage": "Schijfgebruik",
|
||||
"networkInterfaces": "Netwerkinterfaces",
|
||||
"uptime": "Beschikbaarheid",
|
||||
"systemInfo": "Systeeminformatie",
|
||||
"guestsSummary": "Gasten",
|
||||
"storagePools": "Opslagbassins",
|
||||
"clusterHealth": "Clustergezondheid",
|
||||
"cpuCores_one": "{{count}} Kern",
|
||||
"cpuCores_other": "{{count}} Kernen",
|
||||
"used": "Gebruikt",
|
||||
"total": "Totaal",
|
||||
"hostname": "Hostnaam",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE-versie",
|
||||
"name": "Naam",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Schijf",
|
||||
"running": "Rennen",
|
||||
"stopped": "Gestopt",
|
||||
"guestCounts": "{{running}}/{{total}} rennend",
|
||||
"inactive": "Inactief",
|
||||
"notClustered": "Zelfstandig knooppunt (geen onderdeel van een cluster)",
|
||||
"quorate": "Quoraat",
|
||||
"notQuorate": "Niet quorum",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Geen host geselecteerd",
|
||||
"pveshNotFound": "pvesh niet gevonden - is dit een Proxmox-node?",
|
||||
"nodeDetectionFailed": "Kan geen geldige Proxmox-knooppuntnaam bepalen",
|
||||
"connecting": "Verbinden...",
|
||||
"connectionFailed": "Verbinding met Proxmox-node mislukt.",
|
||||
"refresh": "Vernieuwen",
|
||||
"searchGuests": "Gasten zoeken...",
|
||||
"filterAll": "Alle",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Er zijn geen VM's of LXC-containers gevonden op dit knooppunt.",
|
||||
"noGuestsMatch": "Er zijn geen gasten die aan de huidige filters voldoen."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Proxmox-statistieken inschakelen",
|
||||
"enableProxmoxStatsDesc": "Toon een tabblad Proxmox-statistieken voor deze host met live statistieken van knooppunten, gastsystemen, opslag en het cluster, verzameld via SSH.",
|
||||
"proxmoxStatsPollInterval": "Pollinterval (seconden)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimaal 15 seconden. Hoe vaak moeten de Proxmox-node-statistieken worden vernieuwd?",
|
||||
"proxmoxStatsNodeOverride": "Knooppuntnaam overschrijven",
|
||||
"proxmoxStatsNodeOverrideDesc": "Schakel de automatische detectie van de Proxmox-knooppuntnaam uit (deze wordt standaard gedetecteerd via de hostnaam).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automatisch gedetecteerd op basis van hostnaam",
|
||||
"authTypeStepca": "Stap CA",
|
||||
"proxmoxIntegration": "Proxmox-integratie",
|
||||
"enableProxmox": "Proxmox inschakelen",
|
||||
"enableProxmoxDesc": "Markeer deze host als een Proxmox-node. Dit maakt het mogelijk om virtuele machines direct vanaf deze host te detecteren en te importeren.",
|
||||
"proxmoxDefaultAuthType": "Standaard authenticatietype",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authenticatiemethode toegepast op geïmporteerde gasthosts. Kies het authenticatietype dat overeenkomt met de manier waarop u verbinding maakt met uw gasten.",
|
||||
"authTypePassword": "Wachtwoord",
|
||||
"authTypeKey": "SSH-sleutel",
|
||||
"authTypeCredential": "Referentie",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Geen",
|
||||
"proxmoxDefaultCredential": "Standaard inloggegevens",
|
||||
"proxmoxDefaultCredentialDesc": "De inloggegevens die worden gebruikt voor geïmporteerde gasthosts wanneer het authenticatietype is ingesteld op 'Inloggegevens'.",
|
||||
"proxmoxWindowsDetection": "Windows-/RDP-detectie",
|
||||
"proxmoxWindowsDetectionDesc": "Door komma's gescheiden naampatronen die RDP in plaats van SSH activeren (hoofdlettergevoeligheid speelt geen rol).",
|
||||
"proxmoxDockerDetection": "Docker-detectie",
|
||||
"proxmoxDockerDetectionDesc": "Door komma's gescheiden naampatronen waarmee Docker gastsystemen kan herkennen.",
|
||||
"proxmoxPreferredRanges": "Voorkeurs-IP-bereiken",
|
||||
"proxmoxPreferredRangesDesc": "Door komma's gescheiden voorvoegsels in prioriteitsvolgorde voor IP-selectie wanneer een gast meerdere interfaces heeft.",
|
||||
"proxmoxAutoSync": "Automatische synchronisatie van gasten",
|
||||
"proxmoxAutoSyncDesc": "Ontdek periodiek dit Proxmox-knooppunt en maak of update geïmporteerde gasthosts terwijl uw sessie ontgrendeld is.",
|
||||
"proxmoxSyncInterval": "Synchronisatie-interval (minuten)",
|
||||
"proxmoxSyncIntervalDesc": "Minimaal 5 minuten. De planner slaat vergrendelde gebruikerssessies over.",
|
||||
"proxmoxMarkMissing": "Markeer ontbrekende gasten",
|
||||
"proxmoxMarkMissingDesc": "Voeg een 'proxmox-missing'-tag toe wanneer een eerder geïmporteerde gast verdwijnt, in plaats van deze te verwijderen.",
|
||||
"proxmoxLastSync": "Laatste synchronisatie",
|
||||
"proxmoxLastSyncNever": "Nog niet gesynchroniseerd",
|
||||
"proxmoxLastSyncNoResult": "Geen resultaatdetails",
|
||||
"proxmoxLastSyncSummary": "{{created}} aangemaakt, {{updated}} bijgewerkt, {{markedMissing}} ontbreekt, {{skipped}} overgeslagen",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Mislukt",
|
||||
"pending": "In behandeling",
|
||||
"success": "Succes"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Ontdek en importeer Proxmox-gasten",
|
||||
"proxmoxImportTitle": "Importeren vanuit Proxmox",
|
||||
"proxmoxSelectHost": "Selecteer een Proxmox-host…",
|
||||
"proxmoxDiscover": "Ontdekken",
|
||||
"proxmoxDiscovering": "Ontdekken…",
|
||||
"proxmoxDiscoverGuests": "Ontdek gasten",
|
||||
"proxmoxSelectAll": "Alles selecteren",
|
||||
"proxmoxDeselectAll": "Alles deselecteren",
|
||||
"proxmoxNoGuests": "Er zijn geen gasten gevonden op dit Proxmox-knooppunt.",
|
||||
"proxmoxResultImported": "{{count}} geïmporteerd",
|
||||
"proxmoxResultUpdated": "{{count}} bijgewerkt",
|
||||
"proxmoxResultFailed": "{{count}} mislukt",
|
||||
"proxmoxImportComplete": "Proxmox-import voltooid: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Ontdekking mislukt",
|
||||
"proxmoxImportFailed": "Importeren mislukt",
|
||||
"docsLink": "Documenten bekijken",
|
||||
"hostNotFound": "Host niet gevonden",
|
||||
"loadingHost": "Host wordt geladen...",
|
||||
"none": "Geen",
|
||||
"proxmoxGuestsSelected_one": "{{count}} gast, {{selected}} geselecteerd",
|
||||
"proxmoxGuestsSelected_other": "{{count}} gasten, {{selected}} geselecteerd",
|
||||
"proxmoxImportButton_one": "Importeer {{count}} host",
|
||||
"proxmoxImportButton_other": "Importeer {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Aangesloten"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU-bruk",
|
||||
"memoryUsage": "Minnebruk",
|
||||
"diskUsage": "Diskbruk",
|
||||
"networkInterfaces": "Nettverksgrensesnitt",
|
||||
"uptime": "Oppetid",
|
||||
"systemInfo": "Systeminformasjon",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Brukt",
|
||||
"total": "Totalt",
|
||||
"hostname": "Vertsnavn",
|
||||
"kernel": "Kjerne",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Navn",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Kjører",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inaktiv",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Tilkoblet",
|
||||
"nodeOffline": "Frakoblet",
|
||||
"noHostSelected": "Ingen vert valgt",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Kobler til...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Oppdater",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Passord",
|
||||
"authTypeKey": "SSH-nøkkel",
|
||||
"authTypeCredential": "Legitimasjon",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Ingen",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Mislyktes",
|
||||
"pending": "Pending",
|
||||
"success": "Suksess"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} mislyktes",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import mislyktes",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "Ingen",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Tilkoblet"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Statystyki Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Statystyki Proxmox",
|
||||
"cpuUsage": "Wykorzystanie procesora",
|
||||
"memoryUsage": "Wykorzystanie pamięci",
|
||||
"diskUsage": "Wykorzystanie dysku",
|
||||
"networkInterfaces": "Interfejsy sieciowe",
|
||||
"uptime": "Czas działania",
|
||||
"systemInfo": "Informacje o systemie",
|
||||
"guestsSummary": "Goście",
|
||||
"storagePools": "Pule magazynowe",
|
||||
"clusterHealth": "Kondycja klastra",
|
||||
"cpuCores_one": "{{count}} rdzeń",
|
||||
"cpuCores_other": "{{count}} rdzeni",
|
||||
"used": "Użyte",
|
||||
"total": "Łącznie",
|
||||
"hostname": "Nazwa hosta",
|
||||
"kernel": "Jądro",
|
||||
"pveVersion": "Wersja PVE",
|
||||
"name": "Nazwa",
|
||||
"status": "Status",
|
||||
"type": "Typ",
|
||||
"cpu": "Procesor",
|
||||
"mem": "Pamięć",
|
||||
"disk": "Dysk",
|
||||
"running": "Działa",
|
||||
"stopped": "Zatrzymany",
|
||||
"guestCounts": "{{running}}/{{total}} działa",
|
||||
"inactive": "Nieaktywny",
|
||||
"notClustered": "Samodzielny węzeł (nie jest częścią klastra)",
|
||||
"quorate": "Kworum osiągnięte",
|
||||
"notQuorate": "Brak kworum",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Nie wybrano hosta",
|
||||
"pveshNotFound": "Nie znaleziono pvesh. Czy to węzeł Proxmox?",
|
||||
"nodeDetectionFailed": "Nie można ustalić prawidłowej nazwy węzła Proxmox",
|
||||
"connecting": "Łączenie...",
|
||||
"connectionFailed": "Nie udało się połączyć z węzłem Proxmox",
|
||||
"refresh": "Odśwież",
|
||||
"searchGuests": "Wyszukaj gości...",
|
||||
"filterAll": "Wszystkie",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Nie znaleziono maszyn wirtualnych ani kontenerów LXC na tym węźle.",
|
||||
"noGuestsMatch": "Żaden gość nie pasuje do bieżących filtrów."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Włącz statystyki Proxmox",
|
||||
"enableProxmoxStatsDesc": "Pokaż kartę statystyk Proxmox dla tego hosta z bieżącymi statystykami węzła, gości, magazynu i klastra zbieranymi przez SSH.",
|
||||
"proxmoxStatsPollInterval": "Interwał sondowania (sekundy)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 sekund. Jak często odświeżać statystyki węzła Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Nadpisanie nazwy węzła",
|
||||
"proxmoxStatsNodeOverrideDesc": "Zastąp automatyczne wykrywanie nazwy węzła Proxmox (domyślnie wykrywane za pomocą nazwy hosta).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Automatycznie wykryto na podstawie nazwy hosta",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Integracja Proxmox",
|
||||
"enableProxmox": "Włącz Proxmox",
|
||||
"enableProxmoxDesc": "Oznacz ten host jako węzeł Proxmox. Umożliwia wykrywanie gości i importowanie bezpośrednio z tego hosta.",
|
||||
"proxmoxDefaultAuthType": "Domyślny typ uwierzytelniania",
|
||||
"proxmoxDefaultAuthTypeDesc": "Metoda uwierzytelniania stosowana do importowanych hostów gościnnych. Wybierz typ uwierzytelniania odpowiadający sposobowi, w jaki łączysz się z gośćmi.",
|
||||
"authTypePassword": "Hasło",
|
||||
"authTypeKey": "Klucz SSH",
|
||||
"authTypeCredential": "Poświadczenie",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Brak",
|
||||
"proxmoxDefaultCredential": "Domyślne poświadczenie",
|
||||
"proxmoxDefaultCredentialDesc": "Poświadczenie używane dla importowanych hostów gości, gdy typ uwierzytelniania to Poświadczenie.",
|
||||
"proxmoxWindowsDetection": "Wykrywanie systemu Windows/RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Wzorce nazw rozdzielone przecinkami, które wyzwalają protokół RDP zamiast protokołu SSH (bez uwzględniania wielkości liter)",
|
||||
"proxmoxDockerDetection": "Wykrywanie Dockera",
|
||||
"proxmoxDockerDetectionDesc": "Wzorce nazw rozdzielone przecinkami, które włączają Docker dla pasujących gości",
|
||||
"proxmoxPreferredRanges": "Preferowane zakresy adresów IP",
|
||||
"proxmoxPreferredRangesDesc": "Prefiksy rozdzielone przecinkami w kolejności priorytetów wyboru adresu IP, gdy gość ma wiele interfejsów",
|
||||
"proxmoxAutoSync": "Automatyczna synchronizacja gości",
|
||||
"proxmoxAutoSyncDesc": "Okresowo wykrywaj ten węzeł Proxmox i twórz lub aktualizuj importowane hosty gości, dopóki Twoja sesja jest odblokowana.",
|
||||
"proxmoxSyncInterval": "Interwał synchronizacji (minuty)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minut. Harmonogram pomija zablokowane sesje użytkowników.",
|
||||
"proxmoxMarkMissing": "Oznacz brakujących gości",
|
||||
"proxmoxMarkMissingDesc": "Dodaj tag proxmox-missing, gdy wcześniej zaimportowany gość zniknie, zamiast go usuwać.",
|
||||
"proxmoxLastSync": "Ostatnia synchronizacja",
|
||||
"proxmoxLastSyncNever": "Jeszcze nie zsynchronizowano",
|
||||
"proxmoxLastSyncNoResult": "Brak szczegółów wyników",
|
||||
"proxmoxLastSyncSummary": "utworzono {{created}}, zaktualizowano {{updated}}, brakujących {{markedMissing}}, pominięto {{skipped}}",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Niepowodzenie",
|
||||
"pending": "Oczekuje",
|
||||
"success": "Powodzenie"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Wykryj i zaimportuj gości Proxmox",
|
||||
"proxmoxImportTitle": "Import z Proxmox",
|
||||
"proxmoxSelectHost": "Wybierz hosta Proxmox…",
|
||||
"proxmoxDiscover": "Wykryj",
|
||||
"proxmoxDiscovering": "Wykrywanie…",
|
||||
"proxmoxDiscoverGuests": "Wykryj gości",
|
||||
"proxmoxSelectAll": "Zaznacz wszystko",
|
||||
"proxmoxDeselectAll": "Odznacz wszystko",
|
||||
"proxmoxNoGuests": "Nie znaleziono gości na tym węźle Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} zaimportowano",
|
||||
"proxmoxResultUpdated": "{{count}} zaktualizowano",
|
||||
"proxmoxResultFailed": "{{count}} nie powiodło się",
|
||||
"proxmoxImportComplete": "Zakończono importowanie Proxmox: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Wykrywanie nie powiodło się",
|
||||
"proxmoxImportFailed": "Import nie powiódł się",
|
||||
"docsLink": "Zobacz dokumentację",
|
||||
"hostNotFound": "Nie znaleziono hosta",
|
||||
"loadingHost": "Ładowanie hosta...",
|
||||
"none": "Brak",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Importuj {{count}} host",
|
||||
"proxmoxImportButton_other": "Importuj {{count}} hostów"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Połączono"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Tipo",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Conectando...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Atualizar",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Senha",
|
||||
"authTypeKey": "Chave SSH",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Nenhum",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "Documentação",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "Nenhum",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "Utilização da CPU",
|
||||
"memoryUsage": "Utilização da memória",
|
||||
"diskUsage": "Utilização do disco",
|
||||
"networkInterfaces": "Interfaces de Rede",
|
||||
"uptime": "Tempo de Atividade",
|
||||
"systemInfo": "Informação do Sistema",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Núcleo",
|
||||
"cpuCores_other": "{{count}} Núcleos",
|
||||
"used": "Utilizado",
|
||||
"total": "Total",
|
||||
"hostname": "Nome do host",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Nome",
|
||||
"status": "Estado",
|
||||
"type": "Tipo",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disco",
|
||||
"running": "Em execução",
|
||||
"stopped": "Parado",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inativo",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Nenhum anfitrião selecionado",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "A ligar...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Atualizar",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "Todas",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Integração com Proxmox",
|
||||
"enableProxmox": "Ativar Proxmox",
|
||||
"enableProxmoxDesc": "Marcar este host como um nó Proxmox. Permite a descoberta e importação de convidados diretamente a partir deste host.",
|
||||
"proxmoxDefaultAuthType": "Tipo de Autenticação Padrão",
|
||||
"proxmoxDefaultAuthTypeDesc": "Método de autenticação aplicado aos hosts convidados importados. Escolha o tipo de autenticação que corresponde à forma como se liga aos seus convidados.",
|
||||
"authTypePassword": "Palavra-passe",
|
||||
"authTypeKey": "Chave SSH",
|
||||
"authTypeCredential": "Credencial",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Nenhum",
|
||||
"proxmoxDefaultCredential": "Credencial padrão",
|
||||
"proxmoxDefaultCredentialDesc": "Credencial usada para os convidados importados quando o tipo de autenticação é definido como Credencial.",
|
||||
"proxmoxWindowsDetection": "Deteção de Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Padrões de nomes separados por vírgulas que acionam RDP em vez de SSH (sem distinção entre maiúsculas/minúsculas)",
|
||||
"proxmoxDockerDetection": "Deteção de Docker",
|
||||
"proxmoxDockerDetectionDesc": "Padrões de nomes separados por vírgulas que ativam o Docker nos convidados que correspondam.",
|
||||
"proxmoxPreferredRanges": "Intervalos de IP preferenciais",
|
||||
"proxmoxPreferredRangesDesc": "Prefixos separados por vírgulas em ordem de prioridade para seleção de IP quando um convidado tem várias interfaces.",
|
||||
"proxmoxAutoSync": "Sincronização automática",
|
||||
"proxmoxAutoSyncDesc": "Descobrir periodicamente este nó Proxmox e criar ou atualizar os convidados importados enquanto a sessão estiver desbloqueada.",
|
||||
"proxmoxSyncInterval": "Intervalo de sincronização (minutos)",
|
||||
"proxmoxSyncIntervalDesc": "Mínimo de 5 minutos. O agendador ignora sessões de utilizador bloqueadas.",
|
||||
"proxmoxMarkMissing": "Marcar convidados em falta",
|
||||
"proxmoxMarkMissingDesc": "Adicionar a etiqueta proxmox-missing quando um convidado previamente importado desaparecer, em vez de o eliminar.",
|
||||
"proxmoxLastSync": "Última sincronização",
|
||||
"proxmoxLastSyncNever": "Ainda não sincronizado",
|
||||
"proxmoxLastSyncNoResult": "Sem detalhes do resultado",
|
||||
"proxmoxLastSyncSummary": "{{created}} criados, {{updated}} atualizados, {{markedMissing}} em falta, {{skipped}} ignorados",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Falhou",
|
||||
"pending": "Pendente",
|
||||
"success": "Sucesso"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Descobrir e importar convidados",
|
||||
"proxmoxImportTitle": "Importar do Proxmox",
|
||||
"proxmoxSelectHost": "Selecionar um host Proxmox…",
|
||||
"proxmoxDiscover": "Descobrir",
|
||||
"proxmoxDiscovering": "A descobrir…",
|
||||
"proxmoxDiscoverGuests": "Descobrir convidados",
|
||||
"proxmoxSelectAll": "Selecionar todos",
|
||||
"proxmoxDeselectAll": "Desmarcar todos",
|
||||
"proxmoxNoGuests": "Nenhum convidado encontrado neste nó Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} importados",
|
||||
"proxmoxResultUpdated": "{{count}} atualizados",
|
||||
"proxmoxResultFailed": "{{count}} falhados",
|
||||
"proxmoxImportComplete": "Importação Proxmox concluída: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Descoberta falhou",
|
||||
"proxmoxImportFailed": "Importação falhou",
|
||||
"docsLink": "Ver documentação",
|
||||
"hostNotFound": "Host não encontrado",
|
||||
"loadingHost": "A carregar o host...",
|
||||
"none": "Nenhum",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Importar {{count}} host",
|
||||
"proxmoxImportButton_other": "Importar {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Ligado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Statistici Proxmox"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Statistici Proxmox",
|
||||
"cpuUsage": "Utilizarea procesorului",
|
||||
"memoryUsage": "Utilizarea memoriei",
|
||||
"diskUsage": "Utilizarea discului",
|
||||
"networkInterfaces": "Interfețe de rețea",
|
||||
"uptime": "Timp de funcționare",
|
||||
"systemInfo": "Informații despre sistem",
|
||||
"guestsSummary": "Oaspeți",
|
||||
"storagePools": "Bazine de stocare",
|
||||
"clusterHealth": "Sănătatea clusterului",
|
||||
"cpuCores_one": "{{count}} Nucleu",
|
||||
"cpuCores_other": "{{count}} Nuclee",
|
||||
"used": "Folosit",
|
||||
"total": "Total",
|
||||
"hostname": "Nume gazdă",
|
||||
"kernel": "Nucleu",
|
||||
"pveVersion": "Versiunea PVE",
|
||||
"name": "Nume",
|
||||
"status": "Stare",
|
||||
"type": "Tip",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disc",
|
||||
"running": "Funcţionare",
|
||||
"stopped": "Oprit",
|
||||
"guestCounts": "{{running}}/{{total}} rulează",
|
||||
"inactive": "Inactiv",
|
||||
"notClustered": "Nod independent (nu face parte dintr-un cluster)",
|
||||
"quorate": "Cvoat",
|
||||
"notQuorate": "Nu există cvorum",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "Nicio gazdă selectată",
|
||||
"pveshNotFound": "pvesh nu a fost găsit - este acesta un nod Proxmox?",
|
||||
"nodeDetectionFailed": "Nu se poate determina un nume de nod Proxmox valid",
|
||||
"connecting": "Se conectează...",
|
||||
"connectionFailed": "Conectarea la nodul Proxmox nu a reușit.",
|
||||
"refresh": "Reîmprospăta",
|
||||
"searchGuests": "Caută invitați...",
|
||||
"filterAll": "Toate",
|
||||
"filterVm": "Mașină virtuală",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "Nu s-au găsit mașini virtuale sau containere LXC pe acest nod.",
|
||||
"noGuestsMatch": "Niciun invitat nu corespunde filtrelor actuale."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Activează statisticile Proxmox",
|
||||
"enableProxmoxStatsDesc": "Afișează o filă Statistici Proxmox pentru această gazdă cu statistici live despre noduri, invitați, stocare și cluster, colectate prin SSH.",
|
||||
"proxmoxStatsPollInterval": "Interval de sondaj (secunde)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minim 15 secunde. Cât de des se actualizează statisticile nodului Proxmox.",
|
||||
"proxmoxStatsNodeOverride": "Suprascriere nume nod",
|
||||
"proxmoxStatsNodeOverrideDesc": "Suprascrie detectarea automată a numelui nodului Proxmox (detectat implicit prin numele de gazdă).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Detectat automat de la numele gazdei",
|
||||
"authTypeStepca": "Pasul CA",
|
||||
"proxmoxIntegration": "Integrare Proxmox",
|
||||
"enableProxmox": "Activează Proxmox",
|
||||
"enableProxmoxDesc": "Marchează această gazdă ca nod Proxmox. Activează descoperirea și importul de informații de către oaspeți direct de pe această gazdă.",
|
||||
"proxmoxDefaultAuthType": "Tip de autentificare implicit",
|
||||
"proxmoxDefaultAuthTypeDesc": "Metodă de autentificare aplicată gazdelor invitate importate. Alegeți tipul de autentificare care corespunde modului în care vă conectați la invitați.",
|
||||
"authTypePassword": "Parolă",
|
||||
"authTypeKey": "Cheie SSH",
|
||||
"authTypeCredential": "Acreditare",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Nici unul",
|
||||
"proxmoxDefaultCredential": "Acreditare implicită",
|
||||
"proxmoxDefaultCredentialDesc": "Acreditare utilizată pentru gazdele invitate importate atunci când tipul de autentificare este setat la Acreditare.",
|
||||
"proxmoxWindowsDetection": "Detectare Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Modele de nume separate prin virgulă care declanșează RDP în loc de SSH (fără sensibilitate la majuscule/minuscule)",
|
||||
"proxmoxDockerDetection": "Detectarea Docker",
|
||||
"proxmoxDockerDetectionDesc": "Modele de nume separate prin virgulă care activează Docker pentru invitații potriviți",
|
||||
"proxmoxPreferredRanges": "Intervalele IP preferate",
|
||||
"proxmoxPreferredRangesDesc": "Prefixe separate prin virgulă în ordinea priorității pentru selectarea adresei IP atunci când un oaspete are mai multe interfețe",
|
||||
"proxmoxAutoSync": "Sincronizare automată a invitaților",
|
||||
"proxmoxAutoSyncDesc": "Descoperiți periodic acest nod Proxmox și creați sau actualizați gazdele invitate importate în timp ce sesiunea este deblocată.",
|
||||
"proxmoxSyncInterval": "Interval de sincronizare (minute)",
|
||||
"proxmoxSyncIntervalDesc": "Minim 5 minute. Planificatorul omite sesiunile utilizatorilor blocate.",
|
||||
"proxmoxMarkMissing": "Marchează oaspeții lipsă",
|
||||
"proxmoxMarkMissingDesc": "Adăugați o etichetă proxmox-missing atunci când un invitat importat anterior dispare, în loc să îl ștergeți.",
|
||||
"proxmoxLastSync": "Ultima sincronizare",
|
||||
"proxmoxLastSyncNever": "Încă nesincronizat",
|
||||
"proxmoxLastSyncNoResult": "Niciun detaliu al rezultatului",
|
||||
"proxmoxLastSyncSummary": "{{created}} creat, {{updated}} actualizat, {{markedMissing}} lipsă, {{skipped}} omis",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Eșuat",
|
||||
"pending": "În așteptare",
|
||||
"success": "Succes"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Descoperiți și importați invitați Proxmox",
|
||||
"proxmoxImportTitle": "Import din Proxmox",
|
||||
"proxmoxSelectHost": "Selectați o gazdă Proxmox…",
|
||||
"proxmoxDiscover": "Descoperi",
|
||||
"proxmoxDiscovering": "Descoperind…",
|
||||
"proxmoxDiscoverGuests": "Descoperă oaspeți",
|
||||
"proxmoxSelectAll": "Selectați tot",
|
||||
"proxmoxDeselectAll": "Deselectați tot",
|
||||
"proxmoxNoGuests": "Nu au fost găsiți invitați pe acest nod Proxmox.",
|
||||
"proxmoxResultImported": "{{count}} importat",
|
||||
"proxmoxResultUpdated": "{{count}} actualizat",
|
||||
"proxmoxResultFailed": "{{count}} a eșuat",
|
||||
"proxmoxImportComplete": "Import Proxmox finalizat: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Descoperirea a eșuat",
|
||||
"proxmoxImportFailed": "Importul a eșuat",
|
||||
"docsLink": "Vizualizați documentele",
|
||||
"hostNotFound": "Gazdă negăsită",
|
||||
"loadingHost": "Se încarcă gazda...",
|
||||
"none": "Nici unul",
|
||||
"proxmoxGuestsSelected_one": "{{count}} invitat, {{selected}} selectat",
|
||||
"proxmoxGuestsSelected_other": "{{count}} invitați, {{selected}} selectați",
|
||||
"proxmoxImportButton_one": "Importați gazda {{count}}",
|
||||
"proxmoxImportButton_other": "Importați gazde {{count}}"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Conectat"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "Использование ЦП",
|
||||
"memoryUsage": "Использование памяти",
|
||||
"diskUsage": "Использование диска",
|
||||
"networkInterfaces": "Сетевые интерфейсы",
|
||||
"uptime": "Время работы",
|
||||
"systemInfo": "Информация о системе",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} ядро",
|
||||
"cpuCores_other": "{{count}} ядер",
|
||||
"used": "Занято",
|
||||
"total": "Total",
|
||||
"hostname": "Имя хоста",
|
||||
"kernel": "Ядро",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Название",
|
||||
"status": "Статус",
|
||||
"type": "Тип",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Диск",
|
||||
"running": "Запущен",
|
||||
"stopped": "Остановлен",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Неактивен",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Онлайн",
|
||||
"nodeOffline": "Офлайн",
|
||||
"noHostSelected": "Хост не выбран",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Подключение...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Обновить",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "Все",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Интеграция Proxmox",
|
||||
"enableProxmox": "Включить Proxmox",
|
||||
"enableProxmoxDesc": "Пометить этот хост как узел Proxmox. Включает обнаружение и импорт гостевых систем непосредственно с этого хоста.",
|
||||
"proxmoxDefaultAuthType": "Тип аутентификации по умолчанию",
|
||||
"proxmoxDefaultAuthTypeDesc": "Метод аутентификации, применяемый к импортированным гостевым хостам. Выберите тип аутентификации, соответствующий способу подключения к гостевым хостам.",
|
||||
"authTypePassword": "Пароль",
|
||||
"authTypeKey": "SSH-ключ",
|
||||
"authTypeCredential": "Учётные данные",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Нет",
|
||||
"proxmoxDefaultCredential": "Учётные данные по умолчанию",
|
||||
"proxmoxDefaultCredentialDesc": "Учётные данные, используемые для импортированных гостевых хостов, когда в качестве типа аутентификации выбраны «Учётные данные».",
|
||||
"proxmoxWindowsDetection": "Обнаружение Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Шаблоны имён через запятую, при совпадении с которыми используется RDP вместо SSH (без учёта регистра).",
|
||||
"proxmoxDockerDetection": "Обнаружение Docker",
|
||||
"proxmoxDockerDetectionDesc": "Шаблоны имён через запятую, при совпадении с которыми для гостя включается Docker.",
|
||||
"proxmoxPreferredRanges": "Предпочтительные IP-диапазоны",
|
||||
"proxmoxPreferredRangesDesc": "Список префиксов через запятую в порядке приоритета для выбора IP-адреса, если у гостя несколько интерфейсов.",
|
||||
"proxmoxAutoSync": "Автосинхронизация гостей",
|
||||
"proxmoxAutoSyncDesc": "Периодически обнаруживать этот узел Proxmox и создавать или обновлять импортированные гостевые хосты, пока ваш сеанс разблокирован.",
|
||||
"proxmoxSyncInterval": "Интервал синхронизации (мин.)",
|
||||
"proxmoxSyncIntervalDesc": "Минимум 5 минут. Планировщик пропускает заблокированные сеансы.",
|
||||
"proxmoxMarkMissing": "Помечать отсутствующих гостей",
|
||||
"proxmoxMarkMissingDesc": "Добавлять тег proxmox-missing, когда ранее импортированный гость исчезает, вместо его удаления.",
|
||||
"proxmoxLastSync": "Последняя синхронизация",
|
||||
"proxmoxLastSyncNever": "Не выполнялась",
|
||||
"proxmoxLastSyncNoResult": "Нет сведений о результате",
|
||||
"proxmoxLastSyncSummary": "{{created}} создано, {{updated}} обновлено, {{markedMissing}} отсутствует, {{skipped}} пропущено",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Ошибка",
|
||||
"pending": "Ожидание",
|
||||
"success": "Успешно"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Обнаружить и импортировать гостей Proxmox",
|
||||
"proxmoxImportTitle": "Импорт из Proxmox",
|
||||
"proxmoxSelectHost": "Выберите хост Proxmox…",
|
||||
"proxmoxDiscover": "Обнаружить",
|
||||
"proxmoxDiscovering": "Обнаружение…",
|
||||
"proxmoxDiscoverGuests": "Обнаружить гостей",
|
||||
"proxmoxSelectAll": "Выбрать всё",
|
||||
"proxmoxDeselectAll": "Снять выделение",
|
||||
"proxmoxNoGuests": "На этом узле Proxmox гости не найдены.",
|
||||
"proxmoxResultImported": "{{count}} импортировано",
|
||||
"proxmoxResultUpdated": "{{count}} обновлено",
|
||||
"proxmoxResultFailed": "{{count}} не удалось",
|
||||
"proxmoxImportComplete": "Импорт Proxmox завершен: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Обнаружение не удалось",
|
||||
"proxmoxImportFailed": "Импорт не удался",
|
||||
"docsLink": "Документация",
|
||||
"hostNotFound": "Хост не найден",
|
||||
"loadingHost": "Загрузка хоста...",
|
||||
"none": "Нет",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Импортировать {{count}} хост",
|
||||
"proxmoxImportButton_other": "Импортировать {{count}} хостов"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Подключено"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Kullanımı",
|
||||
"memoryUsage": "Bellek Kullanımı",
|
||||
"diskUsage": "Disk Kullanımı",
|
||||
"networkInterfaces": "Ağ Arayüzleri",
|
||||
"uptime": "Çalışma Süresi",
|
||||
"systemInfo": "Sistem Bilgisi",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Çekirdek",
|
||||
"cpuCores_other": "{{count}} Çekirdek",
|
||||
"used": "Kullanılan",
|
||||
"total": "Total",
|
||||
"hostname": "Ana bilgisayar adı",
|
||||
"kernel": "Çekirdek",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Ad",
|
||||
"status": "Durum",
|
||||
"type": "Tür",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Çalışıyor",
|
||||
"stopped": "Durduruldu",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Etkin Değil",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "çevrimiçi",
|
||||
"nodeOffline": "Çevrimdışı",
|
||||
"noHostSelected": "Sunucu seçilmedi",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Bağlanıyor...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Yenile",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "Tümü",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Entegrasyonu",
|
||||
"enableProxmox": "Proxmox'u Etkinleştir",
|
||||
"enableProxmoxDesc": "Bu ana bilgisayarı bir Proxmox düğümü olarak işaretle. Bu ana bilgisayardan doğrudan konuk keşfi ve içe aktarmayı etkinleştirir.",
|
||||
"proxmoxDefaultAuthType": "Varsayılan Kimlik Doğrulama Türü",
|
||||
"proxmoxDefaultAuthTypeDesc": "İçe aktarılan konuk ana bilgisayarlara uygulanan kimlik doğrulama yöntemi. Konuklarınıza nasıl bağlandığınıza uygun kimlik doğrulama türünü seçin.",
|
||||
"authTypePassword": "Parola",
|
||||
"authTypeKey": "SSH Anahtarı",
|
||||
"authTypeCredential": "Kimlik Bilgisi",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Yok",
|
||||
"proxmoxDefaultCredential": "Varsayılan Kimlik Bilgisi",
|
||||
"proxmoxDefaultCredentialDesc": "Kimlik doğrulama türü Kimlik Bilgisi olarak ayarlandığında içe aktarılan misafir ana makineleri için kullanılan kimlik bilgisi.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP algılama",
|
||||
"proxmoxWindowsDetectionDesc": "SSH yerine RDP'yi tetikleyen virgülle ayrılmış ad desenleri (büyük/küçük harf duyarsız)",
|
||||
"proxmoxDockerDetection": "Docker algılama",
|
||||
"proxmoxDockerDetectionDesc": "Eşleşen misafirler için Docker'ı etkinleştiren virgülle ayrılmış ad desenleri",
|
||||
"proxmoxPreferredRanges": "Tercih edilen IP aralıkları",
|
||||
"proxmoxPreferredRangesDesc": "Bir misafirde birden fazla arayüz olduğunda IP seçimi için öncelik sırasına göre virgülle ayrılmış önekler",
|
||||
"proxmoxAutoSync": "Misafirleri otomatik eşitle",
|
||||
"proxmoxAutoSyncDesc": "Oturumunuz açıkken bu Proxmox düğümünü periyodik olarak keşfet ve içe aktarılan misafir ana makinelerini oluştur veya güncelle.",
|
||||
"proxmoxSyncInterval": "Eşitleme aralığı (dakika)",
|
||||
"proxmoxSyncIntervalDesc": "En az 5 dakika. Zamanlayıcı kilitli kullanıcı oturumlarını atlar.",
|
||||
"proxmoxMarkMissing": "Kayıp misafirleri işaretle",
|
||||
"proxmoxMarkMissingDesc": "Daha önce içe aktarılan bir misafir kaybolduğunda onu silmek yerine bir proxmox-missing etiketi ekle.",
|
||||
"proxmoxLastSync": "Son eşitleme",
|
||||
"proxmoxLastSyncNever": "Henüz eşitlenmedi",
|
||||
"proxmoxLastSyncNoResult": "Sonuç detayı yok",
|
||||
"proxmoxLastSyncSummary": "{{created}} oluşturuldu, {{updated}} güncellendi, {{markedMissing}} kayıp işaretlendi, {{skipped}} atlandı",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Başarısız",
|
||||
"pending": "Beklemede",
|
||||
"success": "Başarılı"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Proxmox misafirlerini keşfet ve içe aktar",
|
||||
"proxmoxImportTitle": "Proxmox'tan içe aktar",
|
||||
"proxmoxSelectHost": "Bir Proxmox ana makinesi seç…",
|
||||
"proxmoxDiscover": "Keşfet",
|
||||
"proxmoxDiscovering": "Keşfediliyor…",
|
||||
"proxmoxDiscoverGuests": "Misafirleri keşfet",
|
||||
"proxmoxSelectAll": "Tümünü seç",
|
||||
"proxmoxDeselectAll": "Tüm seçimi kaldır",
|
||||
"proxmoxNoGuests": "Bu Proxmox düğümünde hiç misafir bulunamadı.",
|
||||
"proxmoxResultImported": "{{count}} içe aktarıldı",
|
||||
"proxmoxResultUpdated": "{{count}} güncellendi",
|
||||
"proxmoxResultFailed": "{{count}} başarısız",
|
||||
"proxmoxImportComplete": "Proxmox içe aktarma tamamlandı: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Keşif başarısız",
|
||||
"proxmoxImportFailed": "İçe aktarma başarısız",
|
||||
"docsLink": "Dokümanları görüntüle",
|
||||
"hostNotFound": "Host bulunamadı",
|
||||
"loadingHost": "Host yükleniyor...",
|
||||
"none": "Hiçbiri",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "{{count}} ana makineyi içe aktar",
|
||||
"proxmoxImportButton_other": "{{count}} sunucuyu içe aktar"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Bağlandı"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU Usage",
|
||||
"memoryUsage": "Memory Usage",
|
||||
"diskUsage": "Disk Usage",
|
||||
"networkInterfaces": "Network Interfaces",
|
||||
"uptime": "Uptime",
|
||||
"systemInfo": "System Information",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Core",
|
||||
"cpuCores_other": "{{count}} Cores",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"hostname": "Hostname",
|
||||
"kernel": "Kernel",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Disk",
|
||||
"running": "Running",
|
||||
"stopped": "Stopped",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Inactive",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Online",
|
||||
"nodeOffline": "Offline",
|
||||
"noHostSelected": "No host selected",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Refresh",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "All",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox Integration",
|
||||
"enableProxmox": "Enable Proxmox",
|
||||
"enableProxmoxDesc": "Mark this host as a Proxmox node. Enables guest discovery and import directly from this host.",
|
||||
"proxmoxDefaultAuthType": "Default Auth Type",
|
||||
"proxmoxDefaultAuthTypeDesc": "Authentication method applied to imported guest hosts. Choose the auth type that matches how you connect to your guests.",
|
||||
"authTypePassword": "Password",
|
||||
"authTypeKey": "SSH Key",
|
||||
"authTypeCredential": "Credential",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "None",
|
||||
"proxmoxDefaultCredential": "Default Credential",
|
||||
"proxmoxDefaultCredentialDesc": "Credential used for imported guest hosts when auth type is set to Credential.",
|
||||
"proxmoxWindowsDetection": "Windows / RDP detection",
|
||||
"proxmoxWindowsDetectionDesc": "Comma-separated name patterns that trigger RDP instead of SSH (case-insensitive)",
|
||||
"proxmoxDockerDetection": "Docker detection",
|
||||
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
|
||||
"proxmoxPreferredRanges": "Preferred IP ranges",
|
||||
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
|
||||
"proxmoxAutoSync": "Auto sync guests",
|
||||
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
|
||||
"proxmoxSyncInterval": "Sync interval (minutes)",
|
||||
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
|
||||
"proxmoxMarkMissing": "Mark missing guests",
|
||||
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
|
||||
"proxmoxLastSync": "Last sync",
|
||||
"proxmoxLastSyncNever": "Not synced yet",
|
||||
"proxmoxLastSyncNoResult": "No result details",
|
||||
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Failed",
|
||||
"pending": "Pending",
|
||||
"success": "Success"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
|
||||
"proxmoxImportTitle": "Import from Proxmox",
|
||||
"proxmoxSelectHost": "Select a Proxmox host…",
|
||||
"proxmoxDiscover": "Discover",
|
||||
"proxmoxDiscovering": "Discovering…",
|
||||
"proxmoxDiscoverGuests": "Discover guests",
|
||||
"proxmoxSelectAll": "Select all",
|
||||
"proxmoxDeselectAll": "Deselect all",
|
||||
"proxmoxNoGuests": "No guests found on this Proxmox node.",
|
||||
"proxmoxResultImported": "{{count}} imported",
|
||||
"proxmoxResultUpdated": "{{count}} updated",
|
||||
"proxmoxResultFailed": "{{count}} failed",
|
||||
"proxmoxImportComplete": "Proxmox import complete: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Discovery failed",
|
||||
"proxmoxImportFailed": "Import failed",
|
||||
"docsLink": "View docs",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host...",
|
||||
"none": "None",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Import {{count}} host",
|
||||
"proxmoxImportButton_other": "Import {{count}} hosts"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "Sử dụng CPU",
|
||||
"memoryUsage": "Sử dụng bộ nhớ",
|
||||
"diskUsage": "Mức sử dụng ổ đĩa",
|
||||
"networkInterfaces": "Giao diện mạng",
|
||||
"uptime": "Thời gian hoạt động",
|
||||
"systemInfo": "Thông tin hệ thống",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} Lõi",
|
||||
"cpuCores_other": "{{count}} Lõi",
|
||||
"used": "Đã dùng",
|
||||
"total": "Total",
|
||||
"hostname": "Tên máy chủ",
|
||||
"kernel": "Nhân",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "Tên",
|
||||
"status": "Trạng thái",
|
||||
"type": "Loại",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "Ổ đĩa",
|
||||
"running": "Đang chạy",
|
||||
"stopped": "Đã dừng",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "Không hoạt động",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "Trực tuyến",
|
||||
"nodeOffline": "Ngoại tuyến",
|
||||
"noHostSelected": "Chưa chọn máy chủ",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "Đang kết nối...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "Làm mới",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "Tất cả",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Tích hợp Proxmox",
|
||||
"enableProxmox": "Bật Proxmox",
|
||||
"enableProxmoxDesc": "Đánh dấu máy chủ này là một node Proxmox. Cho phép khám phá và nhập khách trực tiếp từ máy chủ này.",
|
||||
"proxmoxDefaultAuthType": "Loại xác thực mặc định",
|
||||
"proxmoxDefaultAuthTypeDesc": "Phương thức xác thực áp dụng cho các máy khách được nhập. Chọn loại xác thực phù hợp với cách bạn kết nối đến máy khách của mình.",
|
||||
"authTypePassword": "Mật khẩu",
|
||||
"authTypeKey": "Khóa SSH",
|
||||
"authTypeCredential": "Thông tin xác thực",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "Không",
|
||||
"proxmoxDefaultCredential": "Thông tin đăng nhập mặc định",
|
||||
"proxmoxDefaultCredentialDesc": "Thông tin xác thực dùng cho máy khách được nhập khi phương thức xác thực là Credential.",
|
||||
"proxmoxWindowsDetection": "Phát hiện Windows / RDP",
|
||||
"proxmoxWindowsDetectionDesc": "Các mẫu tên cách nhau bằng dấu phẩy để kích hoạt RDP thay vì SSH (không phân biệt hoa thường).",
|
||||
"proxmoxDockerDetection": "Phát hiện Docker",
|
||||
"proxmoxDockerDetectionDesc": "Các mẫu tên cách nhau bằng dấu phẩy để bật Docker cho các máy khách phù hợp.",
|
||||
"proxmoxPreferredRanges": "Dải IP ưu tiên",
|
||||
"proxmoxPreferredRangesDesc": "Các tiền tố cách nhau bằng dấu phẩy theo thứ tự ưu tiên để chọn IP khi máy khách có nhiều giao diện mạng.",
|
||||
"proxmoxAutoSync": "Tự động đồng bộ máy khách",
|
||||
"proxmoxAutoSyncDesc": "Định kỳ khám phá nút Proxmox này và tạo hoặc cập nhật máy khách đã nhập khi phiên của bạn được mở khóa.",
|
||||
"proxmoxSyncInterval": "Khoảng đồng bộ (phút)",
|
||||
"proxmoxSyncIntervalDesc": "Tối thiểu 5 phút. Bộ lập lịch bỏ qua các phiên người dùng bị khóa.",
|
||||
"proxmoxMarkMissing": "Đánh dấu máy khách bị thiếu",
|
||||
"proxmoxMarkMissingDesc": "Thêm thẻ proxmox-missing khi máy khách đã nhập trước đó biến mất thay vì xóa nó.",
|
||||
"proxmoxLastSync": "Lần đồng bộ cuối",
|
||||
"proxmoxLastSyncNever": "Chưa đồng bộ",
|
||||
"proxmoxLastSyncNoResult": "Không có chi tiết kết quả",
|
||||
"proxmoxLastSyncSummary": "Đã tạo {{created}}, cập nhật {{updated}}, thiếu {{markedMissing}}, bỏ qua {{skipped}}",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "Thất bại",
|
||||
"pending": "Đang chờ",
|
||||
"success": "Thành công"
|
||||
},
|
||||
"proxmoxDiscoverAction": "Khám phá và nhập máy khách Proxmox",
|
||||
"proxmoxImportTitle": "Nhập từ Proxmox",
|
||||
"proxmoxSelectHost": "Chọn máy chủ Proxmox…",
|
||||
"proxmoxDiscover": "Khám phá",
|
||||
"proxmoxDiscovering": "Đang khám phá…",
|
||||
"proxmoxDiscoverGuests": "Khám phá máy khách",
|
||||
"proxmoxSelectAll": "Chọn tất cả",
|
||||
"proxmoxDeselectAll": "Bỏ chọn tất cả",
|
||||
"proxmoxNoGuests": "Không tìm thấy máy khách nào trên nút Proxmox này.",
|
||||
"proxmoxResultImported": "Đã nhập {{count}}",
|
||||
"proxmoxResultUpdated": "Đã cập nhật {{count}}",
|
||||
"proxmoxResultFailed": "{{count}} thất bại",
|
||||
"proxmoxImportComplete": "Hoàn tất nhập từ Proxmox: {{summary}}",
|
||||
"proxmoxDiscoveryFailed": "Khám phá thất bại",
|
||||
"proxmoxImportFailed": "Nhập thất bại",
|
||||
"docsLink": "Xem tài liệu",
|
||||
"hostNotFound": "Không tìm thấy máy chủ",
|
||||
"loadingHost": "Đang tải máy chủ...",
|
||||
"none": "Không có",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "Nhập {{count}} máy chủ",
|
||||
"proxmoxImportButton_other": "Nhập {{count}} máy chủ"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Đã kết nối"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU 使用率",
|
||||
"memoryUsage": "内存使用量",
|
||||
"diskUsage": "磁盘使用率",
|
||||
"networkInterfaces": "网络接口",
|
||||
"uptime": "运行时间",
|
||||
"systemInfo": "系统信息",
|
||||
"guestsSummary": "客户主机",
|
||||
"storagePools": "存储池",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} 核",
|
||||
"cpuCores_other": "{{count}} 核",
|
||||
"used": "已用",
|
||||
"total": "总计",
|
||||
"hostname": "主机名",
|
||||
"kernel": "内核",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "名称",
|
||||
"status": "状态",
|
||||
"type": "类型",
|
||||
"cpu": "CPU",
|
||||
"mem": "内存",
|
||||
"disk": "磁盘",
|
||||
"running": "运行中",
|
||||
"stopped": "已停止",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "未激活",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "达到法定节点数量",
|
||||
"notQuorate": "未达到法定节点数量",
|
||||
"nodeOnline": "在线",
|
||||
"nodeOffline": "离线",
|
||||
"noHostSelected": "未选择主机",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "连接中...",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "刷新",
|
||||
"searchGuests": "搜索客户主机...",
|
||||
"filterAll": "全部",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "没有符合当前筛选条件的客户主机。"
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "显示此主机的 Proxmox 统计信息标签页,其中包含通过 SSH 收集的实时节点、客户机、存储和集群统计信息。",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "覆盖节点名称",
|
||||
"proxmoxStatsNodeOverrideDesc": "覆盖自动检测的 Proxmox 节点名称(默认情况下通过主机名检测)。",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox 集成",
|
||||
"enableProxmox": "启用 Proxmox",
|
||||
"enableProxmoxDesc": "将此主机标记为 Proxmox 节点。启用从此主机直接进行客户机发现和导入。",
|
||||
"proxmoxDefaultAuthType": "默认认证类型",
|
||||
"proxmoxDefaultAuthTypeDesc": "应用于导入的客户机主机的认证方法。选择与您连接客户机的方式相匹配的认证类型。",
|
||||
"authTypePassword": "密码",
|
||||
"authTypeKey": "SSH 密钥",
|
||||
"authTypeCredential": "凭据",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "无",
|
||||
"proxmoxDefaultCredential": "默认凭据",
|
||||
"proxmoxDefaultCredentialDesc": "当认证类型设置为凭据时,用于导入的客户主机的凭据。",
|
||||
"proxmoxWindowsDetection": "Windows / RDP 检测",
|
||||
"proxmoxWindowsDetectionDesc": "逗号分隔的名称模式,匹配时触发 RDP 而非 SSH(不区分大小写)",
|
||||
"proxmoxDockerDetection": "Docker 检测",
|
||||
"proxmoxDockerDetectionDesc": "逗号分隔的名称模式,为匹配的客户主机启用 Docker",
|
||||
"proxmoxPreferredRanges": "首选 IP 范围",
|
||||
"proxmoxPreferredRangesDesc": "逗号分隔的前缀,按优先级顺序用于当客户主机有多个网络接口时的 IP 选择",
|
||||
"proxmoxAutoSync": "自动同步客户主机",
|
||||
"proxmoxAutoSyncDesc": "在会话未锁定时定期发现此 Proxmox 节点,并创建或更新导入的客户主机。",
|
||||
"proxmoxSyncInterval": "同步间隔(分钟)",
|
||||
"proxmoxSyncIntervalDesc": "最小 5 分钟。调度器会跳过已锁定的用户会话。",
|
||||
"proxmoxMarkMissing": "标记缺失的客户主机",
|
||||
"proxmoxMarkMissingDesc": "当之前导入的客户主机消失时,添加 proxmox-missing 标签,而非删除它。",
|
||||
"proxmoxLastSync": "上次同步",
|
||||
"proxmoxLastSyncNever": "尚未同步",
|
||||
"proxmoxLastSyncNoResult": "无结果详情",
|
||||
"proxmoxLastSyncSummary": "已创建 {{created}},已更新 {{updated}},已标记缺失 {{markedMissing}},已跳过 {{skipped}}",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "失败",
|
||||
"pending": "待处理",
|
||||
"success": "成功"
|
||||
},
|
||||
"proxmoxDiscoverAction": "发现并导入 Proxmox 客户主机",
|
||||
"proxmoxImportTitle": "从 Proxmox 导入",
|
||||
"proxmoxSelectHost": "选择 Proxmox 主机…",
|
||||
"proxmoxDiscover": "发现",
|
||||
"proxmoxDiscovering": "正在发现…",
|
||||
"proxmoxDiscoverGuests": "发现客户主机",
|
||||
"proxmoxSelectAll": "全选",
|
||||
"proxmoxDeselectAll": "取消全选",
|
||||
"proxmoxNoGuests": "此 Proxmox 节点上未找到客户主机。",
|
||||
"proxmoxResultImported": "已导入 {{count}} 个",
|
||||
"proxmoxResultUpdated": "已更新 {{count}} 个",
|
||||
"proxmoxResultFailed": "已失败 {{count}} 个",
|
||||
"proxmoxImportComplete": "Proxmox 导入完成:{{summary}}",
|
||||
"proxmoxDiscoveryFailed": "发现失败",
|
||||
"proxmoxImportFailed": "导入失败",
|
||||
"docsLink": "查看文档",
|
||||
"hostNotFound": "未找到主机",
|
||||
"loadingHost": "正在加载主机...",
|
||||
"none": "无",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "导入 {{count}} 个主机",
|
||||
"proxmoxImportButton_other": "导入 {{count}} 个主机"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "已连接"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"nav": {
|
||||
"proxmoxStats": "Proxmox Stats"
|
||||
},
|
||||
"proxmoxStats": {
|
||||
"tabTitle": "Proxmox Stats",
|
||||
"cpuUsage": "CPU 使用率",
|
||||
"memoryUsage": "記憶體使用量",
|
||||
"diskUsage": "磁碟使用率",
|
||||
"networkInterfaces": "網路介面",
|
||||
"uptime": "運行時間",
|
||||
"systemInfo": "系統資訊",
|
||||
"guestsSummary": "Guests",
|
||||
"storagePools": "Storage Pools",
|
||||
"clusterHealth": "Cluster Health",
|
||||
"cpuCores_one": "{{count}} 核心",
|
||||
"cpuCores_other": "{{count}} 核心",
|
||||
"used": "已使用",
|
||||
"total": "Total",
|
||||
"hostname": "主機名稱",
|
||||
"kernel": "核心",
|
||||
"pveVersion": "PVE Version",
|
||||
"name": "名稱",
|
||||
"status": "狀態",
|
||||
"type": "類型",
|
||||
"cpu": "CPU",
|
||||
"mem": "Mem",
|
||||
"disk": "磁碟",
|
||||
"running": "執行中",
|
||||
"stopped": "已停止",
|
||||
"guestCounts": "{{running}}/{{total}} running",
|
||||
"inactive": "未啟用",
|
||||
"notClustered": "Standalone node (not part of a cluster)",
|
||||
"quorate": "Quorate",
|
||||
"notQuorate": "Not quorate",
|
||||
"nodeOnline": "在線",
|
||||
"nodeOffline": "離線",
|
||||
"noHostSelected": "未選擇主機",
|
||||
"pveshNotFound": "pvesh not found - is this a Proxmox node?",
|
||||
"nodeDetectionFailed": "Unable to determine a valid Proxmox node name",
|
||||
"connecting": "連線中…",
|
||||
"connectionFailed": "Failed to connect to Proxmox node",
|
||||
"refresh": "重新整理",
|
||||
"searchGuests": "Search guests...",
|
||||
"filterAll": "全選",
|
||||
"filterVm": "VM",
|
||||
"filterLxc": "LXC",
|
||||
"noGuests": "No VMs or LXC containers found on this node.",
|
||||
"noGuestsMatch": "No guests match the current filters."
|
||||
},
|
||||
"hosts": {
|
||||
"tabProxmox": "Proxmox",
|
||||
"enableProxmoxStats": "Enable Proxmox Stats",
|
||||
"enableProxmoxStatsDesc": "Show a Proxmox Stats tab for this host with live node, guest, storage, and cluster stats collected over SSH.",
|
||||
"proxmoxStatsPollInterval": "Poll interval (seconds)",
|
||||
"proxmoxStatsPollIntervalDesc": "Minimum 15 seconds. How often to refresh Proxmox node stats.",
|
||||
"proxmoxStatsNodeOverride": "Node name override",
|
||||
"proxmoxStatsNodeOverrideDesc": "Override auto-detection of the Proxmox node name (detected via hostname by default).",
|
||||
"proxmoxStatsNodeOverridePlaceholder": "Auto-detected from hostname",
|
||||
"authTypeStepca": "Step CA",
|
||||
"proxmoxIntegration": "Proxmox 整合",
|
||||
"enableProxmox": "啟用 Proxmox",
|
||||
"enableProxmoxDesc": "將此主機標記為 Proxmox 節點。啟用直接從此主機探索及匯入客體的功能。",
|
||||
"proxmoxDefaultAuthType": "預設驗證類型",
|
||||
"proxmoxDefaultAuthTypeDesc": "套用至匯入客體主機的驗證方法。選擇與您連線至客體的方式相符的驗證類型。",
|
||||
"authTypePassword": "密碼",
|
||||
"authTypeKey": "SSH 金鑰",
|
||||
"authTypeCredential": "憑證",
|
||||
"authTypeOpkssh": "OPKSSH",
|
||||
"authTypeNone": "無",
|
||||
"proxmoxDefaultCredential": "預設憑證",
|
||||
"proxmoxDefaultCredentialDesc": "當驗證類型設為憑證時,用於已匯入之訪客主機的憑證。",
|
||||
"proxmoxWindowsDetection": "Windows / RDP 偵測",
|
||||
"proxmoxWindowsDetectionDesc": "逗號分隔的名稱模式,符合時將觸發 RDP 而非 SSH(不分大小寫)",
|
||||
"proxmoxDockerDetection": "Docker 偵測",
|
||||
"proxmoxDockerDetectionDesc": "逗號分隔的名稱模式,符合時將為對應的訪客主機啟用 Docker",
|
||||
"proxmoxPreferredRanges": "偏好的 IP 範圍",
|
||||
"proxmoxPreferredRangesDesc": "當訪客主機有多個網路介面時,以此逗號分隔的前綴清單依優先順序選取 IP",
|
||||
"proxmoxAutoSync": "自動同步訪客主機",
|
||||
"proxmoxAutoSyncDesc": "在您的工作階段解鎖時,定期探索此 Proxmox 節點並建立或更新已匯入的訪客主機。",
|
||||
"proxmoxSyncInterval": "同步間隔(分鐘)",
|
||||
"proxmoxSyncIntervalDesc": "最少 5 分鐘。排程器會略過鎖定的使用者工作階段。",
|
||||
"proxmoxMarkMissing": "標記遺失的訪客主機",
|
||||
"proxmoxMarkMissingDesc": "當先前匯入的訪客主機消失時,新增 proxmox-missing 標籤而非將其刪除。",
|
||||
"proxmoxLastSync": "上次同步",
|
||||
"proxmoxLastSyncNever": "尚未同步",
|
||||
"proxmoxLastSyncNoResult": "無結果詳細資料",
|
||||
"proxmoxLastSyncSummary": "建立 {{created}},更新 {{updated}},遺失 {{markedMissing}},略過 {{skipped}}",
|
||||
"proxmoxLastSyncStatus": {
|
||||
"error": "失敗",
|
||||
"pending": "待處理",
|
||||
"success": "成功"
|
||||
},
|
||||
"proxmoxDiscoverAction": "探索並匯入 Proxmox 訪客主機",
|
||||
"proxmoxImportTitle": "從 Proxmox 匯入",
|
||||
"proxmoxSelectHost": "選取 Proxmox 主機…",
|
||||
"proxmoxDiscover": "探索",
|
||||
"proxmoxDiscovering": "探索中…",
|
||||
"proxmoxDiscoverGuests": "探索訪客主機",
|
||||
"proxmoxSelectAll": "全選",
|
||||
"proxmoxDeselectAll": "取消全選",
|
||||
"proxmoxNoGuests": "此 Proxmox 節點上找不到任何訪客主機。",
|
||||
"proxmoxResultImported": "已匯入 {{count}} 台",
|
||||
"proxmoxResultUpdated": "已更新 {{count}} 台",
|
||||
"proxmoxResultFailed": "失敗 {{count}} 台",
|
||||
"proxmoxImportComplete": "Proxmox 匯入完成:{{summary}}",
|
||||
"proxmoxDiscoveryFailed": "探索失敗",
|
||||
"proxmoxImportFailed": "匯入失敗",
|
||||
"docsLink": "檢視文件",
|
||||
"hostNotFound": "找不到主機",
|
||||
"loadingHost": "正在載入主機...",
|
||||
"none": "無",
|
||||
"proxmoxGuestsSelected_one": "{{count}} guest, {{selected}} selected",
|
||||
"proxmoxGuestsSelected_other": "{{count}} guests, {{selected}} selected",
|
||||
"proxmoxImportButton_one": "匯入 {{count}} 台主機",
|
||||
"proxmoxImportButton_other": "匯入 {{count}} 台主機"
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "已連線"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"id": "proxmox",
|
||||
"name": "Proxmox",
|
||||
"version": "1.0.0",
|
||||
"description": "Find Proxmox VE guests through an SSH host, import them as hosts and keep them in sync.",
|
||||
"author": {
|
||||
"name": "Termix"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Termix-SSH/Termix",
|
||||
"category": "Infrastructure",
|
||||
"icon": "Server",
|
||||
"engine": {
|
||||
"termix": ">=2.9.0",
|
||||
"api": "1"
|
||||
},
|
||||
"capabilities": [
|
||||
"hosts:read",
|
||||
"hosts:write",
|
||||
"credentials:use",
|
||||
"ssh:connect",
|
||||
"db:own",
|
||||
"network:serve",
|
||||
"ui:surface",
|
||||
"users:impersonate"
|
||||
],
|
||||
"contributes": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "proxmox-stats",
|
||||
"titleKey": "nav.proxmoxStats",
|
||||
"icon": "HardDrive",
|
||||
"openFrom": ["host-context-menu", "palette"]
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"host": {
|
||||
"fields": [
|
||||
{
|
||||
"key": "enableProxmox",
|
||||
"type": "custom",
|
||||
"component": "proxmoxHostSettings",
|
||||
"defaultable": false
|
||||
},
|
||||
{
|
||||
"key": "proxmoxConfig",
|
||||
"type": "custom",
|
||||
"component": "proxmoxHostSettings",
|
||||
"defaultable": false
|
||||
},
|
||||
{
|
||||
"key": "enableProxmoxStats",
|
||||
"type": "custom",
|
||||
"component": "proxmoxHostSettings",
|
||||
"defaultable": false
|
||||
},
|
||||
{
|
||||
"key": "proxmoxStatsConfig",
|
||||
"type": "custom",
|
||||
"component": "proxmoxHostSettings",
|
||||
"defaultable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
-- proxmox 0001: adopt_proxmox_stats
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `proxmox_node_history` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`host_id` int NOT NULL,
|
||||
`ts` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`cpu_percent` double,
|
||||
`mem_percent` double,
|
||||
`disk_percent` double,
|
||||
`net_rx_bytes` int,
|
||||
`net_tx_bytes` int,
|
||||
FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
RENAME TABLE `proxmox_node_history` TO `p_proxmox_node_history`;
|
||||
CREATE TABLE IF NOT EXISTS `proxmox_stats_preferences` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` varchar(255) NOT NULL,
|
||||
`host_id` int NOT NULL,
|
||||
`layout` text NOT NULL,
|
||||
`created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`host_id`) REFERENCES `ssh_data` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
RENAME TABLE `proxmox_stats_preferences` TO `p_proxmox_stats_preferences`;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- proxmox 0001: adopt_proxmox_stats
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "proxmox_node_history" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"host_id" integer NOT NULL,
|
||||
"ts" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cpu_percent" double precision,
|
||||
"mem_percent" double precision,
|
||||
"disk_percent" double precision,
|
||||
"net_rx_bytes" integer,
|
||||
"net_tx_bytes" integer,
|
||||
FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE "proxmox_node_history" RENAME TO "p_proxmox_node_history";
|
||||
CREATE TABLE IF NOT EXISTS "proxmox_stats_preferences" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"user_id" varchar(255) NOT NULL,
|
||||
"host_id" integer NOT NULL,
|
||||
"layout" text NOT NULL,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "idx_proxmox_stats_prefs_user_host" ON "proxmox_stats_preferences" ("user_id", "host_id");
|
||||
ALTER TABLE "proxmox_stats_preferences" RENAME TO "p_proxmox_stats_preferences";
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"version": 1,
|
||||
"tables": {
|
||||
"node_history": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"hostId": {
|
||||
"type": "refHost",
|
||||
"notNull": true
|
||||
},
|
||||
"ts": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
},
|
||||
"cpuPercent": {
|
||||
"type": "real"
|
||||
},
|
||||
"memPercent": {
|
||||
"type": "real"
|
||||
},
|
||||
"diskPercent": {
|
||||
"type": "real"
|
||||
},
|
||||
"netRxBytes": {
|
||||
"type": "integer"
|
||||
},
|
||||
"netTxBytes": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"indexes": [],
|
||||
"adopts": "proxmox_node_history"
|
||||
},
|
||||
"stats_preferences": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "refUser",
|
||||
"length": 255,
|
||||
"notNull": true
|
||||
},
|
||||
"hostId": {
|
||||
"type": "refHost",
|
||||
"notNull": true
|
||||
},
|
||||
"layout": {
|
||||
"type": "text",
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
}
|
||||
},
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_proxmox_stats_prefs_user_host",
|
||||
"columns": ["userId", "hostId"],
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"adopts": "proxmox_stats_preferences"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
-- proxmox 0001: adopt_proxmox_stats
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "proxmox_node_history" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"host_id" integer NOT NULL,
|
||||
"ts" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cpu_percent" real,
|
||||
"mem_percent" real,
|
||||
"disk_percent" real,
|
||||
"net_rx_bytes" integer,
|
||||
"net_tx_bytes" integer,
|
||||
FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE "proxmox_node_history" RENAME TO "p_proxmox_node_history";
|
||||
CREATE TABLE IF NOT EXISTS "proxmox_stats_preferences" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" text NOT NULL,
|
||||
"host_id" integer NOT NULL,
|
||||
"layout" text NOT NULL,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("host_id") REFERENCES "ssh_data" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "idx_proxmox_stats_prefs_user_host" ON "proxmox_stats_preferences" ("user_id", "host_id");
|
||||
ALTER TABLE "proxmox_stats_preferences" RENAME TO "p_proxmox_stats_preferences";
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@termix-plugin/proxmox",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "termix-plugin build",
|
||||
"test": "termix-plugin test",
|
||||
"typecheck": "tsc -p .",
|
||||
"validate": "termix-plugin validate"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^1.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@termix/plugin-sdk": "^1.0.0",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"jsdom": "^30.0.1",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@termix/plugin-sdk": "^1.0.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"express": "^5.2.1",
|
||||
"react": "^19.2.8",
|
||||
"sonner": "^2.0.8",
|
||||
"ssh2": "^1.17.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export type ProxmoxSource = {
|
||||
source: "proxmox";
|
||||
sourceHostId: number;
|
||||
node: string;
|
||||
vmid: number;
|
||||
type: "qemu" | "lxc";
|
||||
lastSeenAt?: string;
|
||||
lastStatus?: string;
|
||||
missingSince?: string | null;
|
||||
};
|
||||
|
||||
export function parseJsonObject(value: unknown): Record<string, unknown> {
|
||||
if (!value) return {};
|
||||
if (typeof value === "object") return value as Record<string, unknown>;
|
||||
if (typeof value !== "string") return {};
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === "object"
|
||||
? (parsed as Record<string, unknown>)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** The guest a host was imported from, read from its proxmoxConfig setting. */
|
||||
export function getProxmoxSource(config: unknown): ProxmoxSource | null {
|
||||
const source = parseJsonObject(config).source;
|
||||
if (!source || typeof source !== "object") return null;
|
||||
const src = source as Record<string, unknown>;
|
||||
if (
|
||||
src.source !== "proxmox" ||
|
||||
typeof src.sourceHostId !== "number" ||
|
||||
typeof src.node !== "string" ||
|
||||
typeof src.vmid !== "number" ||
|
||||
(src.type !== "qemu" && src.type !== "lxc")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return src as ProxmoxSource;
|
||||
}
|
||||
|
||||
export function proxmoxSourceKey(source: {
|
||||
sourceHostId: number;
|
||||
node: string;
|
||||
type: string;
|
||||
vmid: number;
|
||||
}): string {
|
||||
return `${source.sourceHostId}:${source.node}:${source.type}:${source.vmid}`;
|
||||
}
|
||||
|
||||
export interface ImportedGuest<H = Record<string, unknown>> {
|
||||
host: H;
|
||||
config: Record<string, unknown>;
|
||||
source: ProxmoxSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* The hosts already imported from one Proxmox node, keyed by guest. The
|
||||
* source lives in each guest's proxmoxConfig host setting, not on the host
|
||||
* row, so the caller passes both.
|
||||
*/
|
||||
export function indexImportedGuests<H extends { id: unknown }>(
|
||||
hosts: H[],
|
||||
configs: Array<{ hostId: number; value: unknown }>,
|
||||
sourceHostId: number,
|
||||
): Map<string, ImportedGuest<H>> {
|
||||
const configById = new Map(configs.map((c) => [c.hostId, c.value]));
|
||||
const out = new Map<string, ImportedGuest<H>>();
|
||||
for (const host of hosts) {
|
||||
const config = parseJsonObject(configById.get(Number(host.id)));
|
||||
const source = getProxmoxSource(config);
|
||||
if (source?.sourceHostId === sourceHostId) {
|
||||
out.set(proxmoxSourceKey(source), { host, config, source });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Router } from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { setPluginSsh } from "./ssh.js";
|
||||
import { setPluginCtx } from "./plugin-ctx.js";
|
||||
import { startProxmoxService, stopProxmoxService } from "./routes.js";
|
||||
import {
|
||||
startProxmoxStatsService,
|
||||
stopProxmoxStatsService,
|
||||
} from "./stats-service.js";
|
||||
import { proxmoxNodeHistory } from "./tables.js";
|
||||
import { createProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js";
|
||||
|
||||
export async function activate(ctx: PluginContext) {
|
||||
setPluginSsh(ctx.ssh);
|
||||
setPluginCtx(ctx);
|
||||
ctx.disposables.add(() => setPluginSsh(null));
|
||||
ctx.disposables.add(() => setPluginCtx(null));
|
||||
|
||||
const historyTable = await ctx.db.define(proxmoxNodeHistory);
|
||||
const historyRepository = createProxmoxNodeHistoryRepository(
|
||||
ctx.db,
|
||||
historyTable,
|
||||
);
|
||||
|
||||
const router = ctx.http.router<Router>();
|
||||
startProxmoxService(router);
|
||||
ctx.disposables.add(stopProxmoxService);
|
||||
startProxmoxStatsService(ctx, router, historyRepository);
|
||||
ctx.disposables.add(stopProxmoxStatsService);
|
||||
ctx.log.info("Proxmox routes mounted at /plugin-api/proxmox");
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
stopProxmoxService();
|
||||
stopProxmoxStatsService();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
|
||||
let current: PluginContext | null = null;
|
||||
|
||||
/** Set in activate, cleared on deactivate. */
|
||||
export function setPluginCtx(ctx: PluginContext | null): void {
|
||||
current = ctx;
|
||||
}
|
||||
|
||||
export function pluginCtx(): PluginContext {
|
||||
if (!current) throw new Error("The plugin is not active");
|
||||
return current;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Pure decision: which auth settings an imported Proxmox guest inherits.
|
||||
//
|
||||
// The frontend carries a parallel copy in
|
||||
// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is
|
||||
// what produced the reported import bug, so both are kept behaviourally
|
||||
// identical and each is unit-tested against the same matrix.
|
||||
export function resolveProxmoxImportAuth(
|
||||
defaultAuthType: string | undefined,
|
||||
credentialId: number | null | undefined,
|
||||
): {
|
||||
authType: string;
|
||||
credentialId: number | null;
|
||||
overrideCredentialUsername: number;
|
||||
} {
|
||||
// An explicit special auth type (none/opkssh/tailscale/vault/…) wins.
|
||||
if (
|
||||
defaultAuthType &&
|
||||
defaultAuthType !== "credential" &&
|
||||
!["password", "key"].includes(defaultAuthType)
|
||||
) {
|
||||
return {
|
||||
authType: defaultAuthType,
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// A credential (configured default OR inherited from the source host) is a
|
||||
// concrete auth source -> use it, even when defaultAuthType is the
|
||||
// "password"/"key" default.
|
||||
if (credentialId) {
|
||||
return {
|
||||
authType: "credential",
|
||||
credentialId,
|
||||
overrideCredentialUsername: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
authType: "none",
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export function parseProxmoxJumpHosts(raw: unknown): unknown[] | null {
|
||||
if (!raw) return null;
|
||||
if (Array.isArray(raw)) return raw;
|
||||
if (typeof raw !== "string") return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { and, gte, lte, eq, asc, lt } from "drizzle-orm";
|
||||
import type { PluginDatabase } from "@termix/plugin-sdk/backend";
|
||||
|
||||
export interface ProxmoxNodeHistoryRow {
|
||||
id: number;
|
||||
hostId: number;
|
||||
ts: string;
|
||||
cpuPercent: number | null;
|
||||
memPercent: number | null;
|
||||
diskPercent: number | null;
|
||||
netRxBytes: number | null;
|
||||
netTxBytes: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxNodeHistoryCreateInput {
|
||||
hostId: number;
|
||||
cpuPercent: number | null;
|
||||
memPercent: number | null;
|
||||
diskPercent: number | null;
|
||||
netRxBytes: number | null;
|
||||
netTxBytes: number | null;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type Table = any;
|
||||
type Drizzle = any;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export type ProxmoxNodeHistoryRepository = ReturnType<
|
||||
typeof createProxmoxNodeHistoryRepository
|
||||
>;
|
||||
|
||||
export function createProxmoxNodeHistoryRepository(
|
||||
db: PluginDatabase,
|
||||
table: Table,
|
||||
) {
|
||||
const client = () => db.client<Drizzle>();
|
||||
|
||||
return {
|
||||
async create(input: ProxmoxNodeHistoryCreateInput): Promise<void> {
|
||||
const drizzle = await client();
|
||||
await drizzle.insert(table).values(input);
|
||||
await db.persist();
|
||||
},
|
||||
|
||||
async listRange(
|
||||
hostId: number,
|
||||
fromIso: string,
|
||||
toIso: string,
|
||||
): Promise<ProxmoxNodeHistoryRow[]> {
|
||||
const drizzle = await client();
|
||||
return drizzle
|
||||
.select()
|
||||
.from(table)
|
||||
.where(
|
||||
and(
|
||||
eq(table.hostId, hostId),
|
||||
gte(table.ts, fromIso),
|
||||
lte(table.ts, toIso),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(table.ts));
|
||||
},
|
||||
|
||||
async pruneOlderThan(hostId: number, retentionDays: number): Promise<void> {
|
||||
// ts columns store CURRENT_TIMESTAMP as "YYYY-MM-DD HH:MM:SS" UTC, which
|
||||
// sorts lexicographically, so the cutoff is a plain string comparison.
|
||||
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
const drizzle = await client();
|
||||
await drizzle
|
||||
.delete(table)
|
||||
.where(and(eq(table.hostId, hostId), lt(table.ts, cutoff)));
|
||||
await db.persist();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Proxmox node names are restricted to [a-zA-Z0-9-] by PVE itself,
|
||||
// but we validate defensively before using in a shell command.
|
||||
const SAFE_NODE_RE = /^[a-zA-Z0-9._-]{1,64}$/;
|
||||
|
||||
export function isSafeNodeName(name: string): boolean {
|
||||
return SAFE_NODE_RE.test(name);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import {
|
||||
collectProxmoxStats,
|
||||
type ProxmoxStatsSnapshot,
|
||||
} from "./proxmox/collect-proxmox-stats.js";
|
||||
import type { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js";
|
||||
import { pluginCtx } from "./plugin-ctx.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
/**
|
||||
* Proxmox stats polling gets its own concurrency limiter so a burst of polls
|
||||
* on many hosts cannot starve one host's collection.
|
||||
*/
|
||||
class ConcurrentLimiter {
|
||||
private running = 0;
|
||||
private queue: Array<() => void> = [];
|
||||
constructor(private readonly max: number) {}
|
||||
|
||||
async run<T>(fn: () => Promise<T>): Promise<T> {
|
||||
if (this.running >= this.max) {
|
||||
await new Promise<void>((resolve) => this.queue.push(resolve));
|
||||
}
|
||||
this.running++;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
this.running--;
|
||||
const next = this.queue.shift();
|
||||
if (next) next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const proxmoxStatsPollLimiter = new ConcurrentLimiter(5);
|
||||
|
||||
export interface ProxmoxStatsPollableHost {
|
||||
id: number;
|
||||
userId: string;
|
||||
proxmoxStatsConfig?: string | ProxmoxStatsPollConfig | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxStatsPollConfig {
|
||||
nodeName?: string | null;
|
||||
pollInterval?: number;
|
||||
enabledCards?: string[];
|
||||
}
|
||||
|
||||
const DEFAULT_POLL_INTERVAL_SECONDS = 60;
|
||||
|
||||
export function parseProxmoxStatsConfig(
|
||||
raw: string | ProxmoxStatsPollConfig | null | undefined,
|
||||
): ProxmoxStatsPollConfig {
|
||||
if (!raw) {
|
||||
return { nodeName: null, pollInterval: DEFAULT_POLL_INTERVAL_SECONDS };
|
||||
}
|
||||
if (typeof raw === "object") {
|
||||
return {
|
||||
nodeName: raw.nodeName ?? null,
|
||||
pollInterval: raw.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS,
|
||||
enabledCards: raw.enabledCards,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as ProxmoxStatsPollConfig;
|
||||
return {
|
||||
nodeName: parsed.nodeName ?? null,
|
||||
pollInterval: parsed.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS,
|
||||
enabledCards: parsed.enabledCards,
|
||||
};
|
||||
} catch {
|
||||
return { nodeName: null, pollInterval: DEFAULT_POLL_INTERVAL_SECONDS };
|
||||
}
|
||||
}
|
||||
|
||||
interface HostPollingEntry<THost extends ProxmoxStatsPollableHost> {
|
||||
host: THost;
|
||||
timer?: NodeJS.Timeout;
|
||||
viewerUserId?: string;
|
||||
}
|
||||
|
||||
interface ViewerDetail {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
hostId: number;
|
||||
lastHeartbeat: number;
|
||||
}
|
||||
|
||||
interface CachedSnapshot {
|
||||
data: ProxmoxStatsSnapshot;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ErrorSnapshot {
|
||||
error: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export class ProxmoxPollingManager<
|
||||
THost extends ProxmoxStatsPollableHost = ProxmoxStatsPollableHost,
|
||||
> {
|
||||
private pollingConfigs = new Map<number, HostPollingEntry<THost>>();
|
||||
private snapshotStore = new Map<number, CachedSnapshot>();
|
||||
private errorStore = new Map<number, ErrorSnapshot>();
|
||||
private activeViewers = new Map<number, Set<string>>();
|
||||
private viewerDetails = new Map<string, ViewerDetail>();
|
||||
private inFlight = new Set<number>();
|
||||
private viewerCleanupInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private readonly deps: {
|
||||
fetchHostById: (
|
||||
hostId: number,
|
||||
userId: string,
|
||||
) => Promise<THost | undefined>;
|
||||
withSshConnection: <T>(
|
||||
host: THost,
|
||||
fn: (client: Client) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
historyRepository: ProxmoxNodeHistoryRepository;
|
||||
historyEnabled?: () => boolean;
|
||||
},
|
||||
) {
|
||||
this.viewerCleanupInterval = setInterval(() => {
|
||||
this.cleanupInactiveViewers();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
private intervalWithJitter(intervalMs: number, hostId: number): number {
|
||||
const spread = Math.min(intervalMs * 0.2, 15_000);
|
||||
const jitter = (hostId * 1103515245) % Math.max(1, Math.floor(spread));
|
||||
return intervalMs + jitter;
|
||||
}
|
||||
|
||||
// Host Metrics' retention setting belongs to that plugin, so node history
|
||||
// keeps a fixed week.
|
||||
private readonly retentionDays = 7;
|
||||
|
||||
private async pollHostStats(
|
||||
host: THost,
|
||||
viewerUserId?: string,
|
||||
): Promise<void> {
|
||||
if (this.inFlight.has(host.id)) return;
|
||||
this.inFlight.add(host.id);
|
||||
|
||||
try {
|
||||
await proxmoxStatsPollLimiter.run(async () => {
|
||||
const userId = viewerUserId || host.userId;
|
||||
const refreshed =
|
||||
(await this.deps.fetchHostById(host.id, userId)) ?? host;
|
||||
const config = parseProxmoxStatsConfig(refreshed.proxmoxStatsConfig);
|
||||
|
||||
try {
|
||||
const snapshot = await this.deps.withSshConnection(
|
||||
refreshed,
|
||||
(client) => collectProxmoxStats(client, config.nodeName),
|
||||
);
|
||||
|
||||
this.snapshotStore.set(host.id, {
|
||||
data: snapshot,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.errorStore.delete(host.id);
|
||||
|
||||
if (this.deps.historyEnabled?.() ?? true) {
|
||||
await this.insertHistory(host.id, snapshot);
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
this.errorStore.set(host.id, {
|
||||
error: message,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
pluginCtx().log.warn(
|
||||
`Proxmox stats poll failed for host ${host.id}: ${message}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.inFlight.delete(host.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async insertHistory(
|
||||
hostId: number,
|
||||
snapshot: ProxmoxStatsSnapshot,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const iface = snapshot.network?.interfaces?.[0];
|
||||
const rxRaw = iface?.rxBytes ? parseInt(iface.rxBytes, 10) : null;
|
||||
const txRaw = iface?.txBytes ? parseInt(iface.txBytes, 10) : null;
|
||||
|
||||
await this.deps.historyRepository.create({
|
||||
hostId,
|
||||
cpuPercent: snapshot.node?.cpu?.percent ?? null,
|
||||
memPercent: snapshot.node?.memory?.percent ?? null,
|
||||
diskPercent: snapshot.node?.disk?.percent ?? null,
|
||||
netRxBytes: rxRaw !== null && !isNaN(rxRaw) ? rxRaw : null,
|
||||
netTxBytes: txRaw !== null && !isNaN(txRaw) ? txRaw : null,
|
||||
});
|
||||
|
||||
await this.deps.historyRepository.pruneOlderThan(
|
||||
hostId,
|
||||
this.retentionDays,
|
||||
);
|
||||
} catch (err) {
|
||||
pluginCtx().log.warn(
|
||||
`Failed to write proxmox node history for host ${hostId}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private startPollingForHost(host: THost, viewerUserId?: string): void {
|
||||
const existing = this.pollingConfigs.get(host.id);
|
||||
if (existing?.timer) {
|
||||
clearInterval(existing.timer);
|
||||
}
|
||||
|
||||
const config = parseProxmoxStatsConfig(host.proxmoxStatsConfig);
|
||||
const intervalMs = this.intervalWithJitter(
|
||||
(config.pollInterval ?? DEFAULT_POLL_INTERVAL_SECONDS) * 1000,
|
||||
host.id,
|
||||
);
|
||||
|
||||
void this.pollHostStats(host, viewerUserId);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const latest = this.pollingConfigs.get(host.id);
|
||||
if (latest) {
|
||||
void this.pollHostStats(latest.host, latest.viewerUserId);
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
this.pollingConfigs.set(host.id, { host, timer, viewerUserId });
|
||||
}
|
||||
|
||||
private stopPollingForHost(hostId: number): void {
|
||||
const config = this.pollingConfigs.get(hostId);
|
||||
if (config?.timer) {
|
||||
clearInterval(config.timer);
|
||||
}
|
||||
this.pollingConfigs.delete(hostId);
|
||||
}
|
||||
|
||||
getStats(
|
||||
hostId: number,
|
||||
): { data: ProxmoxStatsSnapshot; timestamp: number } | undefined {
|
||||
return this.snapshotStore.get(hostId);
|
||||
}
|
||||
|
||||
getError(hostId: number): ErrorSnapshot | undefined {
|
||||
return this.errorStore.get(hostId);
|
||||
}
|
||||
|
||||
async ensurePolling(host: THost, viewerUserId?: string): Promise<void> {
|
||||
if (!this.pollingConfigs.has(host.id)) {
|
||||
this.startPollingForHost(host, viewerUserId);
|
||||
}
|
||||
if (!this.snapshotStore.has(host.id) && !this.inFlight.has(host.id)) {
|
||||
await this.pollHostStats(host, viewerUserId);
|
||||
}
|
||||
}
|
||||
|
||||
registerViewer = (
|
||||
hostId: number,
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
): void => {
|
||||
if (!this.activeViewers.has(hostId)) {
|
||||
this.activeViewers.set(hostId, new Set());
|
||||
}
|
||||
this.activeViewers.get(hostId)!.add(sessionId);
|
||||
|
||||
this.viewerDetails.set(sessionId, {
|
||||
sessionId,
|
||||
userId,
|
||||
hostId,
|
||||
lastHeartbeat: Date.now(),
|
||||
});
|
||||
|
||||
if (this.activeViewers.get(hostId)!.size === 1) {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
const host = await this.deps.fetchHostById(hostId, userId);
|
||||
if (host) {
|
||||
this.startPollingForHost(host, userId);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
pluginCtx().log.warn(
|
||||
`Proxmox stats startPollingForHost rejected (non-fatal) for host ${hostId}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
unregisterViewer = (hostId: number, sessionId: string): void => {
|
||||
const viewers = this.activeViewers.get(hostId);
|
||||
if (viewers) {
|
||||
viewers.delete(sessionId);
|
||||
if (viewers.size === 0) {
|
||||
this.activeViewers.delete(hostId);
|
||||
this.stopPollingForHost(hostId);
|
||||
}
|
||||
}
|
||||
this.viewerDetails.delete(sessionId);
|
||||
};
|
||||
|
||||
updateHeartbeat(sessionId: string): boolean {
|
||||
const viewer = this.viewerDetails.get(sessionId);
|
||||
if (viewer) {
|
||||
viewer.lastHeartbeat = Date.now();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private cleanupInactiveViewers(): void {
|
||||
const now = Date.now();
|
||||
const maxInactivity = 120000;
|
||||
|
||||
for (const [sessionId, viewer] of this.viewerDetails.entries()) {
|
||||
if (now - viewer.lastHeartbeat > maxInactivity) {
|
||||
this.unregisterViewer(viewer.hostId, sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
clearInterval(this.viewerCleanupInterval);
|
||||
for (const hostId of this.pollingConfigs.keys()) {
|
||||
this.stopPollingForHost(hostId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
|
||||
export interface ProxmoxClusterNodeEntry {
|
||||
name: string;
|
||||
online: boolean;
|
||||
local: boolean;
|
||||
ip: string | null;
|
||||
}
|
||||
|
||||
export type ProxmoxClusterHealthResult =
|
||||
| { clustered: false }
|
||||
| {
|
||||
clustered: true;
|
||||
quorate: boolean;
|
||||
clusterName: string | null;
|
||||
nodes: ProxmoxClusterNodeEntry[];
|
||||
};
|
||||
|
||||
const EMPTY_RESULT: ProxmoxClusterHealthResult = { clustered: false };
|
||||
|
||||
export async function collectProxmoxClusterHealth(
|
||||
client: Client,
|
||||
): Promise<ProxmoxClusterHealthResult> {
|
||||
try {
|
||||
const { stdout, code } = await execCommand(
|
||||
client,
|
||||
"pvesh get /cluster/status --output-format json",
|
||||
15000,
|
||||
);
|
||||
if (code !== 0) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const data = JSON.parse(stdout);
|
||||
if (!Array.isArray(data)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const entries = data as Array<Record<string, unknown>>;
|
||||
const clusterEntry = entries.find((e) => e.type === "cluster");
|
||||
if (!clusterEntry) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const nodes: ProxmoxClusterNodeEntry[] = entries
|
||||
.filter((e) => e.type === "node")
|
||||
.map((e) => ({
|
||||
name: typeof e.name === "string" ? e.name : "",
|
||||
online: e.online === 1 || e.online === true,
|
||||
local: e.local === 1 || e.local === true,
|
||||
ip: typeof e.ip === "string" && e.ip ? e.ip : null,
|
||||
}));
|
||||
|
||||
return {
|
||||
clustered: true,
|
||||
quorate: clusterEntry.quorate === 1 || clusterEntry.quorate === true,
|
||||
clusterName:
|
||||
typeof clusterEntry.name === "string" && clusterEntry.name
|
||||
? clusterEntry.name
|
||||
: null,
|
||||
nodes,
|
||||
};
|
||||
} catch {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { isSafeNodeName } from "../proxmox-shared.js";
|
||||
import {
|
||||
collectProxmoxNodeStatus,
|
||||
type ProxmoxNodeStatusResult,
|
||||
} from "./node-status-collector.js";
|
||||
import {
|
||||
collectProxmoxNodeNetwork,
|
||||
type ProxmoxNodeNetworkResult,
|
||||
} from "./node-network-collector.js";
|
||||
import {
|
||||
collectProxmoxGuestsSummary,
|
||||
type ProxmoxGuestsSummaryResult,
|
||||
} from "./guests-collector.js";
|
||||
import {
|
||||
collectProxmoxStorage,
|
||||
type ProxmoxStorageResult,
|
||||
} from "./storage-collector.js";
|
||||
import {
|
||||
collectProxmoxClusterHealth,
|
||||
type ProxmoxClusterHealthResult,
|
||||
} from "./cluster-health-collector.js";
|
||||
|
||||
export interface ProxmoxStatsSnapshot {
|
||||
node: ProxmoxNodeStatusResult;
|
||||
network: ProxmoxNodeNetworkResult;
|
||||
guests: ProxmoxGuestsSummaryResult;
|
||||
storage: ProxmoxStorageResult;
|
||||
cluster: ProxmoxClusterHealthResult;
|
||||
lastChecked: string;
|
||||
}
|
||||
|
||||
async function resolveNodeName(
|
||||
client: Client,
|
||||
configuredNodeName: string | null | undefined,
|
||||
): Promise<string> {
|
||||
if (configuredNodeName) {
|
||||
// An explicitly configured name is validated and used as-is - an unsafe
|
||||
// value is rejected outright rather than silently falling back to
|
||||
// auto-detection, which would mask a misconfigured (or malicious) override.
|
||||
if (!isSafeNodeName(configuredNodeName)) {
|
||||
throw new Error("Unable to determine a valid Proxmox node name");
|
||||
}
|
||||
return configuredNodeName;
|
||||
}
|
||||
|
||||
const { stdout } = await execCommand(client, "hostname", 10000);
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
export async function collectProxmoxStats(
|
||||
client: Client,
|
||||
configuredNodeName: string | null | undefined,
|
||||
): Promise<ProxmoxStatsSnapshot> {
|
||||
const pveshCheck = await execCommand(
|
||||
client,
|
||||
"command -v pvesh >/dev/null 2>&1 && echo ok || echo missing",
|
||||
10000,
|
||||
);
|
||||
if (pveshCheck.stdout.trim() !== "ok") {
|
||||
throw new Error("pvesh not found — is this a Proxmox node?");
|
||||
}
|
||||
|
||||
const nodeName = await resolveNodeName(client, configuredNodeName);
|
||||
if (!isSafeNodeName(nodeName)) {
|
||||
throw new Error("Unable to determine a valid Proxmox node name");
|
||||
}
|
||||
|
||||
const [node, network, guests, storage, cluster] = await Promise.all([
|
||||
collectProxmoxNodeStatus(client, nodeName),
|
||||
collectProxmoxNodeNetwork(client, nodeName),
|
||||
collectProxmoxGuestsSummary(client, nodeName),
|
||||
collectProxmoxStorage(client, nodeName),
|
||||
collectProxmoxClusterHealth(client),
|
||||
]);
|
||||
|
||||
return {
|
||||
node,
|
||||
network,
|
||||
guests,
|
||||
storage,
|
||||
cluster,
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { Client, ClientChannel } from "ssh2";
|
||||
|
||||
/** Runs a command over an already-open ssh2 client and collects its output. */
|
||||
export function execCommand(
|
||||
client: Client,
|
||||
command: string,
|
||||
timeoutMs = 30000,
|
||||
): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number | null;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let stream: ClientChannel | null = null;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(new Error(`Command timeout after ${timeoutMs}ms: ${command}`));
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
if (stream) {
|
||||
try {
|
||||
stream.removeAllListeners();
|
||||
if (stream.stderr) {
|
||||
stream.stderr.removeAllListeners();
|
||||
}
|
||||
stream.destroy();
|
||||
} catch {
|
||||
// expected - cleanup errors ignored
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
client.exec(command, { pty: false }, (err, _stream) => {
|
||||
if (err) {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
stream = _stream;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let exitCode: number | null = null;
|
||||
|
||||
stream
|
||||
.on("close", (code: number | undefined) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
exitCode = typeof code === "number" ? code : null;
|
||||
cleanup();
|
||||
resolve({ stdout, stderr, code: exitCode });
|
||||
}
|
||||
})
|
||||
.on("data", (data: Buffer) => {
|
||||
stdout += data.toString("utf8");
|
||||
})
|
||||
.on("error", (streamErr: Error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(streamErr);
|
||||
}
|
||||
});
|
||||
|
||||
if (stream.stderr) {
|
||||
stream.stderr
|
||||
.on("data", (data: Buffer) => {
|
||||
stderr += data.toString("utf8");
|
||||
})
|
||||
.on("error", (stderrErr: Error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(stderrErr);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function toFixedNum(
|
||||
n: number | null | undefined,
|
||||
digits = 2,
|
||||
): number | null {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand, toFixedNum } from "./common-utils.js";
|
||||
import { isSafeNodeName } from "../proxmox-shared.js";
|
||||
|
||||
export interface ProxmoxGuestSummaryEntry {
|
||||
vmid: number;
|
||||
name: string;
|
||||
type: "qemu" | "lxc";
|
||||
status: string;
|
||||
cpuPercent: number | null;
|
||||
memPercent: number | null;
|
||||
memUsedGiB: number | null;
|
||||
memTotalGiB: number | null;
|
||||
diskPercent: number | null;
|
||||
diskUsedGiB: number | null;
|
||||
diskTotalGiB: number | null;
|
||||
uptimeSeconds: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxGuestsSummaryResult {
|
||||
guests: ProxmoxGuestSummaryEntry[];
|
||||
counts: { running: number; stopped: number; total: number };
|
||||
}
|
||||
|
||||
const EMPTY_RESULT: ProxmoxGuestsSummaryResult = {
|
||||
guests: [],
|
||||
counts: { running: 0, stopped: 0, total: 0 },
|
||||
};
|
||||
|
||||
function bytesToGiB(bytes: number): number {
|
||||
return bytes / (1024 * 1024 * 1024);
|
||||
}
|
||||
|
||||
export async function collectProxmoxGuestsSummary(
|
||||
client: Client,
|
||||
nodeName: string,
|
||||
): Promise<ProxmoxGuestsSummaryResult> {
|
||||
if (!isSafeNodeName(nodeName)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, code } = await execCommand(
|
||||
client,
|
||||
"pvesh get /cluster/resources --output-format json",
|
||||
25000,
|
||||
);
|
||||
if (code !== 0) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const resources = JSON.parse(stdout);
|
||||
if (!Array.isArray(resources)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const guests: ProxmoxGuestSummaryEntry[] = [];
|
||||
for (const r of resources as Array<Record<string, unknown>>) {
|
||||
const type = r.type;
|
||||
if (type !== "qemu" && type !== "lxc") continue;
|
||||
if (r.node !== nodeName) continue;
|
||||
if (r.template === true || r.template === 1) continue;
|
||||
|
||||
const cpuFraction = typeof r.cpu === "number" ? r.cpu : null;
|
||||
const mem = typeof r.mem === "number" ? r.mem : null;
|
||||
const maxmem = typeof r.maxmem === "number" ? r.maxmem : null;
|
||||
const memPercent =
|
||||
mem !== null && maxmem !== null && maxmem > 0
|
||||
? Math.max(0, Math.min(100, (mem / maxmem) * 100))
|
||||
: null;
|
||||
|
||||
const disk = typeof r.disk === "number" ? r.disk : null;
|
||||
const maxdisk = typeof r.maxdisk === "number" ? r.maxdisk : null;
|
||||
// QEMU guests without a running agent report maxdisk: 0 - a false 0%
|
||||
// is worse than an honest "unknown", so treat that as no data at all.
|
||||
const hasDiskData = maxdisk !== null && maxdisk > 0;
|
||||
const diskPercent =
|
||||
hasDiskData && disk !== null
|
||||
? Math.max(0, Math.min(100, (disk / maxdisk) * 100))
|
||||
: null;
|
||||
|
||||
guests.push({
|
||||
vmid: typeof r.vmid === "number" ? r.vmid : Number(r.vmid),
|
||||
name:
|
||||
typeof r.name === "string" && r.name ? r.name : String(r.vmid ?? ""),
|
||||
type,
|
||||
status: typeof r.status === "string" ? r.status : "unknown",
|
||||
cpuPercent: toFixedNum(
|
||||
cpuFraction !== null ? cpuFraction * 100 : null,
|
||||
0,
|
||||
),
|
||||
memPercent: toFixedNum(memPercent, 0),
|
||||
memUsedGiB: mem !== null ? toFixedNum(bytesToGiB(mem), 2) : null,
|
||||
memTotalGiB: maxmem !== null ? toFixedNum(bytesToGiB(maxmem), 2) : null,
|
||||
diskPercent: toFixedNum(diskPercent, 0),
|
||||
diskUsedGiB:
|
||||
hasDiskData && disk !== null ? toFixedNum(bytesToGiB(disk), 2) : null,
|
||||
diskTotalGiB: hasDiskData ? toFixedNum(bytesToGiB(maxdisk), 2) : null,
|
||||
uptimeSeconds: typeof r.uptime === "number" ? r.uptime : null,
|
||||
});
|
||||
}
|
||||
|
||||
const running = guests.filter((g) => g.status === "running").length;
|
||||
const stopped = guests.filter((g) => g.status !== "running").length;
|
||||
|
||||
return {
|
||||
guests,
|
||||
counts: { running, stopped, total: guests.length },
|
||||
};
|
||||
} catch {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand } from "./common-utils.js";
|
||||
import { isSafeNodeName } from "../proxmox-shared.js";
|
||||
|
||||
export interface ProxmoxNodeNetworkInterface {
|
||||
name: string;
|
||||
ip: string | null;
|
||||
state: string | null;
|
||||
rxBytes: string | null;
|
||||
txBytes: string | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxNodeNetworkResult {
|
||||
interfaces: ProxmoxNodeNetworkInterface[];
|
||||
}
|
||||
|
||||
const EMPTY_RESULT: ProxmoxNodeNetworkResult = { interfaces: [] };
|
||||
|
||||
async function collectFromProcNetDev(
|
||||
client: Client,
|
||||
): Promise<ProxmoxNodeNetworkResult> {
|
||||
const interfaces: ProxmoxNodeNetworkInterface[] = [];
|
||||
|
||||
try {
|
||||
const [addrOut, stateOut, procNetOut] = await Promise.all([
|
||||
execCommand(
|
||||
client,
|
||||
"ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'",
|
||||
),
|
||||
execCommand(
|
||||
client,
|
||||
"ip -o link show | awk '{gsub(/:/, \"\", $2); print $2,$9}'",
|
||||
),
|
||||
execCommand(client, "cat /proc/net/dev"),
|
||||
]);
|
||||
|
||||
const ifMap = new Map<
|
||||
string,
|
||||
{ ip: string | null; state: string | null }
|
||||
>();
|
||||
for (const line of addrOut.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const name = parts[0];
|
||||
const ip = parts[1].split("/")[0];
|
||||
if (!ifMap.has(name)) ifMap.set(name, { ip, state: null });
|
||||
}
|
||||
}
|
||||
for (const line of stateOut.stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length >= 2) {
|
||||
const existing = ifMap.get(parts[0]);
|
||||
if (existing) existing.state = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
const rxTxMap = new Map<string, { rx: string; tx: string }>();
|
||||
for (const line of procNetOut.stdout.split("\n").slice(2)) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 10) {
|
||||
const ifName = parts[0].replace(":", "");
|
||||
rxTxMap.set(ifName, { rx: parts[1], tx: parts[9] });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, data] of ifMap.entries()) {
|
||||
const rxTx = rxTxMap.get(name);
|
||||
interfaces.push({
|
||||
name,
|
||||
ip: data.ip,
|
||||
state: data.state,
|
||||
rxBytes: rxTx?.rx ?? null,
|
||||
txBytes: rxTx?.tx ?? null,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
return { interfaces };
|
||||
}
|
||||
|
||||
export async function collectProxmoxNodeNetwork(
|
||||
client: Client,
|
||||
nodeName: string,
|
||||
): Promise<ProxmoxNodeNetworkResult> {
|
||||
if (!isSafeNodeName(nodeName)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, code } = await execCommand(
|
||||
client,
|
||||
`pvesh get /nodes/${nodeName}/netstat --output-format json`,
|
||||
15000,
|
||||
);
|
||||
if (code !== 0) {
|
||||
return collectFromProcNetDev(client);
|
||||
}
|
||||
|
||||
const data = JSON.parse(stdout);
|
||||
if (!Array.isArray(data)) {
|
||||
return collectFromProcNetDev(client);
|
||||
}
|
||||
|
||||
const interfaces: ProxmoxNodeNetworkInterface[] = data
|
||||
.filter(
|
||||
(entry): entry is Record<string, unknown> =>
|
||||
!!entry && typeof entry === "object",
|
||||
)
|
||||
.map((entry) => ({
|
||||
name:
|
||||
typeof entry.dev === "string" ? entry.dev : String(entry.dev ?? ""),
|
||||
ip: null,
|
||||
state: null,
|
||||
rxBytes:
|
||||
typeof entry.in === "number"
|
||||
? String(entry.in)
|
||||
: typeof entry.received === "number"
|
||||
? String(entry.received)
|
||||
: null,
|
||||
txBytes:
|
||||
typeof entry.out === "number"
|
||||
? String(entry.out)
|
||||
: typeof entry.transmitted === "number"
|
||||
? String(entry.transmitted)
|
||||
: null,
|
||||
}))
|
||||
.filter((iface) => iface.name && iface.name !== "lo");
|
||||
|
||||
if (interfaces.length === 0) {
|
||||
return collectFromProcNetDev(client);
|
||||
}
|
||||
|
||||
return { interfaces };
|
||||
} catch {
|
||||
return collectFromProcNetDev(client);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand, toFixedNum } from "./common-utils.js";
|
||||
import { isSafeNodeName } from "../proxmox-shared.js";
|
||||
|
||||
export interface ProxmoxNodeStatusResult {
|
||||
cpu: {
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
load: [number, number, number] | null;
|
||||
};
|
||||
memory: {
|
||||
percent: number | null;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
};
|
||||
disk: {
|
||||
percent: number | null;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
};
|
||||
uptime: {
|
||||
seconds: number | null;
|
||||
formatted: string | null;
|
||||
};
|
||||
system: {
|
||||
hostname: string | null;
|
||||
kernel: string | null;
|
||||
pveVersion: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY_RESULT: ProxmoxNodeStatusResult = {
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedGiB: null, totalGiB: null },
|
||||
uptime: { seconds: null, formatted: null },
|
||||
system: { hostname: null, kernel: null, pveVersion: null },
|
||||
};
|
||||
|
||||
function bytesToGiB(bytes: number): number {
|
||||
return bytes / (1024 * 1024 * 1024);
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
export async function collectProxmoxNodeStatus(
|
||||
client: Client,
|
||||
nodeName: string,
|
||||
): Promise<ProxmoxNodeStatusResult> {
|
||||
if (!isSafeNodeName(nodeName)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, code } = await execCommand(
|
||||
client,
|
||||
`pvesh get /nodes/${nodeName}/status --output-format json`,
|
||||
25000,
|
||||
);
|
||||
if (code !== 0) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const data = JSON.parse(stdout) as Record<string, unknown>;
|
||||
|
||||
const cpuFraction = typeof data.cpu === "number" ? data.cpu : null;
|
||||
const cpuinfo = (data.cpuinfo as Record<string, unknown>) || {};
|
||||
const cores = typeof cpuinfo.cores === "number" ? cpuinfo.cores : null;
|
||||
const loadavgRaw = data.loadavg;
|
||||
let load: [number, number, number] | null = null;
|
||||
if (Array.isArray(loadavgRaw) && loadavgRaw.length >= 3) {
|
||||
const parsed = loadavgRaw
|
||||
.slice(0, 3)
|
||||
.map((v) => Number(v))
|
||||
.map((v) => (Number.isFinite(v) ? v : 0));
|
||||
load = parsed as [number, number, number];
|
||||
}
|
||||
|
||||
const memory = (data.memory as Record<string, unknown>) || {};
|
||||
const memUsed = typeof memory.used === "number" ? memory.used : null;
|
||||
const memTotal = typeof memory.total === "number" ? memory.total : null;
|
||||
const memPercent =
|
||||
memUsed !== null && memTotal !== null && memTotal > 0
|
||||
? Math.max(0, Math.min(100, (memUsed / memTotal) * 100))
|
||||
: null;
|
||||
|
||||
const rootfs = (data.rootfs as Record<string, unknown>) || {};
|
||||
const diskUsed = typeof rootfs.used === "number" ? rootfs.used : null;
|
||||
const diskTotal = typeof rootfs.total === "number" ? rootfs.total : null;
|
||||
const diskPercent =
|
||||
diskUsed !== null && diskTotal !== null && diskTotal > 0
|
||||
? Math.max(0, Math.min(100, (diskUsed / diskTotal) * 100))
|
||||
: null;
|
||||
|
||||
const uptimeSeconds = typeof data.uptime === "number" ? data.uptime : null;
|
||||
|
||||
return {
|
||||
cpu: {
|
||||
percent: toFixedNum(cpuFraction !== null ? cpuFraction * 100 : null, 0),
|
||||
cores,
|
||||
load,
|
||||
},
|
||||
memory: {
|
||||
percent: toFixedNum(memPercent, 0),
|
||||
usedGiB: memUsed !== null ? toFixedNum(bytesToGiB(memUsed), 2) : null,
|
||||
totalGiB:
|
||||
memTotal !== null ? toFixedNum(bytesToGiB(memTotal), 2) : null,
|
||||
},
|
||||
disk: {
|
||||
percent: toFixedNum(diskPercent, 0),
|
||||
usedGiB: diskUsed !== null ? toFixedNum(bytesToGiB(diskUsed), 2) : null,
|
||||
totalGiB:
|
||||
diskTotal !== null ? toFixedNum(bytesToGiB(diskTotal), 2) : null,
|
||||
},
|
||||
uptime: {
|
||||
seconds: uptimeSeconds,
|
||||
formatted: uptimeSeconds !== null ? formatUptime(uptimeSeconds) : null,
|
||||
},
|
||||
system: {
|
||||
hostname:
|
||||
typeof data.hostname === "string" && data.hostname
|
||||
? data.hostname
|
||||
: null,
|
||||
kernel:
|
||||
typeof data.kversion === "string" && data.kversion
|
||||
? data.kversion
|
||||
: null,
|
||||
pveVersion:
|
||||
typeof data.pveversion === "string" && data.pveversion
|
||||
? data.pveversion
|
||||
: null,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Client } from "ssh2";
|
||||
import { execCommand, toFixedNum } from "./common-utils.js";
|
||||
import { isSafeNodeName } from "../proxmox-shared.js";
|
||||
|
||||
export interface ProxmoxStoragePoolEntry {
|
||||
name: string;
|
||||
type: string;
|
||||
active: boolean;
|
||||
enabled: boolean;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
availGiB: number | null;
|
||||
percent: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxStorageResult {
|
||||
pools: ProxmoxStoragePoolEntry[];
|
||||
}
|
||||
|
||||
const EMPTY_RESULT: ProxmoxStorageResult = { pools: [] };
|
||||
|
||||
function bytesToGiB(bytes: number): number {
|
||||
return bytes / (1024 * 1024 * 1024);
|
||||
}
|
||||
|
||||
export async function collectProxmoxStorage(
|
||||
client: Client,
|
||||
nodeName: string,
|
||||
): Promise<ProxmoxStorageResult> {
|
||||
if (!isSafeNodeName(nodeName)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout, code } = await execCommand(
|
||||
client,
|
||||
`pvesh get /nodes/${nodeName}/storage --output-format json`,
|
||||
25000,
|
||||
);
|
||||
if (code !== 0) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const data = JSON.parse(stdout);
|
||||
if (!Array.isArray(data)) {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
|
||||
const pools: ProxmoxStoragePoolEntry[] = (
|
||||
data as Array<Record<string, unknown>>
|
||||
).map((entry) => {
|
||||
const used = typeof entry.used === "number" ? entry.used : null;
|
||||
const total = typeof entry.total === "number" ? entry.total : null;
|
||||
const avail = typeof entry.avail === "number" ? entry.avail : null;
|
||||
const percent =
|
||||
used !== null && total !== null && total > 0
|
||||
? Math.max(0, Math.min(100, (used / total) * 100))
|
||||
: null;
|
||||
|
||||
return {
|
||||
name: typeof entry.storage === "string" ? entry.storage : "",
|
||||
type: typeof entry.type === "string" ? entry.type : "unknown",
|
||||
active: entry.active === 1 || entry.active === true,
|
||||
enabled: entry.enabled === 1 || entry.enabled === true,
|
||||
usedGiB: used !== null ? toFixedNum(bytesToGiB(used), 2) : null,
|
||||
totalGiB: total !== null ? toFixedNum(bytesToGiB(total), 2) : null,
|
||||
availGiB: avail !== null ? toFixedNum(bytesToGiB(avail), 2) : null,
|
||||
percent: toFixedNum(percent, 0),
|
||||
};
|
||||
});
|
||||
|
||||
return { pools };
|
||||
} catch {
|
||||
return EMPTY_RESULT;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
import type { Client } from "ssh2";
|
||||
import type {
|
||||
PluginSsh,
|
||||
PluginSshConnection,
|
||||
PluginSshConnectOptions,
|
||||
PluginSshHost,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
|
||||
let current: PluginSsh | null = null;
|
||||
|
||||
/** Set in activate, cleared on deactivate. */
|
||||
export function setPluginSsh(ssh: PluginSsh | null): void {
|
||||
current = ssh;
|
||||
}
|
||||
|
||||
export function pluginSsh(): PluginSsh {
|
||||
if (!current) throw new Error("The plugin is not active");
|
||||
return current;
|
||||
}
|
||||
|
||||
/** withConnection with ssh2's Client type filled in. */
|
||||
export function withSshConnection<T>(
|
||||
host: number | PluginSshHost,
|
||||
options: PluginSshConnectOptions & { pool: string },
|
||||
fn: (client: Client) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return pluginSsh().withConnection<T, Client>(host, options, fn);
|
||||
}
|
||||
|
||||
/** connect with ssh2's Client type filled in. */
|
||||
export function connectSsh(
|
||||
host: number | PluginSshHost,
|
||||
options?: PluginSshConnectOptions,
|
||||
): Promise<PluginSshConnection<Client>> {
|
||||
return pluginSsh().connect<Client>(host, options);
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Proxmox node stats: polling, viewer sessions and their routes.
|
||||
*
|
||||
* Moved here from host-metrics, which only ever hosted this because the
|
||||
* plugin split predates B5. host-metrics must not import proxmox code, so
|
||||
* this plugin owns its own polling manager, SSH connections and routes.
|
||||
*/
|
||||
|
||||
import type { Router } from "express";
|
||||
import express from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { pluginCtx } from "./plugin-ctx.js";
|
||||
import {
|
||||
ProxmoxPollingManager,
|
||||
parseProxmoxStatsConfig,
|
||||
type ProxmoxStatsPollableHost,
|
||||
} from "./proxmox-stats-polling.js";
|
||||
import type { ProxmoxNodeHistoryRepository } from "./proxmox-node-history-repository.js";
|
||||
|
||||
const EMPTY_SNAPSHOT = {
|
||||
node: {
|
||||
cpu: { percent: null, cores: null, load: null },
|
||||
memory: { percent: null, usedGiB: null, totalGiB: null },
|
||||
disk: { percent: null, usedGiB: null, totalGiB: null },
|
||||
uptime: { seconds: null, formatted: null },
|
||||
system: { hostname: null, kernel: null, pveVersion: null },
|
||||
},
|
||||
network: { interfaces: [] },
|
||||
guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } },
|
||||
storage: { pools: [] },
|
||||
cluster: { clustered: false },
|
||||
lastChecked: new Date(0).toISOString(),
|
||||
};
|
||||
|
||||
type PollableHost = ProxmoxStatsPollableHost & { enableProxmoxStats: boolean };
|
||||
|
||||
let pollingManager: ProxmoxPollingManager<PollableHost> | undefined;
|
||||
let router: Router | undefined;
|
||||
|
||||
async function fetchHostForPolling(
|
||||
ctx: PluginContext,
|
||||
hostId: number,
|
||||
userId: string,
|
||||
): Promise<PollableHost | undefined> {
|
||||
const enabled = await ctx.asUser(userId, () =>
|
||||
ctx.settings.getHost<boolean>(hostId, "enableProxmoxStats"),
|
||||
);
|
||||
if (!enabled) return undefined;
|
||||
const host = await ctx.asUser(userId, () => ctx.hosts.get(hostId));
|
||||
if (!host) return undefined;
|
||||
const proxmoxStatsConfig = await ctx.asUser(userId, () =>
|
||||
ctx.settings.getHost(hostId, "proxmoxStatsConfig"),
|
||||
);
|
||||
return {
|
||||
id: host.id,
|
||||
userId: host.userId,
|
||||
proxmoxStatsConfig: proxmoxStatsConfig as string | undefined,
|
||||
enableProxmoxStats: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Called from activate(). Builds the polling manager and mounts its routes. */
|
||||
export function startProxmoxStatsService(
|
||||
ctx: PluginContext,
|
||||
mountOn: Router,
|
||||
historyRepository: ProxmoxNodeHistoryRepository,
|
||||
): void {
|
||||
pollingManager = new ProxmoxPollingManager<PollableHost>({
|
||||
fetchHostById: (hostId, userId) => fetchHostForPolling(ctx, hostId, userId),
|
||||
withSshConnection: (host, fn) =>
|
||||
ctx.ssh.withConnection(
|
||||
host.id,
|
||||
{
|
||||
pool: "proxmox-stats",
|
||||
purpose: "proxmox",
|
||||
overrides: { readyTimeout: 60000 },
|
||||
},
|
||||
fn,
|
||||
),
|
||||
historyRepository,
|
||||
});
|
||||
|
||||
router = express.Router();
|
||||
registerRoutes(router, pollingManager, historyRepository);
|
||||
mountOn.use("/stats", router);
|
||||
}
|
||||
|
||||
export function stopProxmoxStatsService(): void {
|
||||
pollingManager?.destroy();
|
||||
pollingManager = undefined;
|
||||
router = undefined;
|
||||
}
|
||||
|
||||
function registerRoutes(
|
||||
app: Router,
|
||||
manager: ProxmoxPollingManager<PollableHost>,
|
||||
historyRepository: ProxmoxNodeHistoryRepository,
|
||||
): void {
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/proxmox/stats/{id}:
|
||||
* get:
|
||||
* summary: Get cached Proxmox node stats for a host
|
||||
* description: Returns the most recently polled Proxmox stats snapshot for a host, or an empty skeleton if none has been collected yet.
|
||||
* tags: [Proxmox]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Proxmox stats snapshot.
|
||||
* 404:
|
||||
* description: Stats not available yet.
|
||||
*/
|
||||
app.get("/:id", async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const cached = manager.getStats(id);
|
||||
if (!cached) {
|
||||
const errorState = manager.getError(id);
|
||||
return res.status(404).json({
|
||||
error: errorState?.error || "Stats not available",
|
||||
...EMPTY_SNAPSHOT,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
...cached.data,
|
||||
lastChecked: new Date(cached.timestamp).toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/proxmox/stats/start/{id}:
|
||||
* post:
|
||||
* summary: Start Proxmox stats collection
|
||||
* description: Registers a viewer and starts (or reuses) polling for a host's Proxmox node stats.
|
||||
* tags: [Proxmox]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Polling started, snapshot returned if already available.
|
||||
* 403:
|
||||
* description: Proxmox stats is not enabled for this host.
|
||||
* 404:
|
||||
* description: Host not found.
|
||||
*/
|
||||
app.post("/start/:id", async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const userId = pluginCtx().currentActor()!;
|
||||
|
||||
try {
|
||||
const host = await fetchHostForPolling(pluginCtx(), id, userId);
|
||||
if (!host) {
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: "Proxmox stats is not enabled for this host" });
|
||||
}
|
||||
|
||||
const viewerSessionId = `proxmox-viewer-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
manager.registerViewer(id, viewerSessionId, userId);
|
||||
await manager.ensurePolling(host, userId);
|
||||
|
||||
const cached = manager.getStats(id);
|
||||
if (cached) {
|
||||
return res.json({
|
||||
success: true,
|
||||
viewerSessionId,
|
||||
...cached.data,
|
||||
lastChecked: new Date(cached.timestamp).toISOString(),
|
||||
});
|
||||
}
|
||||
const errorState = manager.getError(id);
|
||||
if (errorState) {
|
||||
return res.json({
|
||||
success: true,
|
||||
viewerSessionId,
|
||||
status: "error",
|
||||
error: errorState.error,
|
||||
});
|
||||
}
|
||||
return res.json({ success: true, viewerSessionId, status: "collecting" });
|
||||
} catch (error) {
|
||||
pluginCtx().log.error(
|
||||
`Failed to start proxmox stats collection for host ${id}`,
|
||||
error as Error,
|
||||
);
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: "Failed to start proxmox stats collection" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/proxmox/stats/stop/{id}:
|
||||
* post:
|
||||
* summary: Stop Proxmox stats collection
|
||||
* description: Unregisters a viewer session for a host's Proxmox node stats polling.
|
||||
* tags: [Proxmox]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: false
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* viewerSessionId:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Polling stopped successfully.
|
||||
*/
|
||||
app.post("/stop/:id", (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { viewerSessionId } = req.body as { viewerSessionId?: string };
|
||||
if (viewerSessionId && typeof viewerSessionId === "string") {
|
||||
manager.unregisterViewer(id, viewerSessionId);
|
||||
}
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/proxmox/stats/heartbeat:
|
||||
* post:
|
||||
* summary: Update Proxmox stats viewer heartbeat
|
||||
* description: Keeps a Proxmox stats viewer session alive.
|
||||
* tags: [Proxmox]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* viewerSessionId:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Heartbeat updated.
|
||||
* 400:
|
||||
* description: Invalid viewerSessionId.
|
||||
* 404:
|
||||
* description: Viewer session not found.
|
||||
*/
|
||||
app.post("/heartbeat", (req, res) => {
|
||||
const { viewerSessionId } = req.body as { viewerSessionId?: string };
|
||||
if (!viewerSessionId || typeof viewerSessionId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid viewerSessionId" });
|
||||
}
|
||||
const ok = manager.updateHeartbeat(viewerSessionId);
|
||||
if (!ok) return res.status(404).json({ error: "Viewer session not found" });
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/proxmox/stats/history/{hostId}:
|
||||
* get:
|
||||
* summary: Get historical Proxmox node stats for a host
|
||||
* tags: [Proxmox]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: hostId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* - in: query
|
||||
* name: range
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [1h, 6h, 24h, 7d, 30d]
|
||||
* - in: query
|
||||
* name: from
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* - in: query
|
||||
* name: to
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Array of node history rows.
|
||||
* 400:
|
||||
* description: Invalid range or date.
|
||||
*/
|
||||
app.get("/history/:hostId", async (req, res) => {
|
||||
const hostId = Number(req.params.hostId);
|
||||
const { range, from, to } = req.query as Record<string, string | undefined>;
|
||||
|
||||
const RANGE_OFFSETS: Record<string, number> = {
|
||||
"1h": 1 * 60 * 60 * 1000,
|
||||
"6h": 6 * 60 * 60 * 1000,
|
||||
"24h": 24 * 60 * 60 * 1000,
|
||||
"7d": 7 * 24 * 60 * 60 * 1000,
|
||||
"30d": 30 * 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
let fromTs: string;
|
||||
let toTs = new Date().toISOString();
|
||||
if (range) {
|
||||
const offsetMs = RANGE_OFFSETS[range];
|
||||
if (!offsetMs) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Invalid range. Use 1h, 6h, 24h, 7d, or 30d" });
|
||||
}
|
||||
fromTs = new Date(Date.now() - offsetMs).toISOString();
|
||||
} else if (from && to) {
|
||||
const fromDate = new Date(from);
|
||||
const toDate = new Date(to);
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
|
||||
return res.status(400).json({ error: "Invalid from/to date format" });
|
||||
}
|
||||
fromTs = fromDate.toISOString();
|
||||
toTs = toDate.toISOString();
|
||||
} else {
|
||||
fromTs = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
try {
|
||||
const toSqlite = (iso: string) =>
|
||||
iso.replace("T", " ").replace(/\.\d{3}Z$/, "");
|
||||
const rows = (
|
||||
await historyRepository.listRange(
|
||||
hostId,
|
||||
toSqlite(fromTs),
|
||||
toSqlite(toTs),
|
||||
)
|
||||
).map((row) => ({
|
||||
ts: row.ts,
|
||||
cpu_percent: row.cpuPercent,
|
||||
mem_percent: row.memPercent,
|
||||
disk_percent: row.diskPercent,
|
||||
net_rx_bytes: row.netRxBytes,
|
||||
net_tx_bytes: row.netTxBytes,
|
||||
}));
|
||||
res.json({ rows, fromTs, toTs });
|
||||
} catch (error) {
|
||||
pluginCtx().log.error(
|
||||
`Failed to fetch proxmox stats history for host ${hostId}`,
|
||||
error as Error,
|
||||
);
|
||||
res.status(500).json({ error: "Failed to fetch proxmox stats history" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { parseProxmoxStatsConfig };
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
adoptLegacyTable,
|
||||
defineTable,
|
||||
id,
|
||||
integer,
|
||||
real,
|
||||
refHost,
|
||||
refUser,
|
||||
text,
|
||||
timestamp,
|
||||
} from "@termix/plugin-sdk/db";
|
||||
|
||||
/**
|
||||
* Per-host historical node stats samples (CPU, memory, disk, network),
|
||||
* polled on an interval while a viewer has the Proxmox stats tab open.
|
||||
*
|
||||
* Adopted from core's proxmox_node_history, so the column and index names are
|
||||
* the legacy ones: the rename carries the existing rows across.
|
||||
*/
|
||||
export const proxmoxNodeHistory = adoptLegacyTable(
|
||||
"proxmox_node_history",
|
||||
defineTable("node_history", {
|
||||
id: id(),
|
||||
hostId: refHost(),
|
||||
ts: timestamp().notNull().defaultNow(),
|
||||
cpuPercent: real(),
|
||||
memPercent: real(),
|
||||
diskPercent: real(),
|
||||
netRxBytes: integer(),
|
||||
netTxBytes: integer(),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* One saved dashboard layout per user per host for the Proxmox stats tab.
|
||||
*
|
||||
* Adopted from core's proxmox_stats_preferences.
|
||||
*/
|
||||
export const proxmoxStatsPreferences = adoptLegacyTable(
|
||||
"proxmox_stats_preferences",
|
||||
defineTable(
|
||||
"stats_preferences",
|
||||
{
|
||||
id: id(),
|
||||
userId: refUser(),
|
||||
hostId: refHost(),
|
||||
// JSON-encoded layout. No secrets, so plain text.
|
||||
layout: text().notNull(),
|
||||
createdAt: timestamp().notNull().defaultNow(),
|
||||
updatedAt: timestamp().notNull().defaultNow(),
|
||||
},
|
||||
{
|
||||
indexes: [
|
||||
{
|
||||
name: "idx_proxmox_stats_prefs_user_host",
|
||||
columns: ["userId", "hostId"],
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const tables = [proxmoxNodeHistory, proxmoxStatsPreferences];
|
||||
@@ -0,0 +1,263 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSshAuthTypes, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Server } from "lucide-react";
|
||||
import {
|
||||
FakeSwitch,
|
||||
Input,
|
||||
SectionCard,
|
||||
Select2,
|
||||
SettingRow,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { listCredentials } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxHostConfig } from "./types";
|
||||
|
||||
interface HostProxmoxForm {
|
||||
enableProxmox: boolean;
|
||||
proxmoxConfig: ProxmoxHostConfig | null;
|
||||
}
|
||||
|
||||
type SetHostField = (key: string, value: unknown) => void;
|
||||
|
||||
export function HostProxmoxTab({
|
||||
form,
|
||||
setField,
|
||||
}: {
|
||||
form: HostProxmoxForm;
|
||||
setField: SetHostField;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const sshAuthTypes = useSshAuthTypes();
|
||||
const [credentials, setCredentials] = useState<
|
||||
{ id: number; name: string; username: string | null }[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
listCredentials()
|
||||
.then((list) =>
|
||||
setCredentials(
|
||||
list.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
username: c.username ?? null,
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const cfg = form.proxmoxConfig ?? {
|
||||
defaultCredentialId: null,
|
||||
defaultAuthType: "password",
|
||||
windowsPatterns: "win, windows",
|
||||
dockerPatterns: "docker",
|
||||
preferredPrefixes: "10., 192.168.",
|
||||
autoSyncEnabled: false,
|
||||
syncIntervalMinutes: 15,
|
||||
markMissingGuests: true,
|
||||
};
|
||||
const lastSyncResult = cfg.lastSyncResult;
|
||||
const lastSyncSummary = lastSyncResult
|
||||
? t("hosts.proxmoxLastSyncSummary", {
|
||||
created: lastSyncResult.created,
|
||||
updated: lastSyncResult.updated,
|
||||
markedMissing: lastSyncResult.markedMissing,
|
||||
skipped: lastSyncResult.skipped,
|
||||
})
|
||||
: t("hosts.proxmoxLastSyncNoResult");
|
||||
const lastSyncDescription = cfg.lastSyncAt
|
||||
? `${new Date(cfg.lastSyncAt).toLocaleString()}, ${lastSyncSummary}`
|
||||
: t("hosts.proxmoxLastSyncNever");
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("hosts.proxmoxIntegration")}
|
||||
icon={<Server className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-0 py-1">
|
||||
<SettingRow
|
||||
label={t("hosts.enableProxmox")}
|
||||
description={
|
||||
<>
|
||||
{t("hosts.enableProxmoxDesc")}{" "}
|
||||
<a
|
||||
href="https://docs.termix.site/features/files-and-hosts/proxmox-import"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-accent-brand hover:underline"
|
||||
>
|
||||
{t("hosts.docsLink")}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.enableProxmox}
|
||||
onChange={(v) => setField("enableProxmox", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{form.enableProxmox && (
|
||||
<>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxDefaultAuthType")}
|
||||
description={t("hosts.proxmoxDefaultAuthTypeDesc")}
|
||||
>
|
||||
<Select2
|
||||
value={cfg.defaultAuthType ?? "password"}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
defaultAuthType: e.target.value,
|
||||
})
|
||||
}
|
||||
className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{sshAuthTypes.types.map((option) => (
|
||||
<option key={option.type} value={option.type}>
|
||||
{t(option.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxDefaultCredential")}
|
||||
description={t("hosts.proxmoxDefaultCredentialDesc")}
|
||||
>
|
||||
<Select2
|
||||
value={cfg.defaultCredentialId ?? ""}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
defaultCredentialId: e.target.value
|
||||
? Number(e.target.value)
|
||||
: null,
|
||||
})
|
||||
}
|
||||
className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{t("hosts.none")}</option>
|
||||
{credentials.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.username ? `${c.name} (${c.username})` : c.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxWindowsDetection")}
|
||||
description={t("hosts.proxmoxWindowsDetectionDesc")}
|
||||
>
|
||||
<Input
|
||||
className="w-44 h-7 text-xs"
|
||||
value={cfg.windowsPatterns}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
windowsPatterns: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="win, windows"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxDockerDetection")}
|
||||
description={t("hosts.proxmoxDockerDetectionDesc")}
|
||||
>
|
||||
<Input
|
||||
className="w-44 h-7 text-xs"
|
||||
value={cfg.dockerPatterns}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
dockerPatterns: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="docker"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxPreferredRanges")}
|
||||
description={t("hosts.proxmoxPreferredRangesDesc")}
|
||||
>
|
||||
<Input
|
||||
className="w-44 h-7 text-xs"
|
||||
value={cfg.preferredPrefixes}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
preferredPrefixes: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="10., 192.168."
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxAutoSync")}
|
||||
description={t("hosts.proxmoxAutoSyncDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={cfg.autoSyncEnabled === true}
|
||||
onChange={(v) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
autoSyncEnabled: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxSyncInterval")}
|
||||
description={t("hosts.proxmoxSyncIntervalDesc")}
|
||||
>
|
||||
<Input
|
||||
className="w-24 h-7 text-xs"
|
||||
type="number"
|
||||
min={5}
|
||||
value={cfg.syncIntervalMinutes ?? 15}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
syncIntervalMinutes: Math.max(
|
||||
5,
|
||||
Number.parseInt(e.target.value || "15", 10),
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxMarkMissing")}
|
||||
description={t("hosts.proxmoxMarkMissingDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={cfg.markMissingGuests !== false}
|
||||
onChange={(v) =>
|
||||
setField("proxmoxConfig", {
|
||||
...cfg,
|
||||
markMissingGuests: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxLastSync")}
|
||||
description={lastSyncDescription}
|
||||
>
|
||||
<span
|
||||
className={`text-xs ${
|
||||
cfg.lastSyncStatus === "error"
|
||||
? "text-destructive"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
title={cfg.lastSyncError ?? undefined}
|
||||
>
|
||||
{cfg.lastSyncStatus
|
||||
? t(`hosts.proxmoxLastSyncStatus.${cfg.lastSyncStatus}`)
|
||||
: t("hosts.proxmoxLastSyncStatus.pending")}
|
||||
</span>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
notifyHostsChanged,
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Server, RefreshCw, CheckSquare, Square, Download } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { Select2 } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
discoverProxmoxGuestsStream,
|
||||
importProxmoxHosts,
|
||||
listHosts,
|
||||
} from "./proxmox-api";
|
||||
import type { PluginHostRecord } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxGuest } from "./types";
|
||||
import { resolveProxmoxImportAuth } from "./proxmox-import-auth";
|
||||
|
||||
interface ProxmoxDiscoverDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
hosts: PluginHostRecord[];
|
||||
onHostsChanged: (hosts: PluginHostRecord[]) => void;
|
||||
/** Pre-select a specific host and skip the host picker */
|
||||
preselectedHostId?: number;
|
||||
/** Credential to use for imported hosts */
|
||||
defaultCredentialId?: number | null;
|
||||
/** Auth type to use for imported hosts */
|
||||
defaultAuthType?: string;
|
||||
/** Username from the default credential */
|
||||
defaultUsername?: string;
|
||||
}
|
||||
|
||||
export function ProxmoxDiscoverDialog({
|
||||
open,
|
||||
onClose,
|
||||
hosts,
|
||||
onHostsChanged,
|
||||
preselectedHostId,
|
||||
defaultCredentialId,
|
||||
defaultAuthType,
|
||||
defaultUsername,
|
||||
}: ProxmoxDiscoverDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedHostId, setSelectedHostId] = useState<string>(
|
||||
preselectedHostId ? String(preselectedHostId) : "",
|
||||
);
|
||||
const [discovering, setDiscovering] = useState(false);
|
||||
const [progress, setProgress] = useState<{
|
||||
done: number;
|
||||
total: number;
|
||||
} | null>(null);
|
||||
const streamCloseRef = useRef<(() => void) | null>(null);
|
||||
const [guests, setGuests] = useState<ProxmoxGuest[] | null>(null);
|
||||
const [discoveredCredentialId, setDiscoveredCredentialId] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [discoveredJumpHosts, setDiscoveredJumpHosts] = useState<
|
||||
unknown[] | null
|
||||
>(null);
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
// When opened from the dropdown (no preselectedHostId), only show Proxmox-enabled hosts
|
||||
const sshHosts = hosts.filter(
|
||||
(h) =>
|
||||
!("isFolder" in h) &&
|
||||
(
|
||||
h.pluginSettings as
|
||||
Record<string, Record<string, unknown> | undefined> | undefined
|
||||
)?.proxmox?.enableProxmox === true,
|
||||
);
|
||||
|
||||
// The Proxmox host the discovery runs against — imported guests are grouped
|
||||
// into a folder named after it. Use the same id resolution as discovery so
|
||||
// it also works when launched directly from a host action (preselectedHostId).
|
||||
const effectiveHostId =
|
||||
preselectedHostId != null ? String(preselectedHostId) : selectedHostId;
|
||||
const sourceHost = hosts.find(
|
||||
(h) => !("isFolder" in h) && String(h.id) === effectiveHostId,
|
||||
);
|
||||
const importFolder = sourceHost?.name || "Proxmox";
|
||||
|
||||
function reset() {
|
||||
if (!preselectedHostId) setSelectedHostId("");
|
||||
setGuests(null);
|
||||
setDiscoveredCredentialId(null);
|
||||
setDiscoveredJumpHosts(null);
|
||||
setSelected(new Set());
|
||||
streamCloseRef.current?.();
|
||||
streamCloseRef.current = null;
|
||||
setProgress(null);
|
||||
setDiscovering(false);
|
||||
setImporting(false);
|
||||
}
|
||||
|
||||
function handleDiscover() {
|
||||
const hostId =
|
||||
preselectedHostId ?? (selectedHostId ? Number(selectedHostId) : null);
|
||||
if (!hostId) return;
|
||||
setDiscovering(true);
|
||||
setGuests(null);
|
||||
setDiscoveredCredentialId(null);
|
||||
setDiscoveredJumpHosts(null);
|
||||
setSelected(new Set());
|
||||
setProgress(null);
|
||||
streamCloseRef.current?.();
|
||||
streamCloseRef.current = discoverProxmoxGuestsStream(hostId, {
|
||||
onProgress: (done, total) => setProgress({ done, total }),
|
||||
onResult: (result) => {
|
||||
setGuests(result.guests);
|
||||
setDiscoveredCredentialId(result.credentialId ?? null);
|
||||
setDiscoveredJumpHosts(result.jumpHosts ?? null);
|
||||
setSelected(
|
||||
new Set(
|
||||
result.guests
|
||||
.filter((g) => g.status === "running")
|
||||
.map((g) => g.vmid),
|
||||
),
|
||||
);
|
||||
setDiscovering(false);
|
||||
setProgress(null);
|
||||
streamCloseRef.current = null;
|
||||
},
|
||||
onError: (message) => {
|
||||
toast.error(message ?? t("hosts.proxmoxDiscoveryFailed"));
|
||||
setDiscovering(false);
|
||||
setProgress(null);
|
||||
streamCloseRef.current = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
if (!guests || selected.size === 0) return;
|
||||
setImporting(true);
|
||||
try {
|
||||
// Prefer explicitly configured credential, then fall back to the host's own credential
|
||||
const credId = defaultCredentialId ?? discoveredCredentialId;
|
||||
const importAuth = resolveProxmoxImportAuth(defaultAuthType, credId);
|
||||
|
||||
const selectedGuests = guests.filter((g) => selected.has(g.vmid));
|
||||
|
||||
const toImport = selectedGuests.map((g) => ({
|
||||
name: g.name,
|
||||
// No IP discovered (e.g. QEMU without a running guest agent): import
|
||||
// with a placeholder so the host is created and the user can fill in
|
||||
// the real IP. Re-sync keeps the manual value (guest.ip || existing.ip).
|
||||
ip: g.ip || "0.0.0.0",
|
||||
port: g.connectionType === "rdp" ? 3389 : 22,
|
||||
username:
|
||||
importAuth.authType === "credential"
|
||||
? ""
|
||||
: (defaultUsername ?? "root"),
|
||||
folder: importFolder,
|
||||
// Inherit the jump-host chain from the scanned Proxmox host so the
|
||||
// imported guests are reachable the same way; user can override.
|
||||
jumpHosts: discoveredJumpHosts ?? undefined,
|
||||
...importAuth,
|
||||
enableSsh: g.connectionType !== "rdp",
|
||||
connectionType: g.connectionType,
|
||||
tags: [
|
||||
"proxmox",
|
||||
g.type,
|
||||
g.node,
|
||||
g.type === "lxc" ? `ct-${g.vmid}` : `vm-${g.vmid}`,
|
||||
...(g.enableDocker ? ["docker"] : []),
|
||||
],
|
||||
// Host settings travel with the import like an export's, keyed by
|
||||
// the plugin that keeps them.
|
||||
pluginSettings: {
|
||||
...(g.connectionType === "rdp"
|
||||
? { "remote-desktop": { enableRdp: true, rdpPort: 3389 } }
|
||||
: {}),
|
||||
...(g.enableDocker ? { docker: { enableDocker: true } } : {}),
|
||||
proxmox: {
|
||||
proxmoxConfig: {
|
||||
source: {
|
||||
source: "proxmox",
|
||||
sourceHostId: Number(effectiveHostId),
|
||||
node: g.node,
|
||||
vmid: g.vmid,
|
||||
type: g.type,
|
||||
lastSeenAt: new Date().toISOString(),
|
||||
lastStatus: g.status,
|
||||
missingSince: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const result = toImport.length
|
||||
? await importProxmoxHosts(toImport)
|
||||
: { success: 0, failed: 0, errors: [] };
|
||||
|
||||
if (toImport.length) {
|
||||
const updated = await listHosts();
|
||||
onHostsChanged(updated);
|
||||
notifyHostsChanged();
|
||||
}
|
||||
|
||||
const msg = [
|
||||
result.success
|
||||
? t("hosts.proxmoxResultImported", { count: result.success })
|
||||
: null,
|
||||
result.failed
|
||||
? t("hosts.proxmoxResultFailed", { count: result.failed })
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
toast.success(t("hosts.proxmoxImportComplete", { summary: msg }));
|
||||
|
||||
if (result.failed === 0) {
|
||||
reset();
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err?.message ?? t("hosts.proxmoxImportFailed"));
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeGroups = guests
|
||||
? guests.reduce(
|
||||
(acc, g) => {
|
||||
if (!acc[g.node]) acc[g.node] = [];
|
||||
acc[g.node].push(g);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ProxmoxGuest[]>,
|
||||
)
|
||||
: {};
|
||||
|
||||
const canDiscover = preselectedHostId != null || !!selectedHostId;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
if (!v) {
|
||||
reset();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Server className="size-4" />
|
||||
{t("hosts.proxmoxImportTitle")}
|
||||
</DialogTitle>
|
||||
<a
|
||||
href="https://docs.termix.site/features/files-and-hosts/proxmox-import"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[10px] text-accent-brand hover:underline"
|
||||
>
|
||||
{t("hosts.docsLink")}
|
||||
</a>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Host selector — hidden when launched from a specific host */}
|
||||
{!preselectedHostId && (
|
||||
<div className="flex gap-2">
|
||||
<Select2
|
||||
value={selectedHostId}
|
||||
onChange={(event) => setSelectedHostId(event.target.value)}
|
||||
disabled={discovering}
|
||||
placeholder={t("hosts.proxmoxSelectHost")}
|
||||
className="flex-1 text-xs h-8"
|
||||
>
|
||||
{sshHosts.map((h) => (
|
||||
<option key={h.id} value={String(h.id)}>
|
||||
{h.name || h.ip}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!canDiscover || discovering}
|
||||
onClick={handleDiscover}
|
||||
className="shrink-0"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`size-3.5 mr-1.5 ${discovering ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("hosts.proxmoxDiscover")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* When launched from a specific host, show Discover directly */}
|
||||
{preselectedHostId && !guests && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={discovering}
|
||||
onClick={handleDiscover}
|
||||
className="w-full"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`size-3.5 mr-1.5 ${discovering ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{discovering
|
||||
? progress
|
||||
? `${t("hosts.proxmoxDiscovering")} ${progress.done}/${progress.total}`
|
||||
: t("hosts.proxmoxDiscovering")
|
||||
: t("hosts.proxmoxDiscoverGuests")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Guest list */}
|
||||
{guests !== null && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>
|
||||
{t("hosts.proxmoxGuestsSelected", {
|
||||
count: guests.length,
|
||||
selected: selected.size,
|
||||
})}
|
||||
</span>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
className="hover:text-foreground transition-colors"
|
||||
onClick={() =>
|
||||
setSelected(new Set(guests.map((g) => g.vmid)))
|
||||
}
|
||||
>
|
||||
{t("hosts.proxmoxSelectAll")}
|
||||
</button>
|
||||
<button
|
||||
className="hover:text-foreground transition-colors"
|
||||
onClick={() => setSelected(new Set())}
|
||||
>
|
||||
{t("hosts.proxmoxDeselectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{guests.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground py-4 text-center">
|
||||
{t("hosts.proxmoxNoGuests")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-64 overflow-y-auto rounded-md border border-border divide-y divide-border">
|
||||
{Object.entries(nodeGroups).map(([node, nodeGuests]) => (
|
||||
<div key={node}>
|
||||
<div className="px-2 py-1 text-[10px] font-medium text-muted-foreground bg-muted/50 uppercase tracking-wider">
|
||||
{node}
|
||||
</div>
|
||||
{nodeGuests.map((g) => (
|
||||
<button
|
||||
key={g.vmid}
|
||||
className="w-full flex items-center gap-2 px-2 py-1.5 text-xs hover:bg-accent transition-colors text-left"
|
||||
onClick={() => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(g.vmid)) next.delete(g.vmid);
|
||||
else next.add(g.vmid);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{selected.has(g.vmid) ? (
|
||||
<CheckSquare className="size-3.5 text-primary shrink-0" />
|
||||
) : (
|
||||
<Square className="size-3.5 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<span className="flex-1 truncate">{g.name}</span>
|
||||
<span className="text-muted-foreground text-[10px] shrink-0">
|
||||
{g.type.toUpperCase()} {g.vmid}
|
||||
</span>
|
||||
<span
|
||||
className={`shrink-0 text-[10px] ${g.status === "running" ? "text-green-400" : "text-muted-foreground"}`}
|
||||
>
|
||||
{g.status}
|
||||
</span>
|
||||
<span
|
||||
className={`shrink-0 text-[10px] font-mono ${
|
||||
g.ip
|
||||
? "text-muted-foreground"
|
||||
: "text-amber-400/80 italic"
|
||||
}`}
|
||||
>
|
||||
{g.ip || "no IP"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
reset();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{guests !== null && selected.size > 0 && (
|
||||
<Button size="sm" disabled={importing} onClick={handleImport}>
|
||||
{importing ? (
|
||||
<RefreshCw className="size-3.5 animate-spin mr-1.5" />
|
||||
) : (
|
||||
<Download className="size-3.5 mr-1.5" />
|
||||
)}
|
||||
{t("hosts.proxmoxImportButton", { count: selected.size })}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useEffect, useState, type ComponentType } from "react";
|
||||
import { Boxes, HardDrive, Server } from "lucide-react";
|
||||
import {
|
||||
useHosts,
|
||||
useTranslation,
|
||||
type HostEditorSectionProps,
|
||||
type StandaloneViewProps,
|
||||
type TabProps,
|
||||
type TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { ComponentSlot, DropdownMenuItem } from "@termix/plugin-sdk/ui";
|
||||
import type { PluginHostRecord } from "@termix/plugin-sdk/frontend";
|
||||
import { setProxmoxApp } from "./proxmox-api";
|
||||
import { ProxmoxDiscoverDialog } from "./ProxmoxDiscoverDialog";
|
||||
import { HostProxmoxTab } from "./HostProxmoxTab";
|
||||
import { ProxmoxStatsTab } from "./stats/ProxmoxStatsTab";
|
||||
import ProxmoxStatsApp from "./stats/ProxmoxStatsApp";
|
||||
import { HostProxmoxStatsTab } from "./stats/HostProxmoxStatsTab";
|
||||
|
||||
interface DiscoverRequest {
|
||||
hostId?: number;
|
||||
defaultCredentialId?: number | null;
|
||||
defaultAuthType?: string;
|
||||
}
|
||||
|
||||
type Opener = (request: DiscoverRequest) => void;
|
||||
|
||||
// The Proxmox fields are this plugin's host settings, saved with the host
|
||||
// through form.pluginSettings.proxmox.
|
||||
function ProxmoxHostSection({ form, updateForm }: HostEditorSectionProps) {
|
||||
const values = (form.pluginSettings?.proxmox ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const setField = (key: string, value: unknown) =>
|
||||
updateForm((current) => {
|
||||
const all = (current.pluginSettings ?? {}) as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
return {
|
||||
...current,
|
||||
pluginSettings: {
|
||||
...all,
|
||||
proxmox: { ...(all.proxmox ?? {}), [key]: value },
|
||||
},
|
||||
};
|
||||
});
|
||||
const sectionForm = {
|
||||
enableProxmox: values.enableProxmox === true,
|
||||
proxmoxConfig: values.proxmoxConfig ?? null,
|
||||
enableProxmoxStats: values.enableProxmoxStats === true,
|
||||
proxmoxStatsConfig: values.proxmoxStatsConfig ?? {
|
||||
pollInterval: 60,
|
||||
nodeName: null,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<HostProxmoxTab
|
||||
form={sectionForm as Parameters<typeof HostProxmoxTab>[0]["form"]}
|
||||
setField={setField as Parameters<typeof HostProxmoxTab>[0]["setField"]}
|
||||
/>
|
||||
<HostProxmoxStatsTab
|
||||
form={sectionForm as Parameters<typeof HostProxmoxStatsTab>[0]["form"]}
|
||||
setField={
|
||||
setField as Parameters<typeof HostProxmoxStatsTab>[0]["setField"]
|
||||
}
|
||||
/>
|
||||
{/* Other plugins add Proxmox-related settings here. */}
|
||||
<ComponentSlot
|
||||
slotId="proxmox.hostEditor"
|
||||
props={{ form: sectionForm, setField }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ProxmoxStatsTabView({ sshHost, label, isVisible }: TabProps) {
|
||||
return (
|
||||
<ProxmoxStatsTab
|
||||
hostConfig={sshHost as never}
|
||||
title={label}
|
||||
isVisible={isVisible}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ProxmoxStatsStandalone({ hostId }: StandaloneViewProps) {
|
||||
return <ProxmoxStatsApp hostId={hostId} />;
|
||||
}
|
||||
|
||||
/** This plugin's host settings, as they arrive on the host record. */
|
||||
export function proxmoxSettings(
|
||||
host: Pick<PluginHostRecord, "pluginSettings">,
|
||||
): Record<string, unknown> {
|
||||
return host.pluginSettings?.proxmox ?? {};
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
setProxmoxApp(app);
|
||||
app.onDispose(() => setProxmoxApp(null));
|
||||
// The dialog lives in the hosts panel; the menu item and host action open
|
||||
// it through this, created per activation.
|
||||
const openers = new Set<Opener>();
|
||||
const openDiscover: Opener = (request) => {
|
||||
for (const open of openers) open(request);
|
||||
};
|
||||
|
||||
function DiscoverDialogHost({
|
||||
hosts,
|
||||
onHostsChanged,
|
||||
}: {
|
||||
hosts: PluginHostRecord[];
|
||||
onHostsChanged: (hosts: PluginHostRecord[]) => void;
|
||||
}) {
|
||||
const [request, setRequest] = useState<DiscoverRequest | null>(null);
|
||||
useEffect(() => {
|
||||
openers.add(setRequest);
|
||||
return () => {
|
||||
openers.delete(setRequest);
|
||||
};
|
||||
}, []);
|
||||
if (!request) return null;
|
||||
return (
|
||||
<ProxmoxDiscoverDialog
|
||||
open
|
||||
onClose={() => setRequest(null)}
|
||||
hosts={hosts}
|
||||
onHostsChanged={onHostsChanged}
|
||||
preselectedHostId={request.hostId}
|
||||
defaultCredentialId={request.defaultCredentialId ?? null}
|
||||
defaultAuthType={request.defaultAuthType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportMenuItem() {
|
||||
const { t } = useTranslation();
|
||||
const { hosts } = useHosts();
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => openDiscover({})}
|
||||
disabled={!hosts.some((host) => proxmoxSettings(host).enableProxmox)}
|
||||
>
|
||||
<Server className="size-3.5 mr-2" />
|
||||
{t("hosts.proxmoxImportTitle")}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
app.registerSlotContribution("hosts.panel", {
|
||||
actionId: "proxmox.discoverDialog",
|
||||
titleKey: "hosts.proxmoxImportTitle",
|
||||
kind: "component",
|
||||
component: DiscoverDialogHost as unknown as ComponentType<
|
||||
Record<string, unknown>
|
||||
>,
|
||||
});
|
||||
|
||||
app.registerSlotContribution("hosts.importMenu", {
|
||||
actionId: "proxmox.import",
|
||||
titleKey: "hosts.proxmoxImportTitle",
|
||||
kind: "component",
|
||||
component: ImportMenuItem as ComponentType<Record<string, unknown>>,
|
||||
});
|
||||
|
||||
app.registerHostAction({
|
||||
id: "proxmox-discover",
|
||||
titleKey: "hosts.proxmoxDiscoverAction",
|
||||
icon: Boxes,
|
||||
kind: "open",
|
||||
order: 90,
|
||||
when: (host) => !!proxmoxSettings(host).enableProxmox,
|
||||
run: (host) => {
|
||||
const config = proxmoxSettings(host).proxmoxConfig as
|
||||
| { defaultCredentialId?: number | null; defaultAuthType?: string }
|
||||
| undefined;
|
||||
openDiscover({
|
||||
hostId: Number(host.id),
|
||||
defaultCredentialId: config?.defaultCredentialId ?? null,
|
||||
defaultAuthType: config?.defaultAuthType ?? undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
app.registerHostEditorSection({
|
||||
id: "proxmox",
|
||||
group: "ssh",
|
||||
titleKey: "hosts.tabProxmox",
|
||||
icon: Server,
|
||||
order: 50,
|
||||
component: ProxmoxHostSection,
|
||||
});
|
||||
|
||||
app.registerTab("proxmox-stats", ProxmoxStatsTabView, {
|
||||
icon: HardDrive,
|
||||
titleKey: "nav.proxmoxStats",
|
||||
requiresHost: true,
|
||||
noHostMessageKey: "proxmoxStats.noHostSelected",
|
||||
standalone: ProxmoxStatsStandalone,
|
||||
preload: () => import("./stats/ProxmoxStatsTab"),
|
||||
});
|
||||
|
||||
app.registerHostAction({
|
||||
id: "proxmox-stats",
|
||||
titleKey: "nav.proxmoxStats",
|
||||
icon: HardDrive,
|
||||
kind: "open",
|
||||
order: 60,
|
||||
tabType: "proxmox-stats",
|
||||
copyUrlView: "proxmox-stats",
|
||||
when: (host) => proxmoxSettings(host).enableProxmoxStats === true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxDiscoverResult } from "./types";
|
||||
|
||||
let current: TermixApp | null = null;
|
||||
|
||||
/** Set in activate, cleared on deactivate. */
|
||||
export function setProxmoxApp(app: TermixApp | null): void {
|
||||
current = app;
|
||||
}
|
||||
|
||||
function app(): TermixApp {
|
||||
if (!current) throw new Error("The Proxmox plugin is not active");
|
||||
return current;
|
||||
}
|
||||
|
||||
export function listHosts(): ReturnType<TermixApp["listHosts"]> {
|
||||
return app().listHosts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a guest discovery over the server-sent event stream and reports
|
||||
* progress. Returns a function that stops listening.
|
||||
*/
|
||||
export function discoverProxmoxGuestsStream(
|
||||
hostId: number,
|
||||
handlers: {
|
||||
onProgress?: (done: number, total: number) => void;
|
||||
onResult: (result: ProxmoxDiscoverResult) => void;
|
||||
onError: (message: string) => void;
|
||||
},
|
||||
): () => void {
|
||||
const controller = new AbortController();
|
||||
let settled = false;
|
||||
const finish = () => {
|
||||
settled = true;
|
||||
controller.abort();
|
||||
};
|
||||
|
||||
const handleEvent = (event: string, data: string) => {
|
||||
if (event === "progress") {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
handlers.onProgress?.(parsed.done, parsed.total);
|
||||
} catch {
|
||||
// ignore malformed progress frames
|
||||
}
|
||||
} else if (event === "result") {
|
||||
finish();
|
||||
try {
|
||||
handlers.onResult(JSON.parse(data));
|
||||
} catch {
|
||||
handlers.onError("Failed to parse discovery result");
|
||||
}
|
||||
} else if (event === "fail") {
|
||||
finish();
|
||||
let message = "Discovery failed";
|
||||
try {
|
||||
message = JSON.parse(data).message || message;
|
||||
} catch {
|
||||
// keep default message
|
||||
}
|
||||
handlers.onError(message);
|
||||
}
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await app().fetch(
|
||||
`/discover/stream?hostId=${encodeURIComponent(String(hostId))}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (!response.ok || !response.body) throw new Error("stream failed");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let split = buffer.indexOf("\n\n");
|
||||
while (split !== -1) {
|
||||
const frame = buffer.slice(0, split);
|
||||
buffer = buffer.slice(split + 2);
|
||||
let event = "message";
|
||||
const data: string[] = [];
|
||||
for (const line of frame.split("\n")) {
|
||||
if (line.startsWith("event:")) event = line.slice(6).trim();
|
||||
else if (line.startsWith("data:")) data.push(line.slice(5).trim());
|
||||
}
|
||||
if (data.length) handleEvent(event, data.join("\n"));
|
||||
split = buffer.indexOf("\n\n");
|
||||
}
|
||||
}
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
handlers.onError("Discovery connection lost");
|
||||
}
|
||||
} catch {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
handlers.onError("Discovery connection lost");
|
||||
}
|
||||
})();
|
||||
|
||||
return finish;
|
||||
}
|
||||
|
||||
export interface ProxmoxImportResult {
|
||||
success: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export async function importProxmoxHosts(
|
||||
hosts: Record<string, unknown>[],
|
||||
): Promise<ProxmoxImportResult> {
|
||||
const response = await app().api.post<ProxmoxImportResult>("/import", {
|
||||
hosts,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
const SECRET_BACKED_AUTH_TYPES = new Set(["password", "key"]);
|
||||
|
||||
export type ProxmoxImportAuth = {
|
||||
authType: string;
|
||||
credentialId?: number;
|
||||
overrideCredentialUsername?: boolean;
|
||||
};
|
||||
|
||||
export function resolveProxmoxImportAuth(
|
||||
defaultAuthType: string | undefined,
|
||||
credentialId: number | null | undefined,
|
||||
): ProxmoxImportAuth {
|
||||
// Any auth type that needs no stored secret (none, agent, or one a plugin
|
||||
// adds) wins, the same rule the backend copy applies.
|
||||
if (
|
||||
defaultAuthType &&
|
||||
defaultAuthType !== "credential" &&
|
||||
!SECRET_BACKED_AUTH_TYPES.has(defaultAuthType)
|
||||
) {
|
||||
return { authType: defaultAuthType };
|
||||
}
|
||||
|
||||
// A credential (configured default OR inherited from the source Proxmox host)
|
||||
// is a concrete auth source -> use it, even when defaultAuthType is the
|
||||
// "password"/"key" default. Otherwise imported guests end up as authType
|
||||
// "none" although the host authenticates via a credential.
|
||||
if (credentialId) {
|
||||
return {
|
||||
authType: "credential",
|
||||
credentialId,
|
||||
overrideCredentialUsername: false,
|
||||
};
|
||||
}
|
||||
|
||||
return { authType: "none" };
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Box, Search, Server as ServerIcon } from "lucide-react";
|
||||
import { Input } from "@termix/plugin-sdk/ui";
|
||||
import type { ProxmoxGuestSummary } from "../types";
|
||||
import { cn } from "@termix/plugin-sdk/ui";
|
||||
|
||||
type StatusFilter = "all" | "running" | "stopped";
|
||||
type TypeFilter = "all" | "qemu" | "lxc";
|
||||
|
||||
function formatUptime(seconds: number | null): string {
|
||||
if (seconds === null || seconds <= 0) return "-";
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (days > 0) return `${days}d ${hours}h`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
function UsageCell({
|
||||
percent,
|
||||
usedGiB,
|
||||
totalGiB,
|
||||
}: {
|
||||
percent: number | null;
|
||||
usedGiB?: number | null;
|
||||
totalGiB?: number | null;
|
||||
}) {
|
||||
if (percent === null) {
|
||||
return <span className="text-[11px] text-muted-foreground">-</span>;
|
||||
}
|
||||
const clamped = Math.min(100, Math.max(0, percent));
|
||||
const barColor =
|
||||
clamped >= 90
|
||||
? "bg-red-500"
|
||||
: clamped >= 75
|
||||
? "bg-yellow-500"
|
||||
: "bg-accent-brand";
|
||||
return (
|
||||
<div className="flex min-w-[92px] flex-col gap-1">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-xs font-semibold tabular-nums">
|
||||
{clamped.toFixed(0)}%
|
||||
</span>
|
||||
{usedGiB != null && totalGiB != null && (
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground">
|
||||
{usedGiB.toFixed(1)}/{totalGiB.toFixed(1)}G
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={cn("h-full rounded-full", barColor)}
|
||||
style={{ width: `${clamped}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GuestTable({ guests }: { guests: ProxmoxGuestSummary[] }) {
|
||||
const { t } = useTranslation();
|
||||
const [query, setQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>("all");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return guests
|
||||
.filter(
|
||||
(g) =>
|
||||
statusFilter === "all" ||
|
||||
(statusFilter === "running") === (g.status === "running"),
|
||||
)
|
||||
.filter((g) => typeFilter === "all" || g.type === typeFilter)
|
||||
.filter(
|
||||
(g) =>
|
||||
!q || g.name.toLowerCase().includes(q) || String(g.vmid).includes(q),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.status === b.status) return a.name.localeCompare(b.name);
|
||||
return a.status === "running" ? -1 : 1;
|
||||
});
|
||||
}, [guests, query, statusFilter, typeFilter]);
|
||||
|
||||
const running = guests.filter((g) => g.status === "running").length;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[420px] flex-1 flex-col overflow-hidden border border-border bg-card">
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2.5">
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("proxmoxStats.guestsSummary")}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{t("proxmoxStats.guestCounts", { running, total: guests.length })}
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t("proxmoxStats.searchGuests")}
|
||||
className="h-7 w-40 pl-6 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex overflow-hidden rounded-none border border-border">
|
||||
{(["all", "qemu", "lxc"] as TypeFilter[]).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setTypeFilter(f)}
|
||||
className={cn(
|
||||
"px-2 py-1 text-[10px] font-semibold uppercase tracking-wide transition-colors",
|
||||
typeFilter === f
|
||||
? "bg-accent-brand text-white"
|
||||
: "bg-background text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{f === "all"
|
||||
? t("proxmoxStats.filterAll")
|
||||
: f === "qemu"
|
||||
? t("proxmoxStats.filterVm")
|
||||
: t("proxmoxStats.filterLxc")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex overflow-hidden rounded-none border border-border">
|
||||
{(["all", "running", "stopped"] as StatusFilter[]).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setStatusFilter(f)}
|
||||
className={cn(
|
||||
"px-2 py-1 text-[10px] font-semibold uppercase tracking-wide transition-colors",
|
||||
statusFilter === f
|
||||
? "bg-accent-brand text-white"
|
||||
: "bg-background text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
{f === "all"
|
||||
? t("proxmoxStats.filterAll")
|
||||
: f === "running"
|
||||
? t("proxmoxStats.running")
|
||||
: t("proxmoxStats.stopped")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto thin-scrollbar">
|
||||
{guests.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center py-16 text-xs text-muted-foreground">
|
||||
{t("proxmoxStats.noGuests")}
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center py-16 text-xs text-muted-foreground">
|
||||
{t("proxmoxStats.noGuestsMatch")}
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead className="sticky top-0 z-10 bg-card">
|
||||
<tr className="border-b border-border text-[10px] uppercase tracking-wider text-muted-foreground">
|
||||
<th className="px-3 py-2 text-left font-semibold">
|
||||
{t("proxmoxStats.name")}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">
|
||||
{t("proxmoxStats.status")}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">ID</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">
|
||||
{t("proxmoxStats.cpu")}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">
|
||||
{t("proxmoxStats.mem")}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">
|
||||
{t("proxmoxStats.disk")}
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">
|
||||
{t("proxmoxStats.uptime")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{filtered.map((guest) => (
|
||||
<tr
|
||||
key={`${guest.type}-${guest.vmid}`}
|
||||
className="transition-colors hover:bg-muted/40"
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{guest.type === "lxc" ? (
|
||||
<Box className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<ServerIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
<span className="min-w-0 truncate font-semibold">
|
||||
{guest.name}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-full border border-border px-1.5 py-0.5 text-[9px] uppercase text-muted-foreground">
|
||||
{guest.type === "lxc" ? "LXC" : "VM"}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 shrink-0 rounded-full",
|
||||
guest.status === "running"
|
||||
? "bg-green-500"
|
||||
: "bg-muted-foreground/50",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
guest.status === "running"
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{guest.status === "running"
|
||||
? t("proxmoxStats.running")
|
||||
: t("proxmoxStats.stopped")}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono text-muted-foreground">
|
||||
{guest.vmid}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<UsageCell percent={guest.cpuPercent} />
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<UsageCell
|
||||
percent={guest.memPercent}
|
||||
usedGiB={guest.memUsedGiB}
|
||||
totalGiB={guest.memTotalGiB}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<UsageCell
|
||||
percent={guest.diskPercent}
|
||||
usedGiB={guest.diskUsedGiB}
|
||||
totalGiB={guest.diskTotalGiB}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap text-muted-foreground">
|
||||
{formatUptime(guest.uptimeSeconds)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Server } from "lucide-react";
|
||||
import {
|
||||
Input,
|
||||
SectionCard,
|
||||
SettingRow,
|
||||
FakeSwitch,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import type { ProxmoxStatsConfig } from "../types";
|
||||
|
||||
interface HostProxmoxStatsForm {
|
||||
enableProxmoxStats: boolean;
|
||||
proxmoxStatsConfig?: ProxmoxStatsConfig | null;
|
||||
}
|
||||
|
||||
type SetHostField = (key: string, value: unknown) => void;
|
||||
|
||||
export function HostProxmoxStatsTab({
|
||||
form,
|
||||
setField,
|
||||
}: {
|
||||
form: HostProxmoxStatsForm;
|
||||
setField: SetHostField;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("hosts.enableProxmoxStats")}
|
||||
icon={<Server className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-0 py-1">
|
||||
<SettingRow
|
||||
label={t("hosts.enableProxmoxStats")}
|
||||
description={t("hosts.enableProxmoxStatsDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.enableProxmoxStats}
|
||||
onChange={(v) => setField("enableProxmoxStats", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{form.enableProxmoxStats && (
|
||||
<>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxStatsPollInterval")}
|
||||
description={t("hosts.proxmoxStatsPollIntervalDesc")}
|
||||
>
|
||||
<Input
|
||||
className="w-24 h-7 text-xs"
|
||||
type="number"
|
||||
min={15}
|
||||
value={form.proxmoxStatsConfig?.pollInterval ?? 60}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxStatsConfig", {
|
||||
...form.proxmoxStatsConfig,
|
||||
pollInterval: Math.max(
|
||||
15,
|
||||
Number.parseInt(e.target.value || "60", 10),
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.proxmoxStatsNodeOverride")}
|
||||
description={t("hosts.proxmoxStatsNodeOverrideDesc")}
|
||||
>
|
||||
<Input
|
||||
className="w-44 h-7 text-xs"
|
||||
value={form.proxmoxStatsConfig?.nodeName ?? ""}
|
||||
placeholder={t("hosts.proxmoxStatsNodeOverridePlaceholder")}
|
||||
onChange={(e) =>
|
||||
setField("proxmoxStatsConfig", {
|
||||
...form.proxmoxStatsConfig,
|
||||
nodeName: e.target.value || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Cpu, MemoryStick, HardDrive, Clock, Server } from "lucide-react";
|
||||
import type { ProxmoxNodeStats } from "../types";
|
||||
import { cn } from "@termix/plugin-sdk/ui";
|
||||
|
||||
function Tile({
|
||||
icon,
|
||||
label,
|
||||
percent,
|
||||
detail,
|
||||
sparkline,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
percent: number | null;
|
||||
detail?: string | null;
|
||||
sparkline?: number[];
|
||||
}) {
|
||||
const clamped = percent === null ? null : Math.min(100, Math.max(0, percent));
|
||||
const barColor =
|
||||
clamped === null
|
||||
? "bg-muted-foreground/30"
|
||||
: clamped >= 90
|
||||
? "bg-red-500"
|
||||
: clamped >= 75
|
||||
? "bg-yellow-500"
|
||||
: "bg-accent-brand";
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2 border border-border bg-card px-4 py-3">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
{icon}
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-2xl font-bold tabular-nums">
|
||||
{clamped === null ? "N/A" : `${clamped.toFixed(0)}%`}
|
||||
</span>
|
||||
{detail && (
|
||||
<span className="truncate text-[11px] text-muted-foreground">
|
||||
{detail}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={cn("h-full rounded-full transition-all", barColor)}
|
||||
style={{ width: `${clamped ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
{sparkline && sparkline.length > 1 && <MiniSparkline data={sparkline} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniSparkline({ data }: { data: number[] }) {
|
||||
const w = 100;
|
||||
const h = 20;
|
||||
const max = 100;
|
||||
const points = data
|
||||
.map((v, i) => {
|
||||
const x = (i / (data.length - 1)) * w;
|
||||
const y = h - (Math.min(100, Math.max(0, v)) / max) * h;
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
})
|
||||
.join(" ");
|
||||
return (
|
||||
<svg viewBox={`0 0 ${w} ${h}`} className="h-5 w-full text-accent-brand/70">
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function NodeSummaryStrip({
|
||||
node,
|
||||
histories,
|
||||
}: {
|
||||
node: ProxmoxNodeStats | undefined;
|
||||
histories: { cpu: number[]; memory: number[]; disk: number[] };
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Tile
|
||||
icon={<Cpu className="size-3.5" />}
|
||||
label={t("proxmoxStats.cpuUsage")}
|
||||
percent={node?.cpu.percent ?? null}
|
||||
detail={
|
||||
node?.cpu.cores != null
|
||||
? t("proxmoxStats.cpuCores", { count: node.cpu.cores })
|
||||
: undefined
|
||||
}
|
||||
sparkline={histories.cpu}
|
||||
/>
|
||||
<Tile
|
||||
icon={<MemoryStick className="size-3.5" />}
|
||||
label={t("proxmoxStats.memoryUsage")}
|
||||
percent={node?.memory.percent ?? null}
|
||||
detail={
|
||||
node?.memory.usedGiB != null && node?.memory.totalGiB != null
|
||||
? `${node.memory.usedGiB.toFixed(1)} / ${node.memory.totalGiB.toFixed(1)} GiB`
|
||||
: undefined
|
||||
}
|
||||
sparkline={histories.memory}
|
||||
/>
|
||||
<Tile
|
||||
icon={<HardDrive className="size-3.5" />}
|
||||
label={t("proxmoxStats.diskUsage")}
|
||||
percent={node?.disk.percent ?? null}
|
||||
detail={
|
||||
node?.disk.usedGiB != null && node?.disk.totalGiB != null
|
||||
? `${node.disk.usedGiB.toFixed(1)} / ${node.disk.totalGiB.toFixed(1)} GiB`
|
||||
: undefined
|
||||
}
|
||||
sparkline={histories.disk}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-between gap-2 border border-border bg-card px-4 py-3">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Clock className="size-3.5" />
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest">
|
||||
{t("proxmoxStats.uptime")}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold">
|
||||
{node?.uptime.formatted ?? "N/A"}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 truncate text-[11px] text-muted-foreground">
|
||||
<Server className="size-3 shrink-0" />
|
||||
<span className="truncate">{node?.system.hostname ?? "-"}</span>
|
||||
{node?.system.pveVersion && (
|
||||
<span className="ml-1 shrink-0">{node.system.pveVersion}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { FullScreenAppWrapper, ConnectionScreen } from "@termix/plugin-sdk/ui";
|
||||
import { ProxmoxStatsTab } from "./ProxmoxStatsTab";
|
||||
|
||||
interface ProxmoxStatsAppProps {
|
||||
hostId?: string;
|
||||
}
|
||||
|
||||
const ProxmoxStatsApp: React.FC<ProxmoxStatsAppProps> = ({ hostId }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FullScreenAppWrapper hostId={hostId}>
|
||||
{(hostConfig, phase) => {
|
||||
if (phase === "loading") {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<ConnectionScreen
|
||||
status="connecting"
|
||||
message={t("hosts.loadingHost")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hostConfig) {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<ConnectionScreen
|
||||
status="disconnected"
|
||||
message={t("hosts.hostNotFound")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProxmoxStatsTab
|
||||
hostConfig={hostConfig}
|
||||
title={hostConfig.name || `${hostConfig.username}@${hostConfig.ip}`}
|
||||
isVisible={true}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FullScreenAppWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxmoxStatsApp;
|
||||
@@ -0,0 +1,387 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
ConnectionScreen,
|
||||
ConnectionLogProvider,
|
||||
useConnectionLog,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
useTranslation,
|
||||
usePluginApi,
|
||||
useHosts,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { RefreshCw, Server } from "lucide-react";
|
||||
import { createProxmoxStatsApi } from "./proxmox-stats-api";
|
||||
import type { ProxmoxStatsSnapshot, ProxmoxStatsConfig } from "../types";
|
||||
import { NodeSummaryStrip } from "./NodeSummaryStrip";
|
||||
import { GuestTable } from "./GuestTable";
|
||||
import { NodeNetworkCard } from "./cards/NodeNetworkCard";
|
||||
import { StoragePoolsCard } from "./cards/StoragePoolsCard";
|
||||
import { ClusterHealthCard } from "./cards/ClusterHealthCard";
|
||||
import { useConnectionRetry } from "@termix/plugin-sdk/frontend";
|
||||
import { runAdaptivePolling } from "@termix/plugin-sdk/ui";
|
||||
|
||||
const HISTORY_LEN = 30;
|
||||
|
||||
function statsChangeKey(data: ProxmoxStatsSnapshot): string {
|
||||
const bucket = (value: number | null | undefined) =>
|
||||
value == null ? null : Math.round(value / 5) * 5;
|
||||
return JSON.stringify({
|
||||
cpu: bucket(data.node.cpu.percent),
|
||||
memory: bucket(data.node.memory.percent),
|
||||
disk: bucket(data.node.disk.percent),
|
||||
guests: data.guests.counts,
|
||||
cluster: data.cluster,
|
||||
});
|
||||
}
|
||||
const DEFAULT_POLL_INTERVAL = 60;
|
||||
|
||||
interface HostConfig {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
username: string;
|
||||
pluginSettings?: {
|
||||
proxmox?: {
|
||||
enableProxmoxStats?: boolean;
|
||||
proxmoxStatsConfig?: string | ProxmoxStatsConfig | null;
|
||||
};
|
||||
};
|
||||
authType?: string;
|
||||
port?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ProxmoxStatsProps {
|
||||
hostConfig?: HostConfig;
|
||||
title?: string;
|
||||
isVisible?: boolean;
|
||||
isTopbarOpen?: boolean;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
interface ProxmoxStatsHistories {
|
||||
cpu: number[];
|
||||
memory: number[];
|
||||
disk: number[];
|
||||
}
|
||||
|
||||
function parseProxmoxStatsConfig(
|
||||
raw?: string | ProxmoxStatsConfig | null,
|
||||
): Required<Pick<ProxmoxStatsConfig, "pollInterval">> & ProxmoxStatsConfig {
|
||||
const defaults = { pollInterval: DEFAULT_POLL_INTERVAL, nodeName: null };
|
||||
if (!raw) return defaults;
|
||||
try {
|
||||
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
return { ...defaults, ...parsed };
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export function ProxmoxStatsTab(props: ProxmoxStatsProps): React.ReactElement {
|
||||
return (
|
||||
<ConnectionLogProvider>
|
||||
<ProxmoxStatsInner {...props} />
|
||||
</ConnectionLogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ProxmoxStatsInner({
|
||||
hostConfig,
|
||||
title,
|
||||
isVisible = true,
|
||||
isTopbarOpen = true,
|
||||
embedded = false,
|
||||
}: ProxmoxStatsProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { addLog, clearLogs } = useConnectionLog();
|
||||
const pluginApi = usePluginApi();
|
||||
const api = React.useMemo(
|
||||
() => createProxmoxStatsApi(pluginApi),
|
||||
[pluginApi],
|
||||
);
|
||||
const { hosts } = useHosts();
|
||||
|
||||
const [snapshot, setSnapshot] = React.useState<ProxmoxStatsSnapshot | null>(
|
||||
null,
|
||||
);
|
||||
const [histories, setHistories] = React.useState<ProxmoxStatsHistories>({
|
||||
cpu: [],
|
||||
memory: [],
|
||||
disk: [],
|
||||
});
|
||||
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||
const [isPageVisible, setIsPageVisible] = React.useState(!document.hidden);
|
||||
const [viewerSessionId, setViewerSessionId] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const proxmoxSettings = currentHostConfig?.pluginSettings?.proxmox;
|
||||
const statsConfig = React.useMemo(
|
||||
() => parseProxmoxStatsConfig(proxmoxSettings?.proxmoxStatsConfig),
|
||||
[proxmoxSettings?.proxmoxStatsConfig],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onVis = () => setIsPageVisible(!document.hidden);
|
||||
document.addEventListener("visibilitychange", onVis);
|
||||
return () => document.removeEventListener("visibilitychange", onVis);
|
||||
}, []);
|
||||
|
||||
const isActuallyVisible = isVisible && isPageVisible;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!viewerSessionId || !isActuallyVisible) return;
|
||||
const interval = setInterval(() => {
|
||||
api.sendHeartbeat(viewerSessionId).catch(() => {});
|
||||
}, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [viewerSessionId, isActuallyVisible, api]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hostConfig?.id !== currentHostConfig?.id) {
|
||||
setSnapshot(null);
|
||||
setHistories({ cpu: [], memory: [], disk: [] });
|
||||
}
|
||||
setCurrentHostConfig(hostConfig);
|
||||
}, [hostConfig?.id]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hostConfig?.id) return;
|
||||
const updated = hosts.find((h) => String(h.id) === String(hostConfig.id));
|
||||
if (updated) setCurrentHostConfig(updated as unknown as HostConfig);
|
||||
}, [hostConfig?.id, hosts]);
|
||||
|
||||
const pushHistory = React.useCallback((data: ProxmoxStatsSnapshot) => {
|
||||
setHistories((prev) => {
|
||||
const add = (arr: number[], v: number | null | undefined) =>
|
||||
[...arr, v ?? 0].slice(-HISTORY_LEN);
|
||||
return {
|
||||
cpu: add(prev.cpu, data.node?.cpu?.percent),
|
||||
memory: add(prev.memory, data.node?.memory?.percent),
|
||||
disk: add(prev.disk, data.node?.disk?.percent),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const notEnabled = proxmoxSettings?.enableProxmoxStats !== true;
|
||||
const stopPollingRef = React.useRef<(() => void) | null>(null);
|
||||
|
||||
const fetchSnapshot = React.useCallback(async (): Promise<void> => {
|
||||
if (!currentHostConfig?.id) return;
|
||||
|
||||
addLog({
|
||||
type: "info",
|
||||
stage: "stats_connecting",
|
||||
message: t("proxmoxStats.connecting"),
|
||||
});
|
||||
|
||||
const result = await api.startPolling(currentHostConfig.id);
|
||||
if (result.viewerSessionId) setViewerSessionId(result.viewerSessionId);
|
||||
|
||||
addLog({
|
||||
type: "info",
|
||||
stage: "stats_polling",
|
||||
message: t("proxmoxStats.connecting"),
|
||||
});
|
||||
|
||||
const data = await api.getStats(currentHostConfig.id);
|
||||
if (!data) {
|
||||
throw new Error(t("proxmoxStats.connectionFailed"));
|
||||
}
|
||||
|
||||
setSnapshot(data);
|
||||
addLog({
|
||||
type: "success",
|
||||
stage: "connected",
|
||||
message: t("terminal.connected"),
|
||||
});
|
||||
|
||||
const intervalMs =
|
||||
(statsConfig.pollInterval ?? DEFAULT_POLL_INTERVAL) * 1000;
|
||||
let signature = statsChangeKey(data);
|
||||
stopPollingRef.current?.();
|
||||
stopPollingRef.current = runAdaptivePolling(
|
||||
async () => {
|
||||
const next = await api.getStats(currentHostConfig.id);
|
||||
if (!next) throw new Error(t("proxmoxStats.connectionFailed"));
|
||||
const nextSignature = statsChangeKey(next);
|
||||
const changed = nextSignature !== signature;
|
||||
signature = nextSignature;
|
||||
setSnapshot(next);
|
||||
pushHistory(next);
|
||||
return changed;
|
||||
},
|
||||
{
|
||||
minIntervalMs: intervalMs,
|
||||
maxIntervalMs: Math.min(120_000, intervalMs * 6),
|
||||
stablePollsPerStep: 3,
|
||||
},
|
||||
{ runImmediately: false },
|
||||
);
|
||||
}, [
|
||||
currentHostConfig?.id,
|
||||
statsConfig.pollInterval,
|
||||
addLog,
|
||||
t,
|
||||
pushHistory,
|
||||
api,
|
||||
]);
|
||||
|
||||
const retry = useConnectionRetry({
|
||||
connect: async () => {
|
||||
try {
|
||||
await fetchSnapshot();
|
||||
retry.markConnected();
|
||||
} catch (error: unknown) {
|
||||
addLog({
|
||||
type: "error",
|
||||
stage: "error",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("proxmoxStats.connectionFailed"),
|
||||
});
|
||||
retry.markFailed();
|
||||
}
|
||||
},
|
||||
enabled: isPageVisible && !notEnabled && !!currentHostConfig?.id,
|
||||
autoStart: false,
|
||||
});
|
||||
|
||||
const retryRef = React.useRef(retry);
|
||||
retryRef.current = retry;
|
||||
|
||||
// Connects once per host and stays connected while this tab exists, even
|
||||
// when the user switches to another tab and back. Only the browser tab
|
||||
// going into the background (isPageVisible) pauses/resumes it -- switching
|
||||
// between Termix tabs must not tear down and reconnect the session.
|
||||
React.useEffect(() => {
|
||||
if (notEnabled || !currentHostConfig?.id) return;
|
||||
|
||||
let cancelled = false;
|
||||
const debounce = setTimeout(() => {
|
||||
if (isPageVisible && !cancelled) {
|
||||
clearLogs();
|
||||
retryRef.current.reset();
|
||||
retryRef.current.retryNow();
|
||||
} else if (!isPageVisible) {
|
||||
stopPollingRef.current?.();
|
||||
stopPollingRef.current = null;
|
||||
if (currentHostConfig?.id) {
|
||||
api.stopPolling(currentHostConfig.id, undefined).catch(() => {});
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(debounce);
|
||||
stopPollingRef.current?.();
|
||||
stopPollingRef.current = null;
|
||||
if (currentHostConfig?.id) {
|
||||
api.stopPolling(currentHostConfig.id).catch(() => {});
|
||||
}
|
||||
};
|
||||
}, [currentHostConfig?.id, notEnabled, isPageVisible, api]);
|
||||
|
||||
const wrapperStyle: React.CSSProperties = embedded
|
||||
? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" }
|
||||
: {
|
||||
opacity: isVisible ? 1 : 0,
|
||||
margin: isTopbarOpen ? "74px 17px 8px 8px" : "16px 17px 8px 8px",
|
||||
height: isTopbarOpen ? "calc(100vh - 82px)" : "calc(100vh - 24px)",
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!currentHostConfig?.id) return;
|
||||
if (retry.status !== "connected") {
|
||||
retry.retryNow();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsRefreshing(true);
|
||||
const data = await api.getStats(currentHostConfig.id);
|
||||
if (data) {
|
||||
setSnapshot(data);
|
||||
pushHistory(data);
|
||||
}
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const showContent = !notEnabled && retry.status === "connected" && snapshot;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={wrapperStyle}
|
||||
className="relative flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
{retry.status === "connected" && !notEnabled && (
|
||||
<div className="mx-3 mt-3 flex shrink-0 items-center justify-between border border-border bg-card px-3 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center border border-border bg-muted">
|
||||
<Server className="size-5 text-accent-brand" />
|
||||
</div>
|
||||
<h1 className="text-lg font-bold md:text-2xl">{title}</h1>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
className="gap-2 font-semibold"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`size-3.5 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("proxmoxStats.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showContent && (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-3 pb-3 pt-3">
|
||||
<div className="shrink-0">
|
||||
<NodeSummaryStrip node={snapshot.node} histories={histories} />
|
||||
</div>
|
||||
|
||||
<GuestTable guests={snapshot.guests.guests} />
|
||||
|
||||
<div className="grid shrink-0 grid-cols-1 gap-3 pb-1 md:grid-cols-3">
|
||||
<NodeNetworkCard snapshot={snapshot} />
|
||||
<StoragePoolsCard snapshot={snapshot} />
|
||||
{snapshot.cluster.clustered && (
|
||||
<ClusterHealthCard snapshot={snapshot} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConnectionScreen
|
||||
status={notEnabled ? "connected" : retry.status}
|
||||
message={t("proxmoxStats.connecting")}
|
||||
attempt={retry.attempt}
|
||||
maxAttempts={retry.maxAttempts}
|
||||
nextRetryInMs={retry.nextRetryInMs}
|
||||
onManualRetry={retry.retryNow}
|
||||
emptyState={
|
||||
notEnabled ? (
|
||||
<div className="text-center opacity-40">
|
||||
<Server className="mx-auto mb-4 size-16" />
|
||||
<p className="text-xl font-bold uppercase tracking-widest">
|
||||
{t("proxmoxStats.noHostSelected")}
|
||||
</p>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Network } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { MetricCard } from "@termix/plugin-sdk/ui";
|
||||
import type { ProxmoxStatsSnapshot } from "../../types";
|
||||
import { cn } from "@termix/plugin-sdk/ui";
|
||||
|
||||
export function ClusterHealthCard({
|
||||
snapshot,
|
||||
}: {
|
||||
snapshot: ProxmoxStatsSnapshot | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const cluster = snapshot?.cluster;
|
||||
|
||||
return (
|
||||
<MetricCard
|
||||
title={t("proxmoxStats.clusterHealth")}
|
||||
icon={<Network className="size-3.5" />}
|
||||
scroll
|
||||
>
|
||||
{!cluster || !cluster.clustered ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("proxmoxStats.notClustered")}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 shrink-0 rounded-full",
|
||||
cluster.quorate ? "bg-green-500" : "bg-red-500",
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium">
|
||||
{cluster.clusterName ?? t("proxmoxStats.clusterHealth")}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{cluster.quorate
|
||||
? t("proxmoxStats.quorate")
|
||||
: t("proxmoxStats.notQuorate")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col divide-y divide-border">
|
||||
{cluster.nodes.map((node) => (
|
||||
<div key={node.name} className="flex items-center gap-2 py-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"size-1.5 shrink-0 rounded-full",
|
||||
node.online ? "bg-green-500" : "bg-muted-foreground/50",
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate font-medium">
|
||||
{node.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||
{node.online
|
||||
? t("proxmoxStats.nodeOnline")
|
||||
: t("proxmoxStats.nodeOffline")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</MetricCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Network } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { StatRow, MetricCard } from "@termix/plugin-sdk/ui";
|
||||
import type { ProxmoxStatsSnapshot } from "../../types";
|
||||
|
||||
export function NodeNetworkCard({
|
||||
snapshot,
|
||||
}: {
|
||||
snapshot: ProxmoxStatsSnapshot | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const interfaces = snapshot?.network.interfaces ?? [];
|
||||
|
||||
return (
|
||||
<MetricCard
|
||||
title={t("proxmoxStats.networkInterfaces")}
|
||||
icon={<Network className="size-3.5" />}
|
||||
scroll
|
||||
>
|
||||
{interfaces.length === 0 ? (
|
||||
<span className="text-xs text-muted-foreground">N/A</span>
|
||||
) : (
|
||||
<div className="flex flex-col divide-y divide-border">
|
||||
{interfaces.map((iface) => (
|
||||
<StatRow
|
||||
key={iface.name}
|
||||
label={iface.name}
|
||||
value={iface.ip ?? iface.state ?? "-"}
|
||||
mono
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</MetricCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Database } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { MetricCard } from "@termix/plugin-sdk/ui";
|
||||
import type { ProxmoxStatsSnapshot } from "../../types";
|
||||
|
||||
export function StoragePoolsCard({
|
||||
snapshot,
|
||||
}: {
|
||||
snapshot: ProxmoxStatsSnapshot | null;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pools = snapshot?.storage.pools ?? [];
|
||||
|
||||
return (
|
||||
<MetricCard
|
||||
title={t("proxmoxStats.storagePools")}
|
||||
icon={<Database className="size-3.5" />}
|
||||
scroll
|
||||
>
|
||||
{pools.length === 0 ? (
|
||||
<span className="text-xs text-muted-foreground">N/A</span>
|
||||
) : (
|
||||
<div className="flex flex-col divide-y divide-border">
|
||||
{pools.map((pool) => (
|
||||
<div key={pool.name} className="flex flex-col gap-1 py-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate font-medium">
|
||||
{pool.name}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-full border border-border px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">
|
||||
{pool.type}
|
||||
</span>
|
||||
{!pool.active && (
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||
{t("proxmoxStats.inactive")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full bg-accent-brand"
|
||||
style={{
|
||||
width: `${Math.min(100, Math.max(0, pool.percent ?? 0))}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
||||
{pool.usedGiB !== null && pool.totalGiB !== null
|
||||
? `${pool.usedGiB.toFixed(1)} / ${pool.totalGiB.toFixed(1)} GiB`
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</MetricCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { PluginApiClient } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxStatsSnapshot } from "../types";
|
||||
|
||||
/**
|
||||
* The Proxmox node-stats routes, through the plugin's own client. Paths are
|
||||
* relative to /plugin-api/proxmox/, which the client already points at.
|
||||
*/
|
||||
export function createProxmoxStatsApi(api: PluginApiClient) {
|
||||
return {
|
||||
async getStats(hostId: number): Promise<ProxmoxStatsSnapshot | null> {
|
||||
try {
|
||||
const response = await api.get<ProxmoxStatsSnapshot>(
|
||||
`/stats/${hostId}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error &&
|
||||
"response" in error &&
|
||||
(error as { response?: { status?: number } }).response?.status === 404
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async startPolling(hostId: number): Promise<{
|
||||
success: boolean;
|
||||
viewerSessionId?: string;
|
||||
status?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
const response = await api.post<{
|
||||
success: boolean;
|
||||
viewerSessionId?: string;
|
||||
status?: string;
|
||||
error?: string;
|
||||
}>(`/stats/start/${hostId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async stopPolling(hostId: number, viewerSessionId?: string): Promise<void> {
|
||||
await api.post(`/stats/stop/${hostId}`, { viewerSessionId });
|
||||
},
|
||||
|
||||
async sendHeartbeat(viewerSessionId: string): Promise<void> {
|
||||
await api.post("/stats/heartbeat", { viewerSessionId });
|
||||
},
|
||||
|
||||
async getHistory(
|
||||
hostId: number,
|
||||
opts: { range?: string; from?: string; to?: string },
|
||||
): Promise<ProxmoxStatsHistoryResponse> {
|
||||
const response = await api.get<ProxmoxStatsHistoryResponse>(
|
||||
`/stats/history/${hostId}`,
|
||||
{ params: opts },
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type ProxmoxStatsApi = ReturnType<typeof createProxmoxStatsApi>;
|
||||
|
||||
export interface ProxmoxStatsHistoryRow {
|
||||
ts: string;
|
||||
cpu_percent: number | null;
|
||||
mem_percent: number | null;
|
||||
disk_percent: number | null;
|
||||
net_rx_bytes: number | null;
|
||||
net_tx_bytes: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxStatsHistoryResponse {
|
||||
rows: ProxmoxStatsHistoryRow[];
|
||||
fromTs: string;
|
||||
toTs: string;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Frontend-side mirror of the backend Proxmox stats collectors' return
|
||||
// shapes (src/backend/proxmox/*.ts). Kept in sync by hand.
|
||||
|
||||
export interface ProxmoxNodeStats {
|
||||
cpu: {
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
load: [number, number, number] | null;
|
||||
};
|
||||
memory: {
|
||||
percent: number | null;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
};
|
||||
disk: {
|
||||
percent: number | null;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
};
|
||||
uptime: {
|
||||
seconds: number | null;
|
||||
formatted: string | null;
|
||||
};
|
||||
system: {
|
||||
hostname: string | null;
|
||||
kernel: string | null;
|
||||
pveVersion: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProxmoxNodeNetwork {
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
ip: string | null;
|
||||
state: string | null;
|
||||
rxBytes: string | null;
|
||||
txBytes: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ProxmoxGuestSummary {
|
||||
vmid: number;
|
||||
name: string;
|
||||
type: "qemu" | "lxc";
|
||||
status: string;
|
||||
cpuPercent: number | null;
|
||||
memPercent: number | null;
|
||||
memUsedGiB: number | null;
|
||||
memTotalGiB: number | null;
|
||||
diskPercent: number | null;
|
||||
diskUsedGiB: number | null;
|
||||
diskTotalGiB: number | null;
|
||||
uptimeSeconds: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxGuestsSummary {
|
||||
guests: ProxmoxGuestSummary[];
|
||||
counts: { running: number; stopped: number; total: number };
|
||||
}
|
||||
|
||||
export interface ProxmoxStoragePool {
|
||||
name: string;
|
||||
type: string;
|
||||
active: boolean;
|
||||
enabled: boolean;
|
||||
usedGiB: number | null;
|
||||
totalGiB: number | null;
|
||||
availGiB: number | null;
|
||||
percent: number | null;
|
||||
}
|
||||
|
||||
export interface ProxmoxStorage {
|
||||
pools: ProxmoxStoragePool[];
|
||||
}
|
||||
|
||||
export type ProxmoxClusterHealth =
|
||||
| { clustered: false }
|
||||
| {
|
||||
clustered: true;
|
||||
quorate: boolean;
|
||||
clusterName: string | null;
|
||||
nodes: Array<{
|
||||
name: string;
|
||||
online: boolean;
|
||||
local: boolean;
|
||||
ip: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export interface ProxmoxStatsSnapshot {
|
||||
node: ProxmoxNodeStats;
|
||||
network: ProxmoxNodeNetwork;
|
||||
guests: ProxmoxGuestsSummary;
|
||||
storage: ProxmoxStorage;
|
||||
cluster: ProxmoxClusterHealth;
|
||||
lastChecked: string;
|
||||
}
|
||||
|
||||
export interface ProxmoxStatsConfig {
|
||||
nodeName?: string | null;
|
||||
pollInterval?: number;
|
||||
enabledCards?: string[];
|
||||
}
|
||||
|
||||
/** The proxmoxConfig host setting. */
|
||||
export interface ProxmoxHostConfig {
|
||||
source?: {
|
||||
source: "proxmox";
|
||||
sourceHostId: number;
|
||||
node: string;
|
||||
vmid: number;
|
||||
type: "qemu" | "lxc";
|
||||
lastSeenAt?: string;
|
||||
lastStatus?: string;
|
||||
missingSince?: string | null;
|
||||
};
|
||||
defaultCredentialId: number | null;
|
||||
defaultAuthType?: string;
|
||||
windowsPatterns: string;
|
||||
dockerPatterns: string;
|
||||
preferredPrefixes: string;
|
||||
autoSyncEnabled?: boolean;
|
||||
syncIntervalMinutes?: number;
|
||||
markMissingGuests?: boolean;
|
||||
lastSyncAt?: string;
|
||||
lastSyncStatus?: "success" | "error";
|
||||
lastSyncError?: string | null;
|
||||
lastSyncResult?: {
|
||||
created: number;
|
||||
updated: number;
|
||||
markedMissing: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProxmoxGuest {
|
||||
name: string;
|
||||
vmid: number;
|
||||
type: "qemu" | "lxc";
|
||||
node: string;
|
||||
status: string;
|
||||
ip: string | null;
|
||||
connectionType: "ssh" | "rdp";
|
||||
enableDocker: boolean;
|
||||
}
|
||||
|
||||
export interface ProxmoxDiscoverResult {
|
||||
guests: ProxmoxGuest[];
|
||||
credentialId: number | null;
|
||||
defaultCredentialId: number | null;
|
||||
jumpHosts?: unknown[] | null;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getProxmoxSource,
|
||||
indexImportedGuests,
|
||||
proxmoxSourceKey,
|
||||
} from "../../src/backend/guest-sync.js";
|
||||
|
||||
const source = {
|
||||
source: "proxmox",
|
||||
sourceHostId: 1,
|
||||
node: "pve",
|
||||
vmid: 101,
|
||||
type: "qemu",
|
||||
};
|
||||
|
||||
describe("getProxmoxSource", () => {
|
||||
it("reads the source from an object or a JSON string", () => {
|
||||
expect(getProxmoxSource({ source })).toEqual(source);
|
||||
expect(getProxmoxSource(JSON.stringify({ source }))).toEqual(source);
|
||||
});
|
||||
|
||||
it("refuses a malformed source", () => {
|
||||
expect(getProxmoxSource(null)).toBeNull();
|
||||
expect(getProxmoxSource({ source: { ...source, vmid: "101" } })).toBeNull();
|
||||
expect(getProxmoxSource({ source: { ...source, type: "vm" } })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("indexImportedGuests", () => {
|
||||
// The host row carries no proxmoxConfig since 2.9.0; the source only
|
||||
// exists in the plugin's host settings.
|
||||
const hosts = [
|
||||
{ id: 10, name: "guest-a" },
|
||||
{ id: 11, name: "guest-b" },
|
||||
{ id: 12, name: "manual" },
|
||||
];
|
||||
|
||||
it("matches hosts to guests through their settings", () => {
|
||||
const index = indexImportedGuests(
|
||||
hosts,
|
||||
[
|
||||
{ hostId: 10, value: { source, lastSyncAt: "x" } },
|
||||
{ hostId: 11, value: { source: { ...source, sourceHostId: 2 } } },
|
||||
],
|
||||
1,
|
||||
);
|
||||
expect([...index.keys()]).toEqual([proxmoxSourceKey(source as never)]);
|
||||
const entry = index.get("1:pve:qemu:101")!;
|
||||
expect(entry.host.id).toBe(10);
|
||||
expect(entry.config.lastSyncAt).toBe("x");
|
||||
});
|
||||
|
||||
it("ignores settings for hosts the user does not own", () => {
|
||||
const index = indexImportedGuests(
|
||||
hosts,
|
||||
[{ hostId: 99, value: { source } }],
|
||||
1,
|
||||
);
|
||||
expect(index.size).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const pluginDir = fileURLToPath(new URL("../..", import.meta.url));
|
||||
@@ -0,0 +1,99 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
|
||||
import { pluginDir } from "./helpers.js";
|
||||
|
||||
// proxmox_node_history and proxmox_stats_preferences as core's SQLite
|
||||
// bootstrap created them before 2.9.0.
|
||||
const LEGACY_DDL = `
|
||||
CREATE TABLE proxmox_node_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE,
|
||||
ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
cpu_percent REAL,
|
||||
mem_percent REAL,
|
||||
disk_percent REAL,
|
||||
net_rx_bytes INTEGER,
|
||||
net_tx_bytes INTEGER
|
||||
);
|
||||
CREATE TABLE proxmox_stats_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
host_id INTEGER NOT NULL REFERENCES ssh_data(id) ON DELETE CASCADE,
|
||||
layout TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_proxmox_stats_prefs_user_host ON proxmox_stats_preferences (user_id, host_id);
|
||||
`;
|
||||
|
||||
let db: TestDb | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
db?.close();
|
||||
db = null;
|
||||
});
|
||||
|
||||
const tableExists = (name: string) =>
|
||||
!!db!.sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
||||
.get(name);
|
||||
|
||||
const indexOn = (table: string) =>
|
||||
(
|
||||
db!.sqlite
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND name NOT LIKE 'sqlite_%'",
|
||||
)
|
||||
.all(table) as { name: string }[]
|
||||
).map((row) => row.name);
|
||||
|
||||
describe("adopting proxmox_node_history and proxmox_stats_preferences", () => {
|
||||
it("keeps every existing row when the legacy tables are there", async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
sqlite.exec(
|
||||
"INSERT INTO users (id, username) VALUES ('user-1', 'user-1')",
|
||||
);
|
||||
sqlite.exec("INSERT INTO ssh_data (id) VALUES (1)");
|
||||
sqlite.exec(LEGACY_DDL);
|
||||
sqlite
|
||||
.prepare(
|
||||
"INSERT INTO proxmox_node_history (host_id, ts, cpu_percent) VALUES (?, ?, ?)",
|
||||
)
|
||||
.run(1, "2026-01-01 00:00:00", 42);
|
||||
sqlite
|
||||
.prepare(
|
||||
"INSERT INTO proxmox_stats_preferences (user_id, host_id, layout) VALUES (?, ?, ?)",
|
||||
)
|
||||
.run("user-1", 1, '{"cards":[]}');
|
||||
},
|
||||
});
|
||||
|
||||
expect(db.applied).toEqual(["0001_adopt_proxmox_stats"]);
|
||||
expect(tableExists("proxmox_node_history")).toBe(false);
|
||||
expect(tableExists("proxmox_stats_preferences")).toBe(false);
|
||||
expect(tableExists("p_proxmox_node_history")).toBe(true);
|
||||
expect(tableExists("p_proxmox_stats_preferences")).toBe(true);
|
||||
|
||||
expect(
|
||||
db.sqlite.prepare("SELECT cpu_percent FROM p_proxmox_node_history").all(),
|
||||
).toEqual([{ cpu_percent: 42 }]);
|
||||
expect(
|
||||
db.sqlite.prepare("SELECT layout FROM p_proxmox_stats_preferences").all(),
|
||||
).toEqual([{ layout: '{"cards":[]}' }]);
|
||||
expect(indexOn("p_proxmox_stats_preferences")).toEqual([
|
||||
"idx_proxmox_stats_prefs_user_host",
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates the tables fresh when there is no legacy one", async () => {
|
||||
db = await createTestDb(pluginDir);
|
||||
|
||||
expect(tableExists("proxmox_node_history")).toBe(false);
|
||||
expect(tableExists("p_proxmox_node_history")).toBe(true);
|
||||
expect(tableExists("p_proxmox_stats_preferences")).toBe(true);
|
||||
expect(
|
||||
db.sqlite.prepare("SELECT * FROM p_proxmox_node_history").all(),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProxmoxImportAuth } from "../../src/backend/proxmox-import-auth.js";
|
||||
|
||||
// The frontend carries its own copy of this decision in
|
||||
// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is
|
||||
// what produced the reported bug, so both are held to the same matrix.
|
||||
describe("resolveProxmoxImportAuth", () => {
|
||||
it("uses the default credential for key auth when one is configured", () => {
|
||||
expect(resolveProxmoxImportAuth("key", 7)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 7,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default credential for password auth when one is configured", () => {
|
||||
expect(resolveProxmoxImportAuth("password", 7)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 7,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to none when a secret-backed default has no credential", () => {
|
||||
for (const authType of ["password", "key", "credential"]) {
|
||||
expect(resolveProxmoxImportAuth(authType, null)).toEqual({
|
||||
authType: "none",
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the credential when no default auth type is configured", () => {
|
||||
expect(resolveProxmoxImportAuth(undefined, 42)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 42,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
expect(resolveProxmoxImportAuth(undefined, null)).toEqual({
|
||||
authType: "none",
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps secretless auth types, with or without a credential", () => {
|
||||
for (const authType of ["none", "agent", "opkssh", "tailscale", "vault"]) {
|
||||
for (const credentialId of [null, 7]) {
|
||||
expect(resolveProxmoxImportAuth(authType, credentialId)).toEqual({
|
||||
authType,
|
||||
credentialId: null,
|
||||
overrideCredentialUsername: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseProxmoxJumpHosts } from "../../src/backend/proxmox-jump-hosts.js";
|
||||
|
||||
describe("Proxmox jump-host persistence", () => {
|
||||
it("reads a resolved array or a stored JSON string", () => {
|
||||
expect(parseProxmoxJumpHosts([{ hostId: 7 }])).toEqual([{ hostId: 7 }]);
|
||||
expect(parseProxmoxJumpHosts('[{"hostId":7}]')).toEqual([{ hostId: 7 }]);
|
||||
});
|
||||
|
||||
it("turns missing or malformed values into null", () => {
|
||||
expect(parseProxmoxJumpHosts(null)).toBeNull();
|
||||
expect(parseProxmoxJumpHosts("invalid")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("../../src/backend/plugin-ctx.js", () => ({
|
||||
pluginCtx: () => ({
|
||||
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
}),
|
||||
}));
|
||||
|
||||
const collectProxmoxStats = vi.fn();
|
||||
vi.mock("../../src/backend/proxmox/collect-proxmox-stats.js", () => ({
|
||||
collectProxmoxStats: (...args: unknown[]) => collectProxmoxStats(...args),
|
||||
}));
|
||||
|
||||
import {
|
||||
ProxmoxPollingManager,
|
||||
parseProxmoxStatsConfig,
|
||||
} from "../../src/backend/proxmox-stats-polling.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
interface TestHost {
|
||||
id: number;
|
||||
userId: string;
|
||||
proxmoxStatsConfig?: string | null;
|
||||
}
|
||||
|
||||
function snapshot(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
return {
|
||||
node: {
|
||||
cpu: { percent: 10, cores: 4, load: [0.1, 0.2, 0.3] },
|
||||
memory: { percent: 20, usedGiB: 2, totalGiB: 8 },
|
||||
disk: { percent: 30, usedGiB: 30, totalGiB: 100 },
|
||||
uptime: { seconds: 100, formatted: "0d 0h 1m" },
|
||||
system: { hostname: "pve1", kernel: "6.8", pveVersion: "8.2" },
|
||||
},
|
||||
network: { interfaces: [{ name: "eth0", rxBytes: "10", txBytes: "20" }] },
|
||||
guests: { guests: [], counts: { running: 0, stopped: 0, total: 0 } },
|
||||
storage: { pools: [] },
|
||||
cluster: { clustered: false },
|
||||
lastChecked: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
collectProxmoxStats.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("parseProxmoxStatsConfig", () => {
|
||||
it("returns default poll interval when config is missing", () => {
|
||||
expect(parseProxmoxStatsConfig(null)).toEqual({
|
||||
nodeName: null,
|
||||
pollInterval: 60,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses a JSON string config", () => {
|
||||
expect(
|
||||
parseProxmoxStatsConfig('{"nodeName":"pve1","pollInterval":30}'),
|
||||
).toEqual({ nodeName: "pve1", pollInterval: 30 });
|
||||
});
|
||||
|
||||
it("falls back to defaults on malformed JSON", () => {
|
||||
expect(parseProxmoxStatsConfig("{not json")).toEqual({
|
||||
nodeName: null,
|
||||
pollInterval: 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProxmoxPollingManager", () => {
|
||||
function makeManager(host: TestHost) {
|
||||
const fetchHostById = vi.fn(async () => host);
|
||||
const withSshConnection = vi.fn(
|
||||
async (_host: TestHost, fn: (client: Client) => Promise<unknown>) =>
|
||||
fn({} as Client),
|
||||
);
|
||||
const historyRepository = {
|
||||
create: vi.fn(async () => {}),
|
||||
pruneOlderThan: vi.fn(async () => {}),
|
||||
listRange: vi.fn(async () => []),
|
||||
};
|
||||
const manager = new ProxmoxPollingManager<TestHost>({
|
||||
fetchHostById,
|
||||
withSshConnection: withSshConnection as never,
|
||||
historyRepository,
|
||||
});
|
||||
return { manager, fetchHostById, withSshConnection, historyRepository };
|
||||
}
|
||||
|
||||
it("starts polling and caches a snapshot when the first viewer registers", async () => {
|
||||
const host: TestHost = { id: 1, userId: "user-1" };
|
||||
collectProxmoxStats.mockResolvedValue(snapshot());
|
||||
const { manager, withSshConnection } = makeManager(host);
|
||||
|
||||
manager.registerViewer(1, "viewer-1", "user-1");
|
||||
// registerViewer kicks off polling via a fire-and-forget promise chain.
|
||||
await vi.waitFor(() => {
|
||||
expect(withSshConnection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const cached = manager.getStats(1);
|
||||
expect(cached?.data.node.cpu.percent).toBe(10);
|
||||
manager.destroy();
|
||||
});
|
||||
|
||||
it("stops polling once the last viewer unregisters", async () => {
|
||||
const host: TestHost = { id: 2, userId: "user-1" };
|
||||
collectProxmoxStats.mockResolvedValue(snapshot());
|
||||
const { manager } = makeManager(host);
|
||||
|
||||
manager.registerViewer(2, "viewer-a", "user-1");
|
||||
manager.registerViewer(2, "viewer-b", "user-1");
|
||||
await vi.waitFor(() => expect(manager.getStats(2)).toBeDefined());
|
||||
|
||||
manager.unregisterViewer(2, "viewer-a");
|
||||
// one viewer left - stats stay cached
|
||||
expect(manager.getStats(2)).toBeDefined();
|
||||
|
||||
manager.unregisterViewer(2, "viewer-b");
|
||||
// Cached snapshot is retained (not cleared) but the interval is stopped;
|
||||
// registering a new viewer must restart polling.
|
||||
manager.destroy();
|
||||
});
|
||||
|
||||
it("updateHeartbeat returns false for an unknown session", () => {
|
||||
const { manager } = makeManager({ id: 3, userId: "user-1" });
|
||||
expect(manager.updateHeartbeat("nope")).toBe(false);
|
||||
manager.destroy();
|
||||
});
|
||||
|
||||
it("records an error snapshot when collection fails, without throwing", async () => {
|
||||
const host: TestHost = { id: 4, userId: "user-1" };
|
||||
collectProxmoxStats.mockRejectedValue(new Error("pvesh not found"));
|
||||
const { manager } = makeManager(host);
|
||||
|
||||
manager.registerViewer(4, "viewer-1", "user-1");
|
||||
await vi.waitFor(() => {
|
||||
expect(manager.getError(4)?.error).toBe("pvesh not found");
|
||||
});
|
||||
expect(manager.getStats(4)).toBeUndefined();
|
||||
manager.destroy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const execElevated = vi.fn();
|
||||
vi.mock("@termix/plugin-sdk/host-commands", async (importOriginal) => ({
|
||||
...(await importOriginal<object>()),
|
||||
execElevated: (...args: unknown[]) => execElevated(...args),
|
||||
}));
|
||||
|
||||
const { execPveshCommand } = await import("../../src/backend/routes.js");
|
||||
|
||||
const client = {} as Client;
|
||||
|
||||
describe("Proxmox discovery sudo execution", () => {
|
||||
beforeEach(() => {
|
||||
execElevated.mockReset();
|
||||
});
|
||||
|
||||
it("forces pvesh through the stored sudo credentials", async () => {
|
||||
execElevated.mockResolvedValue({
|
||||
stdout: '[{"type":"qemu"}]',
|
||||
stderr: "",
|
||||
code: 0,
|
||||
usedSudo: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
execPveshCommand(client, "pvesh get /cluster/resources", "secret", 12000),
|
||||
).resolves.toBe('[{"type":"qemu"}]');
|
||||
expect(execElevated).toHaveBeenCalledWith(
|
||||
client,
|
||||
"pvesh get /cluster/resources",
|
||||
"secret",
|
||||
{ forceSudo: true, timeoutMs: 12000 },
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces a failed elevated pvesh command", async () => {
|
||||
execElevated.mockResolvedValue({
|
||||
stdout: "",
|
||||
stderr: "pvesh failed",
|
||||
code: 255,
|
||||
usedSudo: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
execPveshCommand(client, "pvesh get /cluster/resources", "secret"),
|
||||
).rejects.toThrow("pvesh failed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
}));
|
||||
|
||||
import { collectProxmoxClusterHealth } from "../../../src/backend/proxmox/cluster-health-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
function result(stdout: string, code: number | null = 0) {
|
||||
return { stdout, stderr: "", code };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execCommand.mockReset();
|
||||
});
|
||||
|
||||
describe("collectProxmoxClusterHealth", () => {
|
||||
it("reports clustered:false for a standalone node (no cluster entry)", async () => {
|
||||
execCommand.mockResolvedValueOnce(result(JSON.stringify([]), 0));
|
||||
const res = await collectProxmoxClusterHealth(fakeClient);
|
||||
expect(res).toEqual({ clustered: false });
|
||||
});
|
||||
|
||||
it("parses a clustered response with quorum and node entries", async () => {
|
||||
execCommand.mockResolvedValueOnce(
|
||||
result(
|
||||
JSON.stringify([
|
||||
{ type: "cluster", name: "prod-cluster", quorate: 1, nodes: 3 },
|
||||
{ type: "node", name: "pve1", online: 1, local: 1, ip: "10.0.0.1" },
|
||||
{ type: "node", name: "pve2", online: 0, local: 0, ip: "10.0.0.2" },
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const res = await collectProxmoxClusterHealth(fakeClient);
|
||||
expect(res.clustered).toBe(true);
|
||||
if (res.clustered) {
|
||||
expect(res.quorate).toBe(true);
|
||||
expect(res.clusterName).toBe("prod-cluster");
|
||||
expect(res.nodes).toHaveLength(2);
|
||||
expect(res.nodes[0]).toEqual({
|
||||
name: "pve1",
|
||||
online: true,
|
||||
local: true,
|
||||
ip: "10.0.0.1",
|
||||
});
|
||||
expect(res.nodes[1].online).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns clustered:false when pvesh fails", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("", 1));
|
||||
const res = await collectProxmoxClusterHealth(fakeClient);
|
||||
expect(res).toEqual({ clustered: false });
|
||||
});
|
||||
|
||||
it("returns clustered:false on malformed JSON", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("not json", 0));
|
||||
const res = await collectProxmoxClusterHealth(fakeClient);
|
||||
expect(res).toEqual({ clustered: false });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxStats } from "../../../src/backend/proxmox/collect-proxmox-stats.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
function result(stdout: string, code: number | null = 0) {
|
||||
return { stdout, stderr: "", code };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execCommand.mockReset();
|
||||
});
|
||||
|
||||
describe("collectProxmoxStats", () => {
|
||||
it("throws a distinguishable error when pvesh is missing", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("missing"));
|
||||
|
||||
await expect(collectProxmoxStats(fakeClient, null)).rejects.toThrow(
|
||||
/pvesh not found/i,
|
||||
);
|
||||
// Only the pvesh-presence check should have run - no node resolution or
|
||||
// per-collector execs once that check fails.
|
||||
expect(execCommand).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("auto-detects the node name via hostname when none is configured", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("ok")); // pvesh check
|
||||
execCommand.mockResolvedValueOnce(result("pve-auto\n")); // hostname
|
||||
// Five collectors run concurrently after that; give each a benign failing
|
||||
// response so the aggregator still resolves with null-filled sub-shapes.
|
||||
execCommand.mockResolvedValue(result("", 1));
|
||||
|
||||
const snapshot = await collectProxmoxStats(fakeClient, null);
|
||||
expect(snapshot.lastChecked).toBeTruthy();
|
||||
expect(snapshot.node).toBeDefined();
|
||||
expect(snapshot.guests).toBeDefined();
|
||||
expect(snapshot.storage).toBeDefined();
|
||||
expect(snapshot.cluster).toEqual({ clustered: false });
|
||||
});
|
||||
|
||||
it("uses the configured node name when it is safe, skipping hostname detection", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("ok")); // pvesh check
|
||||
execCommand.mockResolvedValue(result("", 1)); // all collector calls fail benignly
|
||||
|
||||
await collectProxmoxStats(fakeClient, "my-node");
|
||||
|
||||
// hostname auto-detection command should never have been issued.
|
||||
const calls = execCommand.mock.calls.map((c) => c[1] as string);
|
||||
expect(calls).not.toContain("hostname");
|
||||
});
|
||||
|
||||
it("rejects an unsafe configured node name", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("ok")); // pvesh check
|
||||
|
||||
await expect(collectProxmoxStats(fakeClient, "bad;node")).rejects.toThrow(
|
||||
/valid Proxmox node name/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxGuestsSummary } from "../../../src/backend/proxmox/guests-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
function result(stdout: string, code: number | null = 0) {
|
||||
return { stdout, stderr: "", code };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execCommand.mockReset();
|
||||
});
|
||||
|
||||
describe("collectProxmoxGuestsSummary", () => {
|
||||
it("filters to the target node, excludes templates, and computes percentages", async () => {
|
||||
execCommand.mockResolvedValueOnce(
|
||||
result(
|
||||
JSON.stringify([
|
||||
{
|
||||
type: "qemu",
|
||||
node: "pve1",
|
||||
vmid: 100,
|
||||
name: "vm-a",
|
||||
status: "running",
|
||||
cpu: 0.25,
|
||||
mem: 2 * 1024 ** 3,
|
||||
maxmem: 4 * 1024 ** 3,
|
||||
disk: 10 * 1024 ** 3,
|
||||
maxdisk: 40 * 1024 ** 3,
|
||||
uptime: 3600,
|
||||
},
|
||||
{
|
||||
type: "lxc",
|
||||
node: "pve1",
|
||||
vmid: 101,
|
||||
name: "ct-b",
|
||||
status: "stopped",
|
||||
cpu: 0,
|
||||
mem: 0,
|
||||
maxmem: 512 * 1024 ** 2,
|
||||
disk: 0,
|
||||
maxdisk: 8 * 1024 ** 3,
|
||||
uptime: 0,
|
||||
},
|
||||
{
|
||||
// different node - excluded
|
||||
type: "qemu",
|
||||
node: "pve2",
|
||||
vmid: 200,
|
||||
name: "elsewhere",
|
||||
status: "running",
|
||||
},
|
||||
{
|
||||
// template - excluded
|
||||
type: "qemu",
|
||||
node: "pve1",
|
||||
vmid: 999,
|
||||
name: "template",
|
||||
status: "stopped",
|
||||
template: 1,
|
||||
},
|
||||
{
|
||||
// non-guest resource type - excluded
|
||||
type: "storage",
|
||||
node: "pve1",
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const res = await collectProxmoxGuestsSummary(fakeClient, "pve1");
|
||||
|
||||
expect(res.guests).toHaveLength(2);
|
||||
expect(res.counts).toEqual({ running: 1, stopped: 1, total: 2 });
|
||||
|
||||
const vmA = res.guests.find((g) => g.vmid === 100)!;
|
||||
expect(vmA.cpuPercent).toBe(25);
|
||||
expect(vmA.memPercent).toBe(50);
|
||||
expect(vmA.diskPercent).toBe(25);
|
||||
});
|
||||
|
||||
it("reports null disk fields (not a false 0%) when maxdisk is 0", async () => {
|
||||
execCommand.mockResolvedValueOnce(
|
||||
result(
|
||||
JSON.stringify([
|
||||
{
|
||||
type: "qemu",
|
||||
node: "pve1",
|
||||
vmid: 100,
|
||||
name: "vm-no-agent",
|
||||
status: "running",
|
||||
cpu: 0.1,
|
||||
mem: 1024,
|
||||
maxmem: 2048,
|
||||
disk: 0,
|
||||
maxdisk: 0,
|
||||
uptime: 10,
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const res = await collectProxmoxGuestsSummary(fakeClient, "pve1");
|
||||
expect(res.guests[0].diskPercent).toBeNull();
|
||||
expect(res.guests[0].diskUsedGiB).toBeNull();
|
||||
expect(res.guests[0].diskTotalGiB).toBeNull();
|
||||
});
|
||||
|
||||
it("returns an empty guest list when there are no matching resources", async () => {
|
||||
execCommand.mockResolvedValueOnce(result(JSON.stringify([]), 0));
|
||||
const res = await collectProxmoxGuestsSummary(fakeClient, "pve1");
|
||||
expect(res.guests).toEqual([]);
|
||||
expect(res.counts).toEqual({ running: 0, stopped: 0, total: 0 });
|
||||
});
|
||||
|
||||
it("returns an empty result when pvesh is missing (non-zero exit)", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("", 127));
|
||||
const res = await collectProxmoxGuestsSummary(fakeClient, "pve1");
|
||||
expect(res.guests).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty result on malformed JSON", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("{not json", 0));
|
||||
const res = await collectProxmoxGuestsSummary(fakeClient, "pve1");
|
||||
expect(res.guests).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects an unsafe node name before running any command", async () => {
|
||||
const res = await collectProxmoxGuestsSummary(fakeClient, "../etc");
|
||||
expect(res.guests).toEqual([]);
|
||||
expect(execCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
}));
|
||||
|
||||
import { collectProxmoxNodeNetwork } from "../../../src/backend/proxmox/node-network-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
function result(stdout: string, code: number | null = 0) {
|
||||
return { stdout, stderr: "", code };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execCommand.mockReset();
|
||||
});
|
||||
|
||||
describe("collectProxmoxNodeNetwork", () => {
|
||||
it("rejects an unsafe node name before running any command", async () => {
|
||||
const res = await collectProxmoxNodeNetwork(fakeClient, "bad;name");
|
||||
expect(res.interfaces).toEqual([]);
|
||||
expect(execCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to /proc/net/dev when pvesh netstat fails", async () => {
|
||||
// First call: pvesh netstat -> failure.
|
||||
execCommand.mockResolvedValueOnce(result("", 1));
|
||||
// Fallback calls: ip addr, ip link, /proc/net/dev.
|
||||
execCommand.mockResolvedValueOnce(result("eth0 10.0.0.5/24\n"));
|
||||
execCommand.mockResolvedValueOnce(result("eth0 UP\n"));
|
||||
execCommand.mockResolvedValueOnce(
|
||||
result(
|
||||
"Inter-| Receive\n" +
|
||||
" face |bytes packets\n" +
|
||||
"eth0: 123456 10 0 0 0 0 0 0 654321 20 0 0 0 0 0 0\n",
|
||||
),
|
||||
);
|
||||
|
||||
const res = await collectProxmoxNodeNetwork(fakeClient, "pve1");
|
||||
expect(res.interfaces).toHaveLength(1);
|
||||
expect(res.interfaces[0]).toMatchObject({
|
||||
name: "eth0",
|
||||
ip: "10.0.0.5",
|
||||
state: "UP",
|
||||
rxBytes: "123456",
|
||||
txBytes: "654321",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to /proc/net/dev when pvesh returns unparseable data", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("not json", 0));
|
||||
execCommand.mockResolvedValueOnce(result(""));
|
||||
execCommand.mockResolvedValueOnce(result(""));
|
||||
execCommand.mockResolvedValueOnce(result("Inter-| Receive\n face |\n"));
|
||||
|
||||
const res = await collectProxmoxNodeNetwork(fakeClient, "pve1");
|
||||
expect(res.interfaces).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxNodeStatus } from "../../../src/backend/proxmox/node-status-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
function result(stdout: string, code: number | null = 0) {
|
||||
return { stdout, stderr: "", code };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execCommand.mockReset();
|
||||
});
|
||||
|
||||
describe("collectProxmoxNodeStatus", () => {
|
||||
it("parses a healthy pvesh /nodes/{node}/status response", async () => {
|
||||
execCommand.mockResolvedValueOnce(
|
||||
result(
|
||||
JSON.stringify({
|
||||
cpu: 0.15,
|
||||
cpuinfo: { cores: 8 },
|
||||
loadavg: ["0.5", "0.6", "0.7"],
|
||||
memory: { used: 4 * 1024 ** 3, total: 16 * 1024 ** 3 },
|
||||
rootfs: { used: 20 * 1024 ** 3, total: 100 * 1024 ** 3 },
|
||||
uptime: 90061,
|
||||
hostname: "pve1",
|
||||
kversion: "Linux 6.8.0",
|
||||
pveversion: "pve-manager/8.2.0",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const res = await collectProxmoxNodeStatus(fakeClient, "pve1");
|
||||
|
||||
expect(res.cpu.percent).toBe(15);
|
||||
expect(res.cpu.cores).toBe(8);
|
||||
expect(res.cpu.load).toEqual([0.5, 0.6, 0.7]);
|
||||
expect(res.memory.percent).toBe(25);
|
||||
expect(res.memory.usedGiB).toBeCloseTo(4, 1);
|
||||
expect(res.memory.totalGiB).toBeCloseTo(16, 1);
|
||||
expect(res.disk.percent).toBe(20);
|
||||
expect(res.uptime.seconds).toBe(90061);
|
||||
expect(res.uptime.formatted).toBe("1d 1h 1m");
|
||||
expect(res.system.hostname).toBe("pve1");
|
||||
expect(res.system.kernel).toBe("Linux 6.8.0");
|
||||
expect(res.system.pveVersion).toBe("pve-manager/8.2.0");
|
||||
});
|
||||
|
||||
it("returns a fully null-filled shape when pvesh exits non-zero", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("", 1));
|
||||
const res = await collectProxmoxNodeStatus(fakeClient, "pve1");
|
||||
expect(res.cpu.percent).toBeNull();
|
||||
expect(res.memory.percent).toBeNull();
|
||||
expect(res.disk.percent).toBeNull();
|
||||
expect(res.uptime.seconds).toBeNull();
|
||||
expect(res.system.hostname).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null-filled shape on malformed JSON", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("not json", 0));
|
||||
const res = await collectProxmoxNodeStatus(fakeClient, "pve1");
|
||||
expect(res.cpu.percent).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects an unsafe node name before running any command", async () => {
|
||||
const res = await collectProxmoxNodeStatus(fakeClient, "pve1; rm -rf /");
|
||||
expect(res.cpu.percent).toBeNull();
|
||||
expect(execCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const execCommand = vi.fn();
|
||||
vi.mock("../../../src/backend/proxmox/common-utils.js", () => ({
|
||||
execCommand: (...args: unknown[]) => execCommand(...args),
|
||||
toFixedNum: (n: number | null | undefined, digits = 2) => {
|
||||
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
||||
return Number(n.toFixed(digits));
|
||||
},
|
||||
}));
|
||||
|
||||
import { collectProxmoxStorage } from "../../../src/backend/proxmox/storage-collector.js";
|
||||
import type { Client } from "ssh2";
|
||||
|
||||
const fakeClient = {} as Client;
|
||||
|
||||
function result(stdout: string, code: number | null = 0) {
|
||||
return { stdout, stderr: "", code };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
execCommand.mockReset();
|
||||
});
|
||||
|
||||
describe("collectProxmoxStorage", () => {
|
||||
it("parses storage pools with usage percentages", async () => {
|
||||
execCommand.mockResolvedValueOnce(
|
||||
result(
|
||||
JSON.stringify([
|
||||
{
|
||||
storage: "local",
|
||||
type: "dir",
|
||||
active: 1,
|
||||
enabled: 1,
|
||||
used: 20 * 1024 ** 3,
|
||||
total: 100 * 1024 ** 3,
|
||||
avail: 80 * 1024 ** 3,
|
||||
},
|
||||
{
|
||||
storage: "local-zfs",
|
||||
type: "zfspool",
|
||||
active: 0,
|
||||
enabled: 1,
|
||||
used: 0,
|
||||
total: 0,
|
||||
avail: 0,
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const res = await collectProxmoxStorage(fakeClient, "pve1");
|
||||
expect(res.pools).toHaveLength(2);
|
||||
expect(res.pools[0].name).toBe("local");
|
||||
expect(res.pools[0].active).toBe(true);
|
||||
expect(res.pools[0].percent).toBe(20);
|
||||
expect(res.pools[1].active).toBe(false);
|
||||
expect(res.pools[1].percent).toBeNull();
|
||||
});
|
||||
|
||||
it("returns an empty pool list when pvesh fails", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("", 1));
|
||||
const res = await collectProxmoxStorage(fakeClient, "pve1");
|
||||
expect(res.pools).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns an empty pool list on malformed JSON", async () => {
|
||||
execCommand.mockResolvedValueOnce(result("nope", 0));
|
||||
const res = await collectProxmoxStorage(fakeClient, "pve1");
|
||||
expect(res.pools).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects an unsafe node name before running any command", async () => {
|
||||
const res = await collectProxmoxStorage(fakeClient, "$(whoami)");
|
||||
expect(res.pools).toEqual([]);
|
||||
expect(execCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
|
||||
import { proxmoxNodeHistory } from "../../../src/backend/tables.js";
|
||||
import {
|
||||
createProxmoxNodeHistoryRepository,
|
||||
type ProxmoxNodeHistoryRepository,
|
||||
} from "../../../src/backend/proxmox-node-history-repository.js";
|
||||
import { pluginDir } from "../helpers.js";
|
||||
|
||||
describe("ProxmoxNodeHistoryRepository", () => {
|
||||
let db: TestDb | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
db?.close();
|
||||
db = null;
|
||||
});
|
||||
|
||||
async function createRepository(): Promise<ProxmoxNodeHistoryRepository> {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
sqlite.exec(
|
||||
"INSERT INTO users (id, username) VALUES ('user-1', 'user-1'), ('user-2', 'user-2')",
|
||||
);
|
||||
sqlite.exec("INSERT INTO ssh_data (id) VALUES (1), (2)");
|
||||
},
|
||||
skipMigrations: false,
|
||||
});
|
||||
// Migrations already ran (adopting proxmox_node_history), so seed rows on
|
||||
// the real table after the fact.
|
||||
db.sqlite.exec(`
|
||||
INSERT INTO p_proxmox_node_history (
|
||||
host_id, ts, cpu_percent, mem_percent, disk_percent, net_rx_bytes, net_tx_bytes
|
||||
)
|
||||
VALUES
|
||||
(1, '2026-01-01 00:00:00', 10, 20, 30, 100, 200),
|
||||
(1, '2026-01-02 00:00:00', 11, 21, 31, 101, 201),
|
||||
(1, '2999-01-01 00:00:00', 12, 22, 32, 102, 202),
|
||||
(2, '2026-01-02 00:00:00', 99, 99, 99, 999, 999);
|
||||
`);
|
||||
|
||||
return createProxmoxNodeHistoryRepository(
|
||||
db.database,
|
||||
await db.database.define(proxmoxNodeHistory),
|
||||
);
|
||||
}
|
||||
|
||||
it("creates and lists node history rows by range", async () => {
|
||||
const repo = await createRepository();
|
||||
|
||||
await repo.create({
|
||||
hostId: 1,
|
||||
cpuPercent: 12,
|
||||
memPercent: 22,
|
||||
diskPercent: 32,
|
||||
netRxBytes: 102,
|
||||
netTxBytes: 202,
|
||||
});
|
||||
|
||||
const rows = await repo.listRange(
|
||||
1,
|
||||
"2026-01-01 00:00:00",
|
||||
"2026-01-02 23:59:59",
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.cpuPercent)).toEqual([10, 11]);
|
||||
expect(db!.persisted).toBe(1);
|
||||
});
|
||||
|
||||
it("prunes old history for a host only", async () => {
|
||||
const repo = await createRepository();
|
||||
|
||||
await repo.pruneOlderThan(1, 1);
|
||||
|
||||
const rows = await repo.listRange(
|
||||
1,
|
||||
"2000-01-01 00:00:00",
|
||||
"2999-12-31 23:59:59",
|
||||
);
|
||||
expect(rows.map((row) => row.ts)).toEqual(["2999-01-01 00:00:00"]);
|
||||
expect(
|
||||
await repo.listRange(2, "2026-01-01 00:00:00", "2026-01-03 00:00:00"),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { guestPluginSettings } from "../../src/backend/routes.js";
|
||||
|
||||
describe("guestPluginSettings", () => {
|
||||
it("names the plugin that keeps each switch an imported guest needs", () => {
|
||||
expect(guestPluginSettings("rdp", true)).toEqual({
|
||||
"remote-desktop": { enableRdp: true, rdpPort: 3389 },
|
||||
docker: { enableDocker: true },
|
||||
});
|
||||
expect(guestPluginSettings("ssh", false)).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
renderWithApp,
|
||||
type RenderedPluginApp,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import * as plugin from "../../src/frontend/index";
|
||||
import manifestJson from "../../manifest.json";
|
||||
import locales from "../../locales/en.json";
|
||||
|
||||
const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
const declared = (views?: Array<{ id: string }>) =>
|
||||
(views ?? []).map((view) => view.id).sort();
|
||||
|
||||
let rendered: RenderedPluginApp | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
await rendered?.deactivate();
|
||||
rendered = null;
|
||||
});
|
||||
|
||||
describe(`${manifest.id} activate`, () => {
|
||||
it("activates and registers only views its manifest declares", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
const contributes = manifest.contributes as unknown as Record<
|
||||
string,
|
||||
Array<{ id: string }> | undefined
|
||||
>;
|
||||
const tabs = declared(contributes.tabs);
|
||||
// A rail panel may reuse one of the plugin's tab ids.
|
||||
const panels = [...declared(contributes.panels), ...tabs];
|
||||
const cards = declared(contributes.dashboardCards);
|
||||
for (const id of rendered.registered.tabs()) expect(tabs).toContain(id);
|
||||
for (const id of rendered.registered.panels()) expect(panels).toContain(id);
|
||||
for (const id of rendered.registered.dashboardCards()) {
|
||||
expect(cards).toContain(id);
|
||||
}
|
||||
});
|
||||
|
||||
it("reads its switches from its own host settings", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
const host = (settings: Record<string, unknown>) => ({
|
||||
id: "1",
|
||||
name: "pve",
|
||||
ip: "10.0.0.1",
|
||||
port: 22,
|
||||
pluginSettings: { proxmox: settings },
|
||||
});
|
||||
const ids = (settings: Record<string, unknown>) =>
|
||||
rendered!.registered.hostActionsFor(host(settings)).map((a) => a.id);
|
||||
expect(ids({ enableProxmox: true })).toContain("proxmox-discover");
|
||||
expect(ids({})).not.toContain("proxmox-discover");
|
||||
expect(ids({ enableProxmoxStats: true })).toContain("proxmox-stats");
|
||||
expect(plugin.proxmoxSettings(host({ enableProxmoxStats: true }))).toEqual({
|
||||
enableProxmoxStats: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("removes everything it registered on deactivate", async () => {
|
||||
const app = await renderWithApp(plugin, { manifest, locales });
|
||||
await app.deactivate();
|
||||
expect(app.registered.tabs()).toEqual([]);
|
||||
expect(app.registered.panels()).toEqual([]);
|
||||
expect(app.registered.railItems()).toEqual([]);
|
||||
expect(app.registered.hostActions()).toEqual([]);
|
||||
expect(app.registered.hostEditorSections()).toEqual([]);
|
||||
expect(app.registered.dashboardCards()).toEqual([]);
|
||||
expect(app.registered.settingsComponents()).toEqual([]);
|
||||
expect(app.registered.actions()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProxmoxImportAuth } from "../../src/frontend/proxmox-import-auth";
|
||||
|
||||
describe("resolveProxmoxImportAuth", () => {
|
||||
it("inherits an available credential even with the default 'password' authType", () => {
|
||||
// Regression guard: a Proxmox host authenticating via a Termix credential
|
||||
// used to yield authType 'none' for imported guests because the default
|
||||
// authType is 'password', not 'credential'.
|
||||
expect(resolveProxmoxImportAuth("password", 42)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 42,
|
||||
overrideCredentialUsername: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the credential when authType is explicitly 'credential'", () => {
|
||||
expect(resolveProxmoxImportAuth("credential", 7)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 7,
|
||||
overrideCredentialUsername: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the credential when no authType is given", () => {
|
||||
expect(resolveProxmoxImportAuth(undefined, 5)).toEqual({
|
||||
authType: "credential",
|
||||
credentialId: 5,
|
||||
overrideCredentialUsername: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("lets an explicit secretless authType win over an available credential", () => {
|
||||
expect(resolveProxmoxImportAuth("none", 42)).toEqual({ authType: "none" });
|
||||
expect(resolveProxmoxImportAuth("tailscale", 42)).toEqual({
|
||||
authType: "tailscale",
|
||||
});
|
||||
expect(resolveProxmoxImportAuth("agent", 42)).toEqual({
|
||||
authType: "agent",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to 'none' when no credential is available", () => {
|
||||
expect(resolveProxmoxImportAuth("password", null)).toEqual({
|
||||
authType: "none",
|
||||
});
|
||||
expect(resolveProxmoxImportAuth("key", undefined)).toEqual({
|
||||
authType: "none",
|
||||
});
|
||||
expect(resolveProxmoxImportAuth(undefined, undefined)).toEqual({
|
||||
authType: "none",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through a non secret-backed special type without a credential", () => {
|
||||
expect(resolveProxmoxImportAuth("opkssh", null)).toEqual({
|
||||
authType: "opkssh",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useTranslation: () => ({ t: (key: string) => key, language: "en" }),
|
||||
usePluginApi: () => ({}),
|
||||
useHosts: () => ({ hosts: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/frontend/stats/proxmox-stats-api", () => ({
|
||||
createProxmoxStatsApi: () => ({
|
||||
startPolling: () => new Promise(() => {}),
|
||||
stopPolling: () => Promise.resolve(),
|
||||
getSnapshot: () => new Promise(() => {}),
|
||||
sendHeartbeat: () => Promise.resolve(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { ProxmoxStatsTab } from "../../../src/frontend/stats/ProxmoxStatsTab";
|
||||
|
||||
const host = (proxmox: Record<string, unknown>) => ({
|
||||
id: 1,
|
||||
name: "pve",
|
||||
ip: "10.0.0.1",
|
||||
username: "root",
|
||||
pluginSettings: { proxmox },
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("ProxmoxStatsTab", () => {
|
||||
it("reads the stats switch from the plugin host settings", () => {
|
||||
render(<ProxmoxStatsTab hostConfig={host({ enableProxmoxStats: true })} />);
|
||||
expect(
|
||||
screen.queryByText("proxmoxStats.noHostSelected"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the empty state when stats are off for the host", () => {
|
||||
render(<ProxmoxStatsTab hostConfig={host({})} />);
|
||||
expect(screen.getByText("proxmoxStats.noHostSelected")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset";
|
||||
|
||||
export default pluginVitestConfig(import.meta.url);
|
||||
Reference in new issue
Block a user