@goodandready/dsh-server-monitor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/README.ru.md +116 -0
- package/README.zh.md +116 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +199 -0
- package/lib/index.js +112 -0
- package/lib/linux-collector.js +227 -0
- package/lib/profile.js +27 -0
- package/lib/routes.js +116 -0
- package/lib/ssh-service.js +154 -0
- package/lib/vault-service.js +147 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GoodAndReady
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files, to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy,
|
|
8
|
+
modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
9
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @goodandready/dsh-server-monitor
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
[English](README.md) · [简体中文](README.zh.md) · [Русский](README.ru.md)
|
|
5
|
+
|
|
6
|
+
A standalone, read-only Linux server monitor for the DeepSeek Harness sidebar. This package owns its server profiles and credentials; it does not depend on `dsh-remote-workspace` or reuse another plugin’s connection settings.
|
|
7
|
+
|
|
8
|
+
## MVP scope
|
|
9
|
+
|
|
10
|
+
- Add and manage multiple Linux SSH profiles.
|
|
11
|
+
- Connect with an SSH private key (inline or server-side key path) or password authentication.
|
|
12
|
+
- Show current host, CPU, memory, swap, disk, process, container, network, and listening-port data.
|
|
13
|
+
- Refresh every 15 seconds while the sidebar is visible.
|
|
14
|
+
- Read-only monitoring: the plugin does not manage remote services, processes, or containers. History, charts, and alerts are not part of the MVP.
|
|
15
|
+
|
|
16
|
+
## Credential boundary
|
|
17
|
+
|
|
18
|
+
Credentials belong to this plugin and are stored in its private vault on the DSH host. On POSIX hosts the plugin enforces and verifies owner-only mode `0600`, and stops if that permission cannot be established. Secret values are not written to DSH settings or returned to the browser; profile APIs expose only masked values and presence flags. A configured private-key path is read on the DSH host.
|
|
19
|
+
|
|
20
|
+
## Development checks
|
|
21
|
+
|
|
22
|
+
Run the same checks used by Gitea Actions:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm ci
|
|
26
|
+
npm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Tests use mocked SSH connections and do not connect to real servers. The Gitea Actions workflow runs these commands for pushes and pull requests.
|
|
30
|
+
|
|
31
|
+
## Languages
|
|
32
|
+
|
|
33
|
+
The plugin UI ships English and Chinese dictionaries and uses the DSH locale service, allowing `dsh-russian-lang` to supply Russian translations. See [README.zh.md](README.zh.md) and [README.ru.md](README.ru.md).
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- A DSH web profile with the plugin installed and SSH reachability from DSH to each Linux host.
|
|
37
|
+
- A remote account allowed to run standard read-only system commands. Docker/Podman data requires permission to query that runtime.
|
|
38
|
+
- A private-key path must be readable by the DSH service account on the DSH host.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Replace `web` with your DSH profile:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
dsh plugin --profile web add @goodandready/dsh-server-monitor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Follow the CLI prompts. In the plugin settings card, add a server, choose key or password authentication, test the connection, and save. Open **Server Monitor** in the sidebar. Remove with `dsh plugin --profile web remove @goodandready/dsh-server-monitor`.
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Manage profiles in the DSH settings card. Settings contain connection metadata only: do not place passwords, private-key contents, or passphrases in settings or config files.
|
|
53
|
+
|
|
54
|
+
| Field | Type | Default | Meaning |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `profiles` | array | `[]` | Plugin-owned SSH profiles. |
|
|
57
|
+
| `profiles[].id` | string | generated | Stable profile ID. |
|
|
58
|
+
| `profiles[].name` | string | empty | Display name. |
|
|
59
|
+
| `profiles[].host` | string | empty | Linux host reachable from DSH. |
|
|
60
|
+
| `profiles[].port` | number | `22` | SSH port. |
|
|
61
|
+
| `profiles[].username` | string | `root` | Remote account; prefer restricted permissions. |
|
|
62
|
+
| `profiles[].authType` | string | `key` | `key` or `password`. |
|
|
63
|
+
| `profiles[].privateKeyPath` | string | empty | Optional key path on the DSH host. |
|
|
64
|
+
| `activeProfileId` | string | empty | Selected profile ID. |
|
|
65
|
+
|
|
66
|
+
The settings card stores secret values in the plugin-owned vault on the DSH host. POSIX vault operations require verified owner-only permissions (`0600`).
|
|
67
|
+
|
|
68
|
+
## Collected data
|
|
69
|
+
|
|
70
|
+
The bounded, read-only collector reports host/OS/kernel/CPU, load and uptime, memory and swap, mounted filesystem usage, up to 12 CPU-heavy processes, running Docker or Podman containers, network byte/packet counters, and listening TCP/UDP ports (`ss`, falling back to `netstat`). A section can be empty if a command, runtime, or permission is unavailable.
|
|
71
|
+
|
|
72
|
+
## Architecture
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
UI[DSH sidebar] --> Client[Plugin client]
|
|
77
|
+
Client --> Routes[Trusted local routes]
|
|
78
|
+
Routes --> Settings[Profiles]
|
|
79
|
+
Routes --> Vault[Credential vault]
|
|
80
|
+
Routes --> SSH[SSH service]
|
|
81
|
+
SSH --> Linux[Read-only Linux collector]
|
|
82
|
+
Linux --> Snapshot[Current snapshot]
|
|
83
|
+
Snapshot --> Client
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Module | Responsibility |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `lib/index.js` | Cordis registration, settings, service lifecycle. |
|
|
89
|
+
| `lib/client.js` | Sidebar/settings UI; pauses polling while hidden. |
|
|
90
|
+
| `lib/routes.js` | Trusted-request checks, profile operations, snapshot cache. |
|
|
91
|
+
| `lib/profile.js` | Profile normalization and validation. |
|
|
92
|
+
| `lib/vault-service.js` | Credential storage and POSIX permission verification. |
|
|
93
|
+
| `lib/ssh-service.js` | SSH authentication, bounded commands, reuse and cleanup. |
|
|
94
|
+
| `lib/linux-collector.js` | Linux collection and snapshot parsing. |
|
|
95
|
+
|
|
96
|
+
## Internal HTTP routes
|
|
97
|
+
|
|
98
|
+
These DSH-client routes are protected by a trusted-request check; they are not a public or remote-management API.
|
|
99
|
+
|
|
100
|
+
| Method | Path | Purpose |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| GET | `/dsh-server-monitor/state` | Sanitized profiles and selected ID. |
|
|
103
|
+
| GET | `/dsh-server-monitor/snapshot?profileId=<id>` | Current snapshot; defaults to active profile. |
|
|
104
|
+
| POST | `/dsh-server-monitor/profiles/save` | Create/update a profile. |
|
|
105
|
+
| POST | `/dsh-server-monitor/profiles/delete` | Delete profile and stored secrets. |
|
|
106
|
+
| POST | `/dsh-server-monitor/profiles/active` | Select active profile. |
|
|
107
|
+
| POST | `/dsh-server-monitor/test` | Test SSH connection. |
|
|
108
|
+
|
|
109
|
+
Snapshots are cached per profile for 15 seconds from collection start; concurrent requests share a collection. Save/delete invalidates that profile’s cache.
|
|
110
|
+
|
|
111
|
+
## Security and support
|
|
112
|
+
|
|
113
|
+
The plugin owns its SSH credentials, never returns secrets to the browser, and only runs read-only monitoring commands. Linux only; history, charts, alerts, and remote actions are not in the MVP.
|
|
114
|
+
|
|
115
|
+
- Issues: [GitHub Issues](https://github.com/GooDAnDReaDY/dsh-server-monitor/issues)
|
|
116
|
+
- License: [MIT](LICENSE)
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @goodandready/dsh-server-monitor
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
[English](README.md) · [简体中文](README.zh.md) · [Русский](README.ru.md)
|
|
5
|
+
|
|
6
|
+
Независимый плагин мониторинга Linux-серверов только для чтения в боковой панели DeepSeek Harness. Он хранит собственные профили и учётные данные и не зависит от `dsh-remote-workspace` или подключений других плагинов.
|
|
7
|
+
|
|
8
|
+
## Объём MVP
|
|
9
|
+
|
|
10
|
+
- Добавление и управление несколькими Linux SSH-профилями.
|
|
11
|
+
- Подключение по приватному SSH-ключу (содержимое или путь на хосте DSH) либо по паролю.
|
|
12
|
+
- Текущие сведения о хосте, CPU, памяти, swap, дисках, процессах, контейнерах, сети и слушающих портах.
|
|
13
|
+
- Обновление каждые 15 секунд, пока боковая панель видима.
|
|
14
|
+
- Только чтение: плагин не управляет удалёнными службами, процессами и контейнерами. История, графики и оповещения не входят в MVP.
|
|
15
|
+
|
|
16
|
+
## Граница хранения секретов
|
|
17
|
+
|
|
18
|
+
Учётные данные принадлежат этому плагину и хранятся в его отдельном хранилище на хосте DSH. В POSIX-системах плагин устанавливает и проверяет права только для владельца (`0600`); если обеспечить их не удаётся, операция прекращается. Секреты не записываются в настройки DSH и не возвращаются браузеру: API отдаёт только маскированные значения и признаки наличия секрета. Файл приватного ключа читается на хосте DSH.
|
|
19
|
+
|
|
20
|
+
## Проверки разработки
|
|
21
|
+
|
|
22
|
+
Запустите те же проверки, что выполняет Gitea Actions:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm ci
|
|
26
|
+
npm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Тесты используют имитацию SSH и не подключаются к реальным серверам. Gitea Actions запускает эти команды при push и создании pull request.
|
|
30
|
+
|
|
31
|
+
## Языки
|
|
32
|
+
|
|
33
|
+
В интерфейсе есть английский и китайский словари; используется locale-сервис DSH, чтобы `dsh-russian-lang` мог предоставить русский перевод. См. также [README.md](README.md) и [README.zh.md](README.zh.md).
|
|
34
|
+
## Требования
|
|
35
|
+
|
|
36
|
+
- Профиль DSH web с плагином; хост DSH должен иметь SSH-доступ к Linux-серверам.
|
|
37
|
+
- Удалённой учётной записи нужны права на стандартные команды только для чтения; для контейнеров необходим доступ к Docker/Podman.
|
|
38
|
+
- Файл приватного ключа должен находиться на хосте DSH и читаться системной учётной записью DSH.
|
|
39
|
+
|
|
40
|
+
## Установка
|
|
41
|
+
|
|
42
|
+
Замените `web` на имя используемого профиля DSH:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
dsh plugin --profile web add @goodandready/dsh-server-monitor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Следуйте подсказкам CLI. В карточке настроек добавьте сервер, выберите ключ или пароль, проверьте подключение и сохраните профиль. Затем откройте **Server Monitor** в боковой панели. Удаление: `dsh plugin --profile web remove @goodandready/dsh-server-monitor`.
|
|
49
|
+
|
|
50
|
+
## Справочник настроек
|
|
51
|
+
|
|
52
|
+
Управляйте профилями в карточке DSH. Настройки содержат только параметры подключения: не записывайте пароли, приватные ключи и парольные фразы в настройки или конфиги.
|
|
53
|
+
|
|
54
|
+
| Поле | Тип | По умолчанию | Описание |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `profiles` | массив | `[]` | Профили SSH плагина. |
|
|
57
|
+
| `profiles[].id` | строка | генерируется | Постоянный ID профиля. |
|
|
58
|
+
| `profiles[].name` | строка | пусто | Отображаемое имя. |
|
|
59
|
+
| `profiles[].host` | строка | пусто | Linux-хост, доступный из DSH. |
|
|
60
|
+
| `profiles[].port` | число | `22` | SSH-порт. |
|
|
61
|
+
| `profiles[].username` | строка | `root` | Удалённая учётная запись; предпочтительны ограниченные права. |
|
|
62
|
+
| `profiles[].authType` | строка | `key` | `key` или `password`. |
|
|
63
|
+
| `profiles[].privateKeyPath` | строка | пусто | Необязательный путь к ключу на хосте DSH. |
|
|
64
|
+
| `activeProfileId` | строка | пусто | ID выбранного профиля. |
|
|
65
|
+
|
|
66
|
+
Секреты сохраняются в хранилище плагина на хосте DSH. В POSIX-системах права файла должны быть проверены как доступные только владельцу (`0600`).
|
|
67
|
+
|
|
68
|
+
## Собираемые данные
|
|
69
|
+
|
|
70
|
+
Сборщик только для чтения получает сведения о хосте/ОС/ядре/CPU, нагрузке и времени работы, памяти и swap, файловых системах, до 12 процессов с высокой загрузкой CPU, запущенных Docker/Podman, сетевых счётчиках и слушающих TCP/UDP-портах (`ss` или запасной вариант `netstat`). Раздел может быть пустым при недоступности команды, runtime или прав.
|
|
71
|
+
|
|
72
|
+
## Архитектура
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
UI[Боковая панель DSH] --> Client[Клиент плагина]
|
|
77
|
+
Client --> Routes[Доверенные локальные маршруты]
|
|
78
|
+
Routes --> Settings[Профили]
|
|
79
|
+
Routes --> Vault[Хранилище секретов]
|
|
80
|
+
Routes --> SSH[SSH-служба]
|
|
81
|
+
SSH --> Linux[Сборщик Linux только для чтения]
|
|
82
|
+
Linux --> Snapshot[Текущий снимок]
|
|
83
|
+
Snapshot --> Client
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Модуль | Ответственность |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `lib/index.js` | Регистрация Cordis, настройки и жизненный цикл служб. |
|
|
89
|
+
| `lib/client.js` | Интерфейс боковой панели/настроек; пауза опроса при скрытой странице. |
|
|
90
|
+
| `lib/routes.js` | Проверка доверенного запроса, профили и кэш снимков. |
|
|
91
|
+
| `lib/profile.js` | Нормализация и проверка профилей. |
|
|
92
|
+
| `lib/vault-service.js` | Хранение секретов и проверка POSIX-прав. |
|
|
93
|
+
| `lib/ssh-service.js` | SSH-аутентификация, ограниченные команды и очистка. |
|
|
94
|
+
| `lib/linux-collector.js` | Сбор данных Linux и разбор снимка. |
|
|
95
|
+
|
|
96
|
+
## Внутренние HTTP-маршруты
|
|
97
|
+
|
|
98
|
+
Маршруты предназначены для клиента DSH и защищены проверкой доверенного запроса; это не публичный API управления.
|
|
99
|
+
|
|
100
|
+
| Метод | Путь | Назначение |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| GET | `/dsh-server-monitor/state` | Очищенные профили и выбранный ID. |
|
|
103
|
+
| GET | `/dsh-server-monitor/snapshot?profileId=<id>` | Снимок; по умолчанию активный профиль. |
|
|
104
|
+
| POST | `/dsh-server-monitor/profiles/save` | Создать/изменить профиль. |
|
|
105
|
+
| POST | `/dsh-server-monitor/profiles/delete` | Удалить профиль и секреты. |
|
|
106
|
+
| POST | `/dsh-server-monitor/profiles/active` | Выбрать профиль. |
|
|
107
|
+
| POST | `/dsh-server-monitor/test` | Проверить SSH. |
|
|
108
|
+
|
|
109
|
+
Снимки кэшируются по профилю на 15 секунд; параллельные запросы используют один сбор. Сохранение/удаление сбрасывает кэш.
|
|
110
|
+
|
|
111
|
+
## Безопасность, поддержка и лицензия
|
|
112
|
+
|
|
113
|
+
Плагин самостоятельно хранит SSH-учётные данные и выполняет только мониторинг на чтение. Поддерживается только Linux; история, графики, оповещения и удалённые действия не входят в MVP.
|
|
114
|
+
|
|
115
|
+
- Ошибки и предложения: [GitHub Issues](https://github.com/GooDAnDReaDY/dsh-server-monitor/issues)
|
|
116
|
+
- Лицензия: [MIT](LICENSE)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @goodandready/dsh-server-monitor
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
[English](README.md) · [简体中文](README.zh.md) · [Русский](README.ru.md)
|
|
5
|
+
|
|
6
|
+
适用于 DeepSeek Harness 侧边栏的独立、只读 Linux 服务器监控插件。插件自行管理服务器配置和凭据,不依赖 `dsh-remote-workspace`,也不会复用其他插件的连接配置。
|
|
7
|
+
|
|
8
|
+
## MVP 范围
|
|
9
|
+
|
|
10
|
+
- 添加和管理多个 Linux SSH 配置。
|
|
11
|
+
- 支持 SSH 私钥(内联内容或 DSH 主机上的密钥路径)以及密码认证。
|
|
12
|
+
- 显示主机、CPU、内存、交换空间、磁盘、进程、容器、网络和监听端口的当前状态。
|
|
13
|
+
- 侧边栏可见时每 15 秒刷新一次。
|
|
14
|
+
- 只读监控:不会管理远程服务、进程或容器。历史记录、图表和告警不属于 MVP。
|
|
15
|
+
|
|
16
|
+
## 凭据边界
|
|
17
|
+
|
|
18
|
+
凭据由本插件独立管理,保存在 DSH 主机上的专属保险库中。在 POSIX 主机上,插件会设置并验证仅所有者可访问的 `0600` 权限;无法确保权限时会停止操作。密钥不会写入 DSH 设置,也不会通过 API 返回给浏览器;配置接口只返回掩码和是否已保存的信息。私钥路径在 DSH 主机上读取。
|
|
19
|
+
|
|
20
|
+
## 开发测试
|
|
21
|
+
|
|
22
|
+
执行与 Gitea Actions 相同的检查:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm ci
|
|
26
|
+
npm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
测试使用模拟 SSH 连接,不会连接真实服务器。Gitea Actions 会在推送和拉取请求时执行这些命令。
|
|
30
|
+
|
|
31
|
+
## 语言
|
|
32
|
+
|
|
33
|
+
界面内置英语和中文词典,并使用 DSH locale 服务,以便由 `dsh-russian-lang` 提供俄语翻译。另见 [README.md](README.md) 和 [README.ru.md](README.ru.md)。
|
|
34
|
+
## 环境要求
|
|
35
|
+
|
|
36
|
+
- 已安装插件的 DSH web 配置,且 DSH 主机可通过 SSH 访问 Linux 服务器。
|
|
37
|
+
- 远程账户可运行常见只读系统命令;容器数据需要有权查询 Docker/Podman。
|
|
38
|
+
- 私钥路径必须指向 DSH 主机上可由 DSH 服务账户读取的文件。
|
|
39
|
+
|
|
40
|
+
## 安装
|
|
41
|
+
|
|
42
|
+
将 `web` 替换为所用的 DSH 配置:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
dsh plugin --profile web add @goodandready/dsh-server-monitor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
按 CLI 提示操作。在插件设置卡片中添加服务器,选择密钥或密码认证,测试并保存,然后在侧边栏打开 **Server Monitor**。卸载:`dsh plugin --profile web remove @goodandready/dsh-server-monitor`。
|
|
49
|
+
|
|
50
|
+
## 配置参考
|
|
51
|
+
|
|
52
|
+
请在 DSH 设置卡片中管理配置。设置仅包含连接元数据;不要将密码、私钥内容或口令写入设置或配置文件。
|
|
53
|
+
|
|
54
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `profiles` | 数组 | `[]` | 本插件管理的 SSH 配置。 |
|
|
57
|
+
| `profiles[].id` | 字符串 | 自动生成 | 稳定配置 ID。 |
|
|
58
|
+
| `profiles[].name` | 字符串 | 空 | 显示名称。 |
|
|
59
|
+
| `profiles[].host` | 字符串 | 空 | DSH 可访问的 Linux 主机。 |
|
|
60
|
+
| `profiles[].port` | 数字 | `22` | SSH 端口。 |
|
|
61
|
+
| `profiles[].username` | 字符串 | `root` | 远程账户;建议限制权限。 |
|
|
62
|
+
| `profiles[].authType` | 字符串 | `key` | `key` 或 `password`。 |
|
|
63
|
+
| `profiles[].privateKeyPath` | 字符串 | 空 | DSH 主机上的可选密钥路径。 |
|
|
64
|
+
| `activeProfileId` | 字符串 | 空 | 活动配置 ID。 |
|
|
65
|
+
|
|
66
|
+
秘密值保存在 DSH 主机上由插件管理的保险库中。POSIX 系统要求文件权限验证为仅所有者可访问(`0600`)。
|
|
67
|
+
|
|
68
|
+
## 采集内容
|
|
69
|
+
|
|
70
|
+
有界只读采集器获取主机/操作系统/内核/CPU、负载和运行时间、内存与 swap、文件系统使用情况、最多 12 个 CPU 占用靠前的进程、运行中的 Docker 或 Podman 容器、网络计数器及 TCP/UDP 监听端口(使用 `ss`,否则回退到 `netstat`)。命令、运行时或权限不可用时,相应区块可能为空。
|
|
71
|
+
|
|
72
|
+
## 架构
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
UI[DSH 侧边栏] --> Client[插件客户端]
|
|
77
|
+
Client --> Routes[可信本地路由]
|
|
78
|
+
Routes --> Settings[服务器配置]
|
|
79
|
+
Routes --> Vault[凭据保险库]
|
|
80
|
+
Routes --> SSH[SSH 服务]
|
|
81
|
+
SSH --> Linux[只读 Linux 采集器]
|
|
82
|
+
Linux --> Snapshot[当前快照]
|
|
83
|
+
Snapshot --> Client
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| 模块 | 职责 |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `lib/index.js` | Cordis 注册、设置集成和服务生命周期。 |
|
|
89
|
+
| `lib/client.js` | 侧边栏/设置界面;页面隐藏时暂停轮询。 |
|
|
90
|
+
| `lib/routes.js` | 可信请求校验、配置操作、快照缓存。 |
|
|
91
|
+
| `lib/profile.js` | 配置规范化和校验。 |
|
|
92
|
+
| `lib/vault-service.js` | 凭据保存及 POSIX 权限验证。 |
|
|
93
|
+
| `lib/ssh-service.js` | SSH 认证、有界命令、连接复用和清理。 |
|
|
94
|
+
| `lib/linux-collector.js` | Linux 数据采集和快照解析。 |
|
|
95
|
+
|
|
96
|
+
## 内部 HTTP 路由
|
|
97
|
+
|
|
98
|
+
这些路由供 DSH 客户端使用并受可信请求检查保护,不是公开或远程管理 API。
|
|
99
|
+
|
|
100
|
+
| 方法 | 路径 | 用途 |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| GET | `/dsh-server-monitor/state` | 清理后的配置和所选 ID。 |
|
|
103
|
+
| GET | `/dsh-server-monitor/snapshot?profileId=<id>` | 当前快照;默认活动配置。 |
|
|
104
|
+
| POST | `/dsh-server-monitor/profiles/save` | 新建/更新配置。 |
|
|
105
|
+
| POST | `/dsh-server-monitor/profiles/delete` | 删除配置和凭据。 |
|
|
106
|
+
| POST | `/dsh-server-monitor/profiles/active` | 选择活动配置。 |
|
|
107
|
+
| POST | `/dsh-server-monitor/test` | 测试 SSH 连接。 |
|
|
108
|
+
|
|
109
|
+
快照按配置缓存 15 秒,并发请求共享同一次采集;保存或删除会清除该配置缓存。
|
|
110
|
+
|
|
111
|
+
## 安全、支持与许可证
|
|
112
|
+
|
|
113
|
+
插件独立管理 SSH 凭据,不将秘密返回浏览器,仅运行只读监控命令。仅支持 Linux;MVP 不含历史、图表、告警或远程操作。
|
|
114
|
+
|
|
115
|
+
- 问题和建议:[GitHub Issues](https://github.com/GooDAnDReaDY/dsh-server-monitor/issues)
|
|
116
|
+
- 许可证:[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@goodandready/dsh-server-monitor',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const NS = 'dsh-server-monitor';
|
|
7
|
+
const API = '/dsh-server-monitor';
|
|
8
|
+
|
|
9
|
+
const en = {
|
|
10
|
+
title: 'Server Monitor', subtitle: 'Read-only Linux server status',
|
|
11
|
+
servers: 'Servers', guideDescription: 'Read-only Linux server status',
|
|
12
|
+
add: 'Add server', save: 'Save server', delete: 'Delete', test: 'Test connection',
|
|
13
|
+
refresh: 'Refresh', active: 'Active', connected: 'Online', offline: 'Offline', partial: 'Partial',
|
|
14
|
+
noServers: 'No servers configured yet.', noSnapshot: 'No snapshot available.',
|
|
15
|
+
name: 'Name', host: 'Host', port: 'Port', username: 'Username', auth: 'Authentication',
|
|
16
|
+
key: 'Private key', password: 'Password', passphrase: 'Key passphrase', keyPath: 'Key path',
|
|
17
|
+
keyHint: 'The key is stored only in this plugin vault.', keepSecret: 'Leave empty to keep stored value',
|
|
18
|
+
system: 'System', resources: 'Resources', disks: 'Disks', processes: 'Top processes', containers: 'Containers',
|
|
19
|
+
network: 'Network', ports: 'Listening ports', hostname: 'Hostname', os: 'OS', kernel: 'Kernel',
|
|
20
|
+
architecture: 'Architecture', cpu: 'CPU', uptime: 'Uptime', load: 'Load average', memory: 'Memory', swap: 'Swap',
|
|
21
|
+
used: 'used', available: 'available', filesystem: 'Filesystem', mount: 'Mount', command: 'Command',
|
|
22
|
+
state: 'State', image: 'Image', interface: 'Interface', rx: 'RX', tx: 'TX', protocol: 'Protocol',
|
|
23
|
+
address: 'Address', error: 'Error', loading: 'Loading…', unavailable: 'Unavailable', saved: 'Saved', confirmDelete: 'Delete this server profile?', cancel: 'Cancel',
|
|
24
|
+
saving: 'Saving…', testing: 'Testing…', select: 'Select server'
|
|
25
|
+
};
|
|
26
|
+
const zh = {
|
|
27
|
+
title: '服务器监控', subtitle: '只读 Linux 服务器状态',
|
|
28
|
+
servers: '服务器', guideDescription: '只读 Linux 服务器状态',
|
|
29
|
+
add: '添加服务器', save: '保存服务器', delete: '删除', test: '测试连接',
|
|
30
|
+
refresh: '刷新', active: '当前', connected: '在线', offline: '离线', partial: '部分可用',
|
|
31
|
+
noServers: '尚未配置服务器。', noSnapshot: '暂无状态快照。',
|
|
32
|
+
name: '名称', host: '主机', port: '端口', username: '用户名', auth: '认证方式',
|
|
33
|
+
key: '私钥', password: '密码', passphrase: '密钥口令', keyPath: '密钥路径',
|
|
34
|
+
keyHint: '密钥只存储在此插件的保险库中。', keepSecret: '留空以保留已存储的值',
|
|
35
|
+
system: '系统', resources: '资源', disks: '磁盘', processes: '高负载进程', containers: '容器',
|
|
36
|
+
network: '网络', ports: '监听端口', hostname: '主机名', os: '操作系统', kernel: '内核',
|
|
37
|
+
architecture: '架构', cpu: 'CPU', uptime: '运行时间', load: '平均负载', memory: '内存', swap: '交换分区',
|
|
38
|
+
used: '已用', available: '可用', filesystem: '文件系统', mount: '挂载点', command: '命令',
|
|
39
|
+
state: '状态', image: '镜像', interface: '接口', rx: '接收', tx: '发送', protocol: '协议',
|
|
40
|
+
address: '地址', error: '错误', loading: '加载中…', unavailable: '不可用', saved: '已保存', confirmDelete: '删除此服务器配置?', cancel: '取消',
|
|
41
|
+
saving: '保存中…', testing: '测试中…', select: '选择服务器'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function labels(ctx) {
|
|
45
|
+
const translate = ctx?.locale?.bind ? ctx.locale.bind(NS) : (key) => en[key] || key;
|
|
46
|
+
return new Proxy({}, { get: (_target, key) => typeof key === 'symbol' ? undefined : translate(String(key)) });
|
|
47
|
+
}
|
|
48
|
+
function text(value, fallback = '—') { return value === undefined || value === null || value === '' ? fallback : String(value); }
|
|
49
|
+
function pct(value) { return `${Math.round(Number(value) || 0)}%`; }
|
|
50
|
+
function formatMb(value) { const mb = Number(value) || 0; return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${Math.round(mb)} MB`; }
|
|
51
|
+
async function request(path, options = {}) {
|
|
52
|
+
const response = await fetch(API + path, { credentials: 'same-origin', ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } });
|
|
53
|
+
const body = await response.json().catch(() => ({}));
|
|
54
|
+
if (!response.ok || body.ok === false) throw new Error(body.error || `HTTP ${response.status}`);
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
function ensureCss() {
|
|
58
|
+
if (typeof document === 'undefined' || document.getElementById('dsh-server-monitor-css')) return;
|
|
59
|
+
const style = document.createElement('style');
|
|
60
|
+
style.id = 'dsh-server-monitor-css';
|
|
61
|
+
style.dataset.dshPlugin = NS;
|
|
62
|
+
style.textContent = `
|
|
63
|
+
.dsm-root{display:flex;flex-direction:column;gap:12px;padding:4px 0 18px;color:var(--dsw-alias-label-primary)}
|
|
64
|
+
.dsm-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:10px}
|
|
65
|
+
.dsm-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.dsm-title{font-size:15px;font-weight:700}.dsm-sub{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:3px}
|
|
66
|
+
.dsm-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:7px}.dsm-stat{border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:8px;background:var(--dsw-alias-bg-layer-2);min-width:0}.dsm-val{font-size:13px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-label{font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:3px}
|
|
67
|
+
.dsm-section{display:flex;flex-direction:column;gap:6px}.dsm-section-title{font-size:12px;font-weight:650}.dsm-table{width:100%;border-collapse:collapse;font-size:11px}.dsm-table th,.dsm-table td{text-align:left;padding:5px 4px;border-bottom:1px solid var(--dsw-alias-border-l2);vertical-align:top}.dsm-table th{color:var(--dsw-alias-label-secondary);font-weight:550}.dsm-muted{color:var(--dsw-alias-label-secondary);font-size:11px}.dsm-badge{font-size:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 7px;white-space:nowrap}.dsm-ok{color:var(--dsw-alias-state-success-primary)}.dsm-warn{color:var(--dsw-alias-state-warning-primary)}.dsm-err{color:var(--dsw-alias-state-error-primary)}
|
|
68
|
+
.dsm-btn{appearance:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:7px;padding:5px 9px;font:inherit;font-size:11px;cursor:pointer}.dsm-btn:disabled{opacity:.55;cursor:not-allowed}.dsm-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}.dsm-row{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.dsm-field{display:flex;flex-direction:column;gap:4px;min-width:140px;flex:1}.dsm-field label{font-size:11px;color:var(--dsw-alias-label-secondary)}.dsm-input{height:30px;box-sizing:border-box;width:100%;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:7px;padding:0 8px;font:inherit;font-size:12px}.dsm-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}.dsm-form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:9px}.dsm-error{padding:8px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:7px;color:var(--dsw-alias-state-error-primary);font-size:11px}.dsm-progress{height:5px;background:var(--dsw-alias-bg-layer-1);border-radius:4px;overflow:hidden;margin-top:5px}.dsm-progress>span{display:block;height:100%;background:var(--dsw-alias-state-brand-primary)}
|
|
69
|
+
`;
|
|
70
|
+
document.head.appendChild(style);
|
|
71
|
+
}
|
|
72
|
+
function useMonitor(ctx) {
|
|
73
|
+
const [state, setState] = React.useState({ profiles: [], activeId: '', snapshot: null, loading: true, error: '' });
|
|
74
|
+
const refresh = React.useCallback(async (profileId) => {
|
|
75
|
+
try {
|
|
76
|
+
const selected = profileId || state.activeId;
|
|
77
|
+
const base = await request('/state');
|
|
78
|
+
const id = selected || base.activeId || base.profiles[0]?.id || '';
|
|
79
|
+
const snapshot = id ? (await request(`/snapshot?profileId=${encodeURIComponent(id)}`)).snapshot : null;
|
|
80
|
+
setState({ profiles: base.profiles || [], activeId: id, snapshot, loading: false, error: '' });
|
|
81
|
+
} catch (error) { setState((old) => ({ ...old, loading: false, error: error.message })); }
|
|
82
|
+
}, [state.activeId]);
|
|
83
|
+
React.useEffect(() => { const doc = typeof document === 'undefined' ? null : document; refresh(); const tick = () => { if (!doc || !doc.hidden) refresh(); }; const timer = setInterval(tick, 15000); const onVisibility = () => { if (!doc?.hidden) refresh(); }; doc?.addEventListener?.('visibilitychange', onVisibility); return () => { clearInterval(timer); doc?.removeEventListener?.('visibilitychange', onVisibility); }; }, []);
|
|
84
|
+
return { state, refresh };
|
|
85
|
+
}
|
|
86
|
+
function Badge({ status, t }) { const cls = status === 'ready' ? 'dsm-ok' : status === 'partial' ? 'dsm-warn' : 'dsm-err'; const label = status === 'ready' ? t.connected : status === 'partial' ? t.partial : t.offline; return React.createElement('span', { className: `dsm-badge ${cls}` }, label); }
|
|
87
|
+
function Stat({ label, value }) { return React.createElement('div', { className: 'dsm-stat' }, React.createElement('div', { className: 'dsm-val' }, text(value)), React.createElement('div', { className: 'dsm-label' }, label)); }
|
|
88
|
+
function ResourceBar({ label, resource }) { const percent = resource?.percent || 0; return React.createElement('div', { className: 'dsm-stat' }, React.createElement('div', { className: 'dsm-val' }, `${formatMb(resource?.usedMb)} / ${formatMb(resource?.totalMb)}`), React.createElement('div', { className: 'dsm-label' }, `${label} · ${pct(percent)}`), React.createElement('div', { className: 'dsm-progress' }, React.createElement('span', { style: { width: `${Math.min(100, Math.max(0, percent))}%` } })) ); }
|
|
89
|
+
function SnapshotView({ snapshot, t }) {
|
|
90
|
+
if (!snapshot) return React.createElement('div', { className: 'dsm-muted' }, t.noSnapshot);
|
|
91
|
+
const i = snapshot.identity || {}; const s = snapshot.system || {};
|
|
92
|
+
return React.createElement(React.Fragment, null,
|
|
93
|
+
React.createElement('div', { className: 'dsm-card' }, React.createElement('div', { className: 'dsm-head' }, React.createElement('div', null, React.createElement('div', { className: 'dsm-title' }, t.system), React.createElement('div', { className: 'dsm-sub' }, text(i.hostname), ' · ', snapshot.timestamp ? new Date(snapshot.timestamp).toLocaleTimeString() : '—')), React.createElement(Badge, { status: snapshot.status, t })), React.createElement('div', { className: 'dsm-grid' }, React.createElement(Stat, { label: t.os, value: i.os }), React.createElement(Stat, { label: t.kernel, value: i.kernel }), React.createElement(Stat, { label: t.architecture, value: i.architecture }), React.createElement(Stat, { label: t.cpu, value: `${text(i.cpuModel)} · ${i.cpuCores || 0}` }), React.createElement(Stat, { label: t.uptime, value: text(s.uptime) }), React.createElement(Stat, { label: t.load, value: (s.loadAverage || []).join(' · ') || '—' }))),
|
|
94
|
+
React.createElement('div', { className: 'dsm-card' }, React.createElement('div', { className: 'dsm-section-title' }, t.resources), React.createElement('div', { className: 'dsm-grid' }, React.createElement(ResourceBar, { label: t.memory, resource: s.memory }), React.createElement(ResourceBar, { label: t.swap, resource: s.swap }))),
|
|
95
|
+
React.createElement(ListSection, { title: t.disks, rows: snapshot.disks, columns: [t.filesystem, t.mount, t.used], render: (row) => [row.filesystem, row.mount, `${formatMb(row.usedMb)} / ${formatMb(row.totalMb)} (${pct(row.percent)})`] }),
|
|
96
|
+
React.createElement(ListSection, { title: t.processes, rows: snapshot.processes, columns: ['PID', t.command, 'CPU', 'RAM'], render: (row) => [row.pid, row.command, pct(row.cpuPercent), pct(row.memoryPercent)] }),
|
|
97
|
+
React.createElement(ListSection, { title: t.containers, rows: snapshot.containers, columns: [t.name, t.image, t.state], render: (row) => [row.name, row.image, row.state || row.status], empty: t.unavailable }),
|
|
98
|
+
React.createElement(ListSection, { title: t.network, rows: snapshot.network?.interfaces, columns: [t.interface, t.rx, t.tx], render: (row) => [row.name, `${row.rxBytes} B`, `${row.txBytes} B`] }),
|
|
99
|
+
React.createElement(ListSection, { title: t.ports, rows: snapshot.ports, columns: [t.protocol, t.address, t.port], render: (row) => [row.protocol, row.address, row.port] })
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function ListSection({ title, rows = [], columns, render, empty = '—' }) { return React.createElement('div', { className: 'dsm-card dsm-section' }, React.createElement('div', { className: 'dsm-section-title' }, title), rows.length ? React.createElement('table', { className: 'dsm-table' }, React.createElement('thead', null, React.createElement('tr', null, columns.map((item) => React.createElement('th', { key: item }, item)))), React.createElement('tbody', null, rows.slice(0, 10).map((row, index) => React.createElement('tr', { key: row.id || row.pid || row.name || index }, render(row).map((item, itemIndex) => React.createElement('td', { key: itemIndex }, text(item))))))) : React.createElement('div', { className: 'dsm-muted' }, empty)); }
|
|
103
|
+
function MonitorPane({ ctx }) {
|
|
104
|
+
ensureCss();
|
|
105
|
+
const t = labels(ctx);
|
|
106
|
+
const { state, refresh } = useMonitor(ctx);
|
|
107
|
+
const [selected, setSelected] = React.useState('');
|
|
108
|
+
const id = selected || state.activeId;
|
|
109
|
+
const profile = state.profiles.find((item) => item.id === id);
|
|
110
|
+
const header = React.createElement('div', { className: 'dsm-card' },
|
|
111
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
112
|
+
React.createElement('div', null,
|
|
113
|
+
React.createElement('div', { className: 'dsm-title' }, t.title),
|
|
114
|
+
React.createElement('div', { className: 'dsm-sub' }, profile ? `${profile.name} · ${profile.username}@${profile.host}` : t.noServers)
|
|
115
|
+
),
|
|
116
|
+
React.createElement('button', { className: 'dsm-btn', type: 'button', onClick: () => refresh(id) }, t.refresh)
|
|
117
|
+
),
|
|
118
|
+
state.profiles.length ? React.createElement('select', {
|
|
119
|
+
className: 'dsm-input', value: id,
|
|
120
|
+
onChange: (event) => { setSelected(event.target.value); refresh(event.target.value); }
|
|
121
|
+
}, state.profiles.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.name))) : null
|
|
122
|
+
);
|
|
123
|
+
const body = state.loading ? React.createElement('div', { className: 'dsm-muted' }, t.loading)
|
|
124
|
+
: state.error ? React.createElement('div', { className: 'dsm-error' }, state.error)
|
|
125
|
+
: React.createElement(SnapshotView, { snapshot: state.snapshot, t });
|
|
126
|
+
return React.createElement('div', { className: 'dsm-root' }, header, body);
|
|
127
|
+
}
|
|
128
|
+
function SettingsCard({ ctx }) {
|
|
129
|
+
ensureCss();
|
|
130
|
+
const t = labels(ctx);
|
|
131
|
+
const [state, setState] = React.useState({ profiles: [], activeId: '' });
|
|
132
|
+
const [draft, setDraft] = React.useState(null);
|
|
133
|
+
const [busy, setBusy] = React.useState(false);
|
|
134
|
+
const [error, setError] = React.useState('');
|
|
135
|
+
const [testResult, setTestResult] = React.useState('');
|
|
136
|
+
const load = React.useCallback(() => request('/state').then(setState).catch((e) => setError(e.message)), []);
|
|
137
|
+
React.useEffect(() => { load(); }, [load]);
|
|
138
|
+
const edit = (profile) => setDraft({ ...(profile || { id: '', name: '', host: '', port: 22, username: 'root', authType: 'key', privateKeyPath: '' }), password: '', privateKey: '', passphrase: '' });
|
|
139
|
+
const field = (key, label, type = 'text') => React.createElement('div', { className: 'dsm-field' },
|
|
140
|
+
React.createElement('label', null, label),
|
|
141
|
+
React.createElement('input', { className: 'dsm-input', type, value: draft?.[key] ?? '', onChange: (event) => setDraft((old) => ({ ...old, [key]: type === 'number' ? Number(event.target.value) : event.target.value })) })
|
|
142
|
+
);
|
|
143
|
+
const save = async () => {
|
|
144
|
+
setBusy(true); setError('');
|
|
145
|
+
try { await request('/profiles/save', { method: 'POST', body: JSON.stringify(draft) }); setDraft(null); await load(); }
|
|
146
|
+
catch (e) { setError(e.message); } finally { setBusy(false); }
|
|
147
|
+
};
|
|
148
|
+
const testConnection = async () => {
|
|
149
|
+
setBusy(true); setError(''); setTestResult('');
|
|
150
|
+
try { const result = await request('/test', { method: 'POST', body: JSON.stringify({ profile: draft }) }); setTestResult(`${t.connected}: ${result.os || ''} · ${result.latencyMs || 0} ms`); }
|
|
151
|
+
catch (e) { setError(e.message); } finally { setBusy(false); }
|
|
152
|
+
};
|
|
153
|
+
const profileRows = state.profiles.map((profile) => React.createElement('div', { className: 'dsm-row', key: profile.id },
|
|
154
|
+
React.createElement('button', { className: 'dsm-btn', type: 'button', onClick: () => edit(profile) }, profile.name),
|
|
155
|
+
React.createElement('span', { className: 'dsm-muted' }, `${profile.username}@${profile.host}:${profile.port}`),
|
|
156
|
+
state.activeId === profile.id ? React.createElement('span', { className: 'dsm-badge dsm-ok' }, t.active) : null,
|
|
157
|
+
React.createElement('button', { className: 'dsm-btn', type: 'button', onClick: async () => { if (typeof window !== 'undefined' && !window.confirm(`${t.confirmDelete} ${profile.name}`)) return; await request('/profiles/delete', { method: 'POST', body: JSON.stringify({ id: profile.id }) }); load(); } }, t.delete)
|
|
158
|
+
));
|
|
159
|
+
const form = draft ? React.createElement('div', { className: 'dsm-card' },
|
|
160
|
+
React.createElement('div', { className: 'dsm-section-title' }, t.add),
|
|
161
|
+
React.createElement('div', { className: 'dsm-form-grid' },
|
|
162
|
+
field('name', t.name), field('host', t.host), field('port', t.port, 'number'), field('username', t.username),
|
|
163
|
+
field('privateKeyPath', t.keyPath), field('privateKey', t.key, 'password'), field('passphrase', t.passphrase, 'password'), field('password', t.password, 'password'), React.createElement('div', { className: 'dsm-field' }, React.createElement('label', null, t.auth), React.createElement('select', { className: 'dsm-input', value: draft?.authType || 'key', onChange: (event) => setDraft((old) => ({ ...old, authType: event.target.value })) }, React.createElement('option', { value: 'key' }, t.key), React.createElement('option', { value: 'password' }, t.password)))
|
|
164
|
+
),
|
|
165
|
+
React.createElement('div', { className: 'dsm-muted' }, t.keyHint, ' · ', t.keepSecret),
|
|
166
|
+
React.createElement('div', { className: 'dsm-row' },
|
|
167
|
+
React.createElement('button', { className: 'dsm-btn', type: 'button', disabled: busy, onClick: testConnection }, busy ? t.testing : t.test),
|
|
168
|
+
React.createElement('button', { className: 'dsm-btn dsm-btn-primary', type: 'button', disabled: busy, onClick: save }, busy ? t.saving : t.save),
|
|
169
|
+
React.createElement('button', { className: 'dsm-btn', type: 'button', onClick: () => setDraft(null) }, t.cancel)
|
|
170
|
+
),
|
|
171
|
+
testResult ? React.createElement('div', { className: 'dsm-muted' }, testResult) : null
|
|
172
|
+
) : null;
|
|
173
|
+
return React.createElement('div', { className: 'dsm-root' },
|
|
174
|
+
React.createElement('div', { className: 'dsm-card' },
|
|
175
|
+
React.createElement('div', { className: 'dsm-head' },
|
|
176
|
+
React.createElement('div', null, React.createElement('div', { className: 'dsm-title' }, t.title), React.createElement('div', { className: 'dsm-sub' }, t.subtitle)),
|
|
177
|
+
React.createElement('button', { className: 'dsm-btn dsm-btn-primary', type: 'button', onClick: () => edit(null) }, t.add)
|
|
178
|
+
),
|
|
179
|
+
state.profiles.length ? profileRows : React.createElement('div', { className: 'dsm-muted' }, t.noServers)
|
|
180
|
+
),
|
|
181
|
+
form,
|
|
182
|
+
error ? React.createElement('div', { className: 'dsm-error' }, error) : null
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
function register(ctx) {
|
|
186
|
+
const registerLocale = () => ctx.locale?.register?.(NS, { en, zh });
|
|
187
|
+
if (ctx.effect) ctx.effect(registerLocale, 'dsh-server-monitor: dictionaries');
|
|
188
|
+
else registerLocale();
|
|
189
|
+
const registerSettings = () => ctx.slots?.register?.({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, SettingsCard);
|
|
190
|
+
try { if (ctx.slots?.inject) ctx.slots.inject('settings.plugin.item', registerSettings); else registerSettings(); } catch (error) { console.warn('[dsh-server-monitor] settings slot registration failed', error); }
|
|
191
|
+
try { ctx.inject?.(['sidebarRightTabs'], (sctx) => { const tabs = sctx?.sidebarRightTabs; if (!tabs?.register) return; const t = labels(ctx); const definition = { id: '@goodandready/dsh-server-monitor', kind: 'server-monitor', priority: 'extension', title: () => t.servers, guide: [{ order: 40, title: () => t.title, description: () => t.guideDescription, icon: () => React.createElement('span', null, '◫') }] }; const registerTab = () => tabs.register(definition); if (sctx.effect) sctx.effect(registerTab); else registerTab(); const registerPane = () => ctx.slots?.register?.({ name: 'sidebar.right.pane.tab', key: '@goodandready/dsh-server-monitor', locale: NS, inject: () => ({ ctx }) }, MonitorPane); if (ctx.slots?.inject) ctx.slots.inject('sidebar.right.pane.tab', registerPane); else registerPane(); }); } catch (error) { console.warn('[dsh-server-monitor] native sidebar registration failed', error); }
|
|
192
|
+
try { ctx.inject?.(['betterSidebar'], (sctx) => { const service = sctx?.betterSidebar; if (!service?.registerTab) return; const registerTab = () => service.registerTab({ id: 'dsh-server-monitor:tab', title: () => labels(ctx).servers, icon: () => React.createElement('span', null, '◫'), order: 40, component: ({ scope }) => React.createElement(MonitorPane, { ctx, scope }) }); if (sctx.effect) sctx.effect(registerTab); else registerTab(); }); } catch (error) { console.warn('[dsh-server-monitor] legacy sidebar registration failed', error); }
|
|
193
|
+
}
|
|
194
|
+
module.exports.inject = ['slots', 'locale'];
|
|
195
|
+
module.exports.apply = register;
|
|
196
|
+
module.exports._test = { en, zh, labels, register };
|
|
197
|
+
return module.exports;
|
|
198
|
+
}
|
|
199
|
+
});
|