@goodandready/dsh-remote-workspace 0.1.0 → 0.1.1
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/docs/design/DESIGN.md +60 -51
- package/docs/plans/findings.md +14 -2
- package/docs/plans/progress.md +11 -10
- package/docs/plans/task_plan.md +16 -19
- package/lib/client.js +439 -127
- package/lib/index.js +19 -14
- package/lib/mirror-sync-service.js +22 -6
- package/lib/routes.js +85 -20
- package/lib/ssh-service.js +31 -8
- package/lib/tunnel-service.js +20 -10
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Remote Workspace Settings Card (settings.plugin.item) & Session Utility Chip.
|
|
1
2
|
window.__ModuleLoader__.load({
|
|
2
3
|
id: '@goodandready/dsh-remote-workspace',
|
|
3
4
|
factory: (require) => {
|
|
@@ -9,24 +10,27 @@ window.__ModuleLoader__.load({
|
|
|
9
10
|
|
|
10
11
|
function ensureCss() {
|
|
11
12
|
if (typeof document === 'undefined') return
|
|
12
|
-
if (document.getElementById('drw-clinebot-
|
|
13
|
+
if (document.getElementById('drw-clinebot-full-css')) return
|
|
13
14
|
const style = document.createElement('style')
|
|
14
|
-
style.id = 'drw-clinebot-
|
|
15
|
+
style.id = 'drw-clinebot-full-css'
|
|
15
16
|
style.dataset.dshPlugin = NS
|
|
16
17
|
style.textContent = `
|
|
17
|
-
.drw-page{display:flex;flex-direction:column;gap:18px;padding:6px 0
|
|
18
|
-
.drw-header{display:flex;flex-direction:column;gap:
|
|
18
|
+
.drw-page{display:flex;flex-direction:column;gap:18px;padding:6px 0 24px;max-width:960px}
|
|
19
|
+
.drw-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
20
|
+
.drw-header-top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
|
|
19
21
|
.drw-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
|
|
20
|
-
.drw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.
|
|
22
|
+
.drw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
|
|
23
|
+
.drw-header-badges{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|
21
24
|
|
|
22
|
-
.drw-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:
|
|
25
|
+
.drw-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:14px;list-style:none}
|
|
23
26
|
.drw-card-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
|
|
24
|
-
.drw-card-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-
|
|
27
|
+
.drw-card-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
|
|
25
28
|
|
|
26
29
|
.drw-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
|
27
30
|
.drw-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:12px}
|
|
31
|
+
.drw-grid-4{display:grid;grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));gap:10px}
|
|
28
32
|
|
|
29
|
-
.drw-badge{font-size:11px;padding:3px
|
|
33
|
+
.drw-badge{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;font-family:monospace}
|
|
30
34
|
.drw-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
|
|
31
35
|
.drw-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
|
|
32
36
|
.drw-badge-err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
|
|
@@ -34,31 +38,33 @@ window.__ModuleLoader__.load({
|
|
|
34
38
|
.drw-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;width:100%;box-sizing:border-box}
|
|
35
39
|
.drw-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
36
40
|
.drw-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);margin-bottom:2px}
|
|
41
|
+
.drw-hint{font-size:11px;color:var(--dsw-alias-label-dimmed, var(--dsw-alias-label-secondary));line-height:1.3}
|
|
37
42
|
.drw-field{display:flex;flex-direction:column;gap:3px;flex:1;min-width:180px}
|
|
38
43
|
|
|
39
44
|
.drw-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
|
|
40
45
|
.drw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
|
|
46
|
+
.drw-btn:disabled{opacity:0.6;cursor:not-allowed}
|
|
41
47
|
.drw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
42
48
|
.drw-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
43
49
|
.drw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
44
50
|
.drw-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
|
|
45
51
|
|
|
46
52
|
.drw-segmented{display:inline-flex;border-radius:8px;padding:2px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);gap:2px}
|
|
47
|
-
.drw-segmented-item{appearance:none;border:none;background:none;cursor:pointer;padding:
|
|
53
|
+
.drw-segmented-item{appearance:none;border:none;background:none;cursor:pointer;padding:5px 12px;font-size:12px;font-weight:500;border-radius:6px;color:var(--dsw-alias-label-secondary);transition:all .15s ease}
|
|
48
54
|
.drw-segmented-item-active{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,0.08);font-weight:600}
|
|
49
55
|
|
|
50
|
-
.drw-table{width:100%;border-collapse:collapse;margin-top:
|
|
51
|
-
.drw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:
|
|
52
|
-
.drw-table td{padding:
|
|
56
|
+
.drw-table{width:100%;border-collapse:collapse;margin-top:4px}
|
|
57
|
+
.drw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
58
|
+
.drw-table td{padding:9px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
53
59
|
.drw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
54
60
|
|
|
55
61
|
.drw-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}
|
|
56
|
-
.drw-stat-val{font-size:
|
|
62
|
+
.drw-stat-val{font-size:14px;font-weight:700;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
57
63
|
.drw-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
58
|
-
.drw-preview{padding:10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
|
|
64
|
+
.drw-preview{padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2);line-height:1.4}
|
|
59
65
|
|
|
60
|
-
.drw-browser-modal{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:8px;max-height:
|
|
61
|
-
.drw-browser-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px;max-height:
|
|
66
|
+
.drw-browser-modal{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:8px;max-height:280px}
|
|
67
|
+
.drw-browser-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px;max-height:190px;padding-right:4px}
|
|
62
68
|
.drw-browser-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:13px;font-family:monospace}
|
|
63
69
|
.drw-browser-item:hover{background:var(--dsw-alias-bg-layer-3)}
|
|
64
70
|
`
|
|
@@ -67,20 +73,27 @@ window.__ModuleLoader__.load({
|
|
|
67
73
|
|
|
68
74
|
const en = {
|
|
69
75
|
title: 'Remote Workspace',
|
|
70
|
-
subtitle: '
|
|
76
|
+
subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
|
|
71
77
|
status: 'Ready',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
badgeOnline: 'Active Host: {name} ({latency} ms)',
|
|
79
|
+
badgeOffline: 'No active host',
|
|
80
|
+
badgeTunnels: 'Tunnels: {count}',
|
|
81
|
+
badgeKey: '🔑 Key Auth',
|
|
82
|
+
badgePassword: '🔒 Password Auth',
|
|
75
83
|
profilesTitle: '🖥️ Remote SSH Profiles',
|
|
76
|
-
profilesDesc: 'Configure remote
|
|
84
|
+
profilesDesc: 'Configure remote machines for code execution, SFTP browsing, and background mirror sync.',
|
|
77
85
|
addBtn: '+ Add Host',
|
|
78
86
|
nameCol: 'Name',
|
|
79
87
|
hostCol: 'Host / User',
|
|
80
88
|
portCol: 'Port',
|
|
81
|
-
pathCol: 'Remote
|
|
89
|
+
pathCol: 'Remote Workspace',
|
|
82
90
|
actionsCol: 'Actions',
|
|
83
|
-
noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create
|
|
91
|
+
noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create your first connection.',
|
|
92
|
+
activeBadge: 'Active',
|
|
93
|
+
pinBtn: 'Pin Active',
|
|
94
|
+
editBtn: 'Edit',
|
|
95
|
+
delBtn: 'Delete',
|
|
96
|
+
delConfirm: 'Delete host profile "{name}"?',
|
|
84
97
|
formTitleNew: 'Add New Host Profile',
|
|
85
98
|
formTitleEdit: 'Edit Host Profile',
|
|
86
99
|
fName: 'Profile Name',
|
|
@@ -90,45 +103,73 @@ window.__ModuleLoader__.load({
|
|
|
90
103
|
fAuthType: 'Authentication Method',
|
|
91
104
|
fAuthKey: 'SSH Private Key',
|
|
92
105
|
fAuthPass: 'Password',
|
|
93
|
-
fKeyPath: 'Key Path',
|
|
106
|
+
fKeyPath: 'Private Key Path',
|
|
107
|
+
fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
|
|
94
108
|
fPassphrase: 'Key Passphrase (optional)',
|
|
95
|
-
fPassword: 'Password',
|
|
96
|
-
fRemotePath: 'Remote
|
|
97
|
-
fRemotePathHint: '
|
|
109
|
+
fPassword: 'Account Password',
|
|
110
|
+
fRemotePath: 'Remote Workspace Directory',
|
|
111
|
+
fRemotePathHint: 'Absolute path on the server (e.g. /home/user/my-project)',
|
|
112
|
+
fLocalMirror: 'Local Mirror Directory',
|
|
113
|
+
fLocalMirrorHint: 'Local directory where files are mirrored for fast 3-way synchronization',
|
|
98
114
|
btnBrowse: '📁 Browse Remote...',
|
|
99
|
-
btnSelectThis: 'Select
|
|
100
|
-
btnTest: '⚡ Test Connection',
|
|
115
|
+
btnSelectThis: 'Select Directory',
|
|
116
|
+
btnTest: '⚡ Test Connection (Ping)',
|
|
101
117
|
btnSave: 'Save Profile',
|
|
102
118
|
btnCancel: 'Cancel',
|
|
103
|
-
btnDelete: 'Delete',
|
|
104
119
|
testing: 'Testing Connection...',
|
|
105
120
|
saving: 'Saving...',
|
|
121
|
+
testOk: '✅ Connection OK! Latency: {latency} ms | OS: {os}',
|
|
122
|
+
testFail: '❌ Connection Failed: {err}',
|
|
106
123
|
diagTitle: '📊 Active Connection Diagnostics',
|
|
107
|
-
diagDesc: '
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
diagDesc: 'Real-time telemetry and engine status for the currently selected remote machine.',
|
|
125
|
+
diagProbeBtn: '⚡ Quick Probe',
|
|
126
|
+
syncTitle: '🔄 Two-Way Workspace Mirror Sync',
|
|
127
|
+
syncDesc: 'Fast 3-way conflict-aware file synchronization between local mirror and remote server.',
|
|
110
128
|
pullBtn: '⬇️ Pull from Remote',
|
|
111
129
|
pushBtn: '⬆️ Push to Remote',
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
pulling: 'Pulling files…',
|
|
131
|
+
pushing: 'Pushing files…',
|
|
132
|
+
syncOk: 'Sync completed! Updated {count} file(s).',
|
|
133
|
+
syncConflict: 'Conflict warning: {count} file(s) modified on both sides.',
|
|
134
|
+
syncErr: 'Sync error: {err}',
|
|
135
|
+
tunnelsTitle: '🔀 SSH Port Forwarding (Tunnels)',
|
|
136
|
+
tunnelsDesc: 'Forward remote ports (e.g. databases, web servers, APIs) to your local machine via encrypted SSH tunnels.',
|
|
137
|
+
tLocalPort: 'Local Port',
|
|
138
|
+
tRemotePort: 'Remote Port',
|
|
139
|
+
tOpenBtn: '+ Open Tunnel',
|
|
140
|
+
tOpening: 'Opening…',
|
|
141
|
+
tStopBtn: 'Stop',
|
|
142
|
+
tNoTunnels: 'No active port forwarding tunnels.',
|
|
143
|
+
tunnelsColId: 'Tunnel ID',
|
|
144
|
+
tunnelsColRoute: 'Forwarding Route',
|
|
145
|
+
tunnelsColActions: 'Actions',
|
|
146
|
+
tOpenOk: 'Tunnel opened: 127.0.0.1:{localPort} -> Remote:{remotePort}',
|
|
147
|
+
tOpenErr: 'Failed to open tunnel: {err}'
|
|
114
148
|
}
|
|
115
149
|
|
|
116
150
|
const ru = {
|
|
117
151
|
title: 'Удалённое рабочее пространство',
|
|
118
|
-
subtitle: '
|
|
152
|
+
subtitle: 'SSH-подключения, доступ к файлам по SFTP, 3-way синхронизация зеркала и проброс портов.',
|
|
119
153
|
status: 'Готово',
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
badgeOnline: 'Активный хост: {name} ({latency} мс)',
|
|
155
|
+
badgeOffline: 'Хост не выбран',
|
|
156
|
+
badgeTunnels: 'Туннелей: {count}',
|
|
157
|
+
badgeKey: '🔑 Авторизация по ключу',
|
|
158
|
+
badgePassword: '🔒 Авторизация по паролю',
|
|
123
159
|
profilesTitle: '🖥️ Профили SSH серверов',
|
|
124
|
-
profilesDesc: 'Настройка удалённых
|
|
160
|
+
profilesDesc: 'Настройка удалённых серверов для запуска команд, SFTP-навигации и синхронизации зеркала.',
|
|
125
161
|
addBtn: '+ Добавить хост',
|
|
126
162
|
nameCol: 'Имя',
|
|
127
163
|
hostCol: 'Хост / Пользователь',
|
|
128
164
|
portCol: 'Порт',
|
|
129
165
|
pathCol: 'Удалённый путь',
|
|
130
166
|
actionsCol: 'Действия',
|
|
131
|
-
noHosts: 'Удалённые хосты ещё не настроены. Нажмите «+ Добавить
|
|
167
|
+
noHosts: 'Удалённые хосты ещё не настроены. Нажмите «+ Добавить хост» для создания подключения.',
|
|
168
|
+
activeBadge: 'Активен',
|
|
169
|
+
pinBtn: 'Сделать активным',
|
|
170
|
+
editBtn: 'Править',
|
|
171
|
+
delBtn: 'Удалить',
|
|
172
|
+
delConfirm: 'Удалить профиль хоста «{name}»?',
|
|
132
173
|
formTitleNew: 'Добавление нового SSH хоста',
|
|
133
174
|
formTitleEdit: 'Редактирование профиля хоста',
|
|
134
175
|
fName: 'Название подключения',
|
|
@@ -138,32 +179,59 @@ window.__ModuleLoader__.load({
|
|
|
138
179
|
fAuthType: 'Способ авторизации',
|
|
139
180
|
fAuthKey: 'SSH-ключ',
|
|
140
181
|
fAuthPass: 'Пароль',
|
|
141
|
-
fKeyPath: 'Путь к ключу',
|
|
182
|
+
fKeyPath: 'Путь к приватному ключу',
|
|
183
|
+
fKeyPathHint: 'Путь к ~/.ssh/id_rsa или /home/user/.ssh/id_ed25519',
|
|
142
184
|
fPassphrase: 'Кодовая фраза ключа (если есть)',
|
|
143
|
-
fPassword: 'Пароль
|
|
185
|
+
fPassword: 'Пароль учётной записи',
|
|
144
186
|
fRemotePath: 'Удалённая папка проекта',
|
|
145
|
-
fRemotePathHint: '
|
|
146
|
-
|
|
147
|
-
|
|
187
|
+
fRemotePathHint: 'Абсолютный путь на сервере (например, /home/user/project)',
|
|
188
|
+
fLocalMirror: 'Локальная папка зеркала',
|
|
189
|
+
fLocalMirrorHint: 'Локальная папка для быстрого зеркалирования и бесконфликтной 3-way синхронизации',
|
|
190
|
+
btnBrowse: '📁 Обзор на сервере…',
|
|
191
|
+
btnSelectThis: 'Выбрать папку',
|
|
148
192
|
btnTest: '⚡ Проверить связь (Ping)',
|
|
149
193
|
btnSave: 'Сохранить профиль',
|
|
150
194
|
btnCancel: 'Отмена',
|
|
151
|
-
btnDelete: 'Удалить',
|
|
152
195
|
testing: 'Проверка соединения…',
|
|
153
196
|
saving: 'Сохранение…',
|
|
197
|
+
testOk: '✅ Соединение успешно! Задержка: {latency} мс | ОС: {os}',
|
|
198
|
+
testFail: '❌ Ошибка подключения: {err}',
|
|
154
199
|
diagTitle: '📊 Диагностика активного подключения',
|
|
155
|
-
diagDesc: '
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
diagDesc: 'Телеметрия в реальном времени и состояние подсистемы для выбранного сервера.',
|
|
201
|
+
diagProbeBtn: '⚡ Опросить сервер',
|
|
202
|
+
syncTitle: '🔄 Двусторонняя синхронизация зеркала',
|
|
203
|
+
syncDesc: 'Бесконфликтная 3-way синхронизация файлов между локальным зеркалом и сервером.',
|
|
158
204
|
pullBtn: '⬇️ Скачать с сервера (Pull)',
|
|
159
205
|
pushBtn: '⬆️ Загрузить на сервер (Push)',
|
|
160
|
-
|
|
161
|
-
|
|
206
|
+
pulling: 'Скачивание файлов…',
|
|
207
|
+
pushing: 'Загрузка файлов…',
|
|
208
|
+
syncOk: 'Синхронизация завершена! Обновлено файлов: {count}.',
|
|
209
|
+
syncConflict: 'Внимание: конфликт изменений в {count} файлах.',
|
|
210
|
+
syncErr: 'Ошибка синхронизации: {err}',
|
|
211
|
+
tunnelsTitle: '🔀 SSH-туннели (Port Forwarding)',
|
|
212
|
+
tunnelsDesc: 'Проброс удалённых портов (базы данных, dev-серверы, API) на локальный компьютер через зашифрованный канал.',
|
|
213
|
+
tLocalPort: 'Локальный порт',
|
|
214
|
+
tRemotePort: 'Удалённый порт',
|
|
215
|
+
tOpenBtn: '+ Открыть туннель',
|
|
216
|
+
tOpening: 'Открытие…',
|
|
217
|
+
tStopBtn: 'Закрыть',
|
|
218
|
+
tNoTunnels: 'Нет активных туннелей проброса портов.',
|
|
219
|
+
tunnelsColId: 'ID туннеля',
|
|
220
|
+
tunnelsColRoute: 'Маршрут перенаправления',
|
|
221
|
+
tunnelsColActions: 'Действия',
|
|
222
|
+
tOpenOk: 'Туннель запущен: 127.0.0.1:{localPort} -> Remote:{remotePort}',
|
|
223
|
+
tOpenErr: 'Не удалось запустить туннель: {err}'
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
function makeT(dict, fallback) {
|
|
165
|
-
return function t(key) {
|
|
166
|
-
|
|
227
|
+
return function t(key, vars) {
|
|
228
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key
|
|
229
|
+
if (vars && typeof val === 'string') {
|
|
230
|
+
for (const k of Object.keys(vars)) {
|
|
231
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return val
|
|
167
235
|
}
|
|
168
236
|
}
|
|
169
237
|
|
|
@@ -185,10 +253,17 @@ window.__ModuleLoader__.load({
|
|
|
185
253
|
const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
|
|
186
254
|
const [profiles, setProfiles] = React.useState([])
|
|
187
255
|
const [activeId, setActiveId] = React.useState(null)
|
|
188
|
-
const [
|
|
256
|
+
const [tunnels, setTunnels] = React.useState([])
|
|
257
|
+
const [editing, setEditing] = React.useState(null)
|
|
189
258
|
const [busy, setBusy] = React.useState(null)
|
|
190
259
|
const [testResult, setTestResult] = React.useState(null)
|
|
191
260
|
const [syncMsg, setSyncMsg] = React.useState(null)
|
|
261
|
+
const [tunnelMsg, setTunnelMsg] = React.useState(null)
|
|
262
|
+
const [showPassword, setShowPassword] = React.useState(false)
|
|
263
|
+
|
|
264
|
+
// Tunnel creation inputs
|
|
265
|
+
const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
|
|
266
|
+
const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
|
|
192
267
|
|
|
193
268
|
// Remote Directory Browser State
|
|
194
269
|
const [browserOpen, setBrowserOpen] = React.useState(false)
|
|
@@ -204,6 +279,7 @@ window.__ModuleLoader__.load({
|
|
|
204
279
|
if (data.ok) {
|
|
205
280
|
setProfiles(data.profiles || [])
|
|
206
281
|
setActiveId(data.activeId || null)
|
|
282
|
+
setTunnels(data.tunnels || [])
|
|
207
283
|
}
|
|
208
284
|
}
|
|
209
285
|
} catch (_) {}
|
|
@@ -225,13 +301,15 @@ window.__ModuleLoader__.load({
|
|
|
225
301
|
if (res.ok) {
|
|
226
302
|
setEditing(null)
|
|
227
303
|
setBrowserOpen(false)
|
|
304
|
+
setTestResult(null)
|
|
228
305
|
await loadState()
|
|
229
306
|
}
|
|
230
307
|
} catch (_) {}
|
|
231
308
|
setBusy(null)
|
|
232
309
|
}
|
|
233
310
|
|
|
234
|
-
const handleDelete = async (id) => {
|
|
311
|
+
const handleDelete = async (id, name) => {
|
|
312
|
+
if (!confirm(t('delConfirm', { name: name || id }))) return
|
|
235
313
|
try {
|
|
236
314
|
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
237
315
|
method: 'POST',
|
|
@@ -305,27 +383,95 @@ window.__ModuleLoader__.load({
|
|
|
305
383
|
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
306
384
|
method: 'POST',
|
|
307
385
|
headers: { 'Content-Type': 'application/json' },
|
|
308
|
-
body: JSON.stringify({
|
|
386
|
+
body: JSON.stringify({ direction })
|
|
387
|
+
})
|
|
388
|
+
const data = await res.json()
|
|
389
|
+
if (data.ok) {
|
|
390
|
+
const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
|
|
391
|
+
if (data.conflicts && data.conflicts.length > 0) {
|
|
392
|
+
setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
|
|
393
|
+
} else {
|
|
394
|
+
setSyncMsg({ ok: true, text: t('syncOk', { count }) })
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
|
|
401
|
+
}
|
|
402
|
+
setBusy(null)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const handleStartTunnel = async () => {
|
|
406
|
+
if (!activeId) return
|
|
407
|
+
setBusy('tunnel_open')
|
|
408
|
+
setTunnelMsg(null)
|
|
409
|
+
try {
|
|
410
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/start', {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: { 'Content-Type': 'application/json' },
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
profileId: activeId,
|
|
415
|
+
localPort: newTunnelLocal,
|
|
416
|
+
remotePort: newTunnelRemote
|
|
417
|
+
})
|
|
309
418
|
})
|
|
310
419
|
const data = await res.json()
|
|
311
420
|
if (data.ok) {
|
|
312
|
-
|
|
421
|
+
setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
|
|
422
|
+
await loadState()
|
|
313
423
|
} else {
|
|
314
|
-
|
|
424
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
|
|
315
425
|
}
|
|
316
426
|
} catch (err) {
|
|
317
|
-
|
|
427
|
+
setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
|
|
318
428
|
}
|
|
319
429
|
setBusy(null)
|
|
320
430
|
}
|
|
321
431
|
|
|
432
|
+
const handleStopTunnel = async (tunnelId) => {
|
|
433
|
+
try {
|
|
434
|
+
const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
437
|
+
body: JSON.stringify({ tunnelId })
|
|
438
|
+
})
|
|
439
|
+
if (res.ok) {
|
|
440
|
+
await loadState()
|
|
441
|
+
}
|
|
442
|
+
} catch (_) {}
|
|
443
|
+
}
|
|
444
|
+
|
|
322
445
|
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
323
446
|
|
|
324
447
|
return React.createElement(
|
|
325
448
|
'div',
|
|
326
449
|
{ className: 'drw-page' },
|
|
327
450
|
|
|
328
|
-
//
|
|
451
|
+
// Header with Live Badges (ClineBot Style)
|
|
452
|
+
React.createElement(
|
|
453
|
+
'div',
|
|
454
|
+
{ className: 'drw-header' },
|
|
455
|
+
React.createElement(
|
|
456
|
+
'div',
|
|
457
|
+
{ className: 'drw-header-top' },
|
|
458
|
+
React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
|
|
459
|
+
React.createElement(
|
|
460
|
+
'div',
|
|
461
|
+
{ className: 'drw-header-badges' },
|
|
462
|
+
activeProfile
|
|
463
|
+
? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
|
|
464
|
+
: React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
|
|
465
|
+
activeProfile
|
|
466
|
+
? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
|
|
467
|
+
: null,
|
|
468
|
+
React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
|
|
469
|
+
)
|
|
470
|
+
),
|
|
471
|
+
React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
|
|
472
|
+
),
|
|
473
|
+
|
|
474
|
+
// Section 1: SSH Profiles Table & Management
|
|
329
475
|
React.createElement(
|
|
330
476
|
'div',
|
|
331
477
|
{ className: 'drw-card' },
|
|
@@ -339,7 +485,7 @@ window.__ModuleLoader__.load({
|
|
|
339
485
|
{
|
|
340
486
|
type: 'button',
|
|
341
487
|
className: 'drw-btn drw-btn-primary',
|
|
342
|
-
onClick: () =>
|
|
488
|
+
onClick: () => {
|
|
343
489
|
setEditing({
|
|
344
490
|
id: 'prof_' + Math.random().toString(36).substr(2, 9),
|
|
345
491
|
name: 'New Server',
|
|
@@ -353,6 +499,8 @@ window.__ModuleLoader__.load({
|
|
|
353
499
|
remoteWorkspace: '',
|
|
354
500
|
localMirrorPath: ''
|
|
355
501
|
})
|
|
502
|
+
setTestResult(null)
|
|
503
|
+
}
|
|
356
504
|
},
|
|
357
505
|
t('addBtn')
|
|
358
506
|
)
|
|
@@ -360,22 +508,26 @@ window.__ModuleLoader__.load({
|
|
|
360
508
|
),
|
|
361
509
|
React.createElement('div', { className: 'drw-card-desc' }, t('profilesDesc')),
|
|
362
510
|
|
|
363
|
-
// Editing Form
|
|
511
|
+
// Interactive Editing Form
|
|
364
512
|
editing
|
|
365
513
|
? React.createElement(
|
|
366
514
|
'div',
|
|
367
515
|
{
|
|
368
516
|
style: {
|
|
369
|
-
padding: '
|
|
370
|
-
borderRadius: '
|
|
517
|
+
padding: '16px',
|
|
518
|
+
borderRadius: '10px',
|
|
371
519
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
372
520
|
border: '1px solid var(--dsw-alias-border-l2)',
|
|
373
521
|
display: 'flex',
|
|
374
522
|
flexDirection: 'column',
|
|
375
|
-
gap: '
|
|
523
|
+
gap: '14px'
|
|
376
524
|
}
|
|
377
525
|
},
|
|
378
|
-
React.createElement(
|
|
526
|
+
React.createElement(
|
|
527
|
+
'div',
|
|
528
|
+
{ style: { fontWeight: 600, fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } },
|
|
529
|
+
editing.name ? t('formTitleEdit') : t('formTitleNew')
|
|
530
|
+
),
|
|
379
531
|
React.createElement(
|
|
380
532
|
'div',
|
|
381
533
|
{ className: 'drw-grid-2' },
|
|
@@ -395,7 +547,7 @@ window.__ModuleLoader__.load({
|
|
|
395
547
|
React.createElement('span', { className: 'drw-label' }, t('fHost')),
|
|
396
548
|
React.createElement('input', {
|
|
397
549
|
className: 'drw-input',
|
|
398
|
-
placeholder: '192.168.1.100 or domain.com',
|
|
550
|
+
placeholder: '192.168.1.100 or server.domain.com',
|
|
399
551
|
value: editing.host || '',
|
|
400
552
|
onChange: (e) => setEditing({ ...editing, host: e.target.value })
|
|
401
553
|
})
|
|
@@ -458,13 +610,27 @@ window.__ModuleLoader__.load({
|
|
|
458
610
|
'div',
|
|
459
611
|
{ className: 'drw-field' },
|
|
460
612
|
React.createElement('span', { className: 'drw-label' }, t('fPassword')),
|
|
461
|
-
React.createElement(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
613
|
+
React.createElement(
|
|
614
|
+
'div',
|
|
615
|
+
{ style: { display: 'flex', gap: '8px' } },
|
|
616
|
+
React.createElement('input', {
|
|
617
|
+
className: 'drw-input',
|
|
618
|
+
type: showPassword ? 'text' : 'password',
|
|
619
|
+
placeholder: '••••••••••••',
|
|
620
|
+
value: editing.password || '',
|
|
621
|
+
onChange: (e) => setEditing({ ...editing, password: e.target.value })
|
|
622
|
+
}),
|
|
623
|
+
React.createElement(
|
|
624
|
+
'button',
|
|
625
|
+
{
|
|
626
|
+
type: 'button',
|
|
627
|
+
className: 'drw-btn',
|
|
628
|
+
style: { fontSize: '11px', whiteSpace: 'nowrap' },
|
|
629
|
+
onClick: () => setShowPassword((v) => !v)
|
|
630
|
+
},
|
|
631
|
+
showPassword ? 'Hide' : 'Show'
|
|
632
|
+
)
|
|
633
|
+
)
|
|
468
634
|
)
|
|
469
635
|
: React.createElement(
|
|
470
636
|
'div',
|
|
@@ -478,7 +644,8 @@ window.__ModuleLoader__.load({
|
|
|
478
644
|
placeholder: '~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
|
|
479
645
|
value: editing.privateKeyPath || '',
|
|
480
646
|
onChange: (e) => setEditing({ ...editing, privateKeyPath: e.target.value })
|
|
481
|
-
})
|
|
647
|
+
}),
|
|
648
|
+
React.createElement('span', { className: 'drw-hint' }, t('fKeyPathHint'))
|
|
482
649
|
),
|
|
483
650
|
React.createElement(
|
|
484
651
|
'div',
|
|
@@ -494,39 +661,52 @@ window.__ModuleLoader__.load({
|
|
|
494
661
|
)
|
|
495
662
|
),
|
|
496
663
|
|
|
497
|
-
//
|
|
664
|
+
// Workspace Paths (Remote Workspace & Local Mirror)
|
|
498
665
|
React.createElement(
|
|
499
666
|
'div',
|
|
500
|
-
{
|
|
667
|
+
{ className: 'drw-grid-2' },
|
|
501
668
|
React.createElement(
|
|
502
669
|
'div',
|
|
503
|
-
{
|
|
670
|
+
{ className: 'drw-field' },
|
|
504
671
|
React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
|
|
505
|
-
React.createElement(
|
|
672
|
+
React.createElement(
|
|
673
|
+
'div',
|
|
674
|
+
{ style: { display: 'flex', gap: '8px' } },
|
|
675
|
+
React.createElement('input', {
|
|
676
|
+
className: 'drw-input',
|
|
677
|
+
placeholder: '/home/user/project',
|
|
678
|
+
value: editing.remoteWorkspace || '',
|
|
679
|
+
onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
|
|
680
|
+
}),
|
|
681
|
+
React.createElement(
|
|
682
|
+
'button',
|
|
683
|
+
{
|
|
684
|
+
type: 'button',
|
|
685
|
+
className: 'drw-btn',
|
|
686
|
+
style: { whiteSpace: 'nowrap', fontSize: '12px' },
|
|
687
|
+
disabled: !editing.host,
|
|
688
|
+
onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
|
|
689
|
+
},
|
|
690
|
+
t('btnBrowse')
|
|
691
|
+
)
|
|
692
|
+
),
|
|
693
|
+
React.createElement('span', { className: 'drw-hint' }, t('fRemotePathHint'))
|
|
506
694
|
),
|
|
507
695
|
React.createElement(
|
|
508
696
|
'div',
|
|
509
|
-
{
|
|
697
|
+
{ className: 'drw-field' },
|
|
698
|
+
React.createElement('span', { className: 'drw-label' }, t('fLocalMirror')),
|
|
510
699
|
React.createElement('input', {
|
|
511
700
|
className: 'drw-input',
|
|
512
|
-
placeholder: '/
|
|
513
|
-
value: editing.
|
|
514
|
-
onChange: (e) => setEditing({ ...editing,
|
|
701
|
+
placeholder: '~/.dsh/mirrors/my-project',
|
|
702
|
+
value: editing.localMirrorPath || '',
|
|
703
|
+
onChange: (e) => setEditing({ ...editing, localMirrorPath: e.target.value })
|
|
515
704
|
}),
|
|
516
|
-
React.createElement(
|
|
517
|
-
'button',
|
|
518
|
-
{
|
|
519
|
-
type: 'button',
|
|
520
|
-
className: 'drw-btn',
|
|
521
|
-
disabled: !editing.host,
|
|
522
|
-
onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
|
|
523
|
-
},
|
|
524
|
-
t('btnBrowse')
|
|
525
|
-
)
|
|
705
|
+
React.createElement('span', { className: 'drw-hint' }, t('fLocalMirrorHint'))
|
|
526
706
|
)
|
|
527
707
|
),
|
|
528
708
|
|
|
529
|
-
//
|
|
709
|
+
// Directory Browser Modal
|
|
530
710
|
browserOpen
|
|
531
711
|
? React.createElement(
|
|
532
712
|
'div',
|
|
@@ -627,6 +807,7 @@ window.__ModuleLoader__.load({
|
|
|
627
807
|
onClick: () => {
|
|
628
808
|
setEditing(null)
|
|
629
809
|
setBrowserOpen(false)
|
|
810
|
+
setTestResult(null)
|
|
630
811
|
}
|
|
631
812
|
},
|
|
632
813
|
t('btnCancel')
|
|
@@ -652,14 +833,14 @@ window.__ModuleLoader__.load({
|
|
|
652
833
|
}
|
|
653
834
|
},
|
|
654
835
|
testResult.ok
|
|
655
|
-
?
|
|
656
|
-
:
|
|
836
|
+
? t('testOk', { latency: testResult.latencyMs ?? testResult.latency ?? 0, os: testResult.remoteOs || testResult.os || 'Linux' })
|
|
837
|
+
: t('testFail', { err: testResult.error || 'Connection timed out' })
|
|
657
838
|
)
|
|
658
839
|
: null
|
|
659
840
|
)
|
|
660
841
|
: null,
|
|
661
842
|
|
|
662
|
-
// Profiles Table
|
|
843
|
+
// Profiles List Table
|
|
663
844
|
profiles.length > 0
|
|
664
845
|
? React.createElement(
|
|
665
846
|
'table',
|
|
@@ -688,7 +869,7 @@ window.__ModuleLoader__.load({
|
|
|
688
869
|
React.createElement(
|
|
689
870
|
'td',
|
|
690
871
|
null,
|
|
691
|
-
React.createElement('span', { style: { fontWeight: 600 } }, p.name),
|
|
872
|
+
React.createElement('span', { style: { fontWeight: 600 } }, p.name || 'Server'),
|
|
692
873
|
React.createElement(
|
|
693
874
|
'span',
|
|
694
875
|
{ className: 'drw-badge', style: { marginLeft: '8px' } },
|
|
@@ -698,15 +879,15 @@ window.__ModuleLoader__.load({
|
|
|
698
879
|
? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
|
|
699
880
|
: null
|
|
700
881
|
),
|
|
701
|
-
React.createElement('td',
|
|
702
|
-
React.createElement('td',
|
|
703
|
-
React.createElement('td',
|
|
882
|
+
React.createElement('td', { style: { fontFamily: 'monospace' } }, `${p.username || 'root'}@${p.host}`),
|
|
883
|
+
React.createElement('td', { style: { fontFamily: 'monospace' } }, p.port || 22),
|
|
884
|
+
React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, p.remoteWorkspace || '—'),
|
|
704
885
|
React.createElement(
|
|
705
886
|
'td',
|
|
706
887
|
{ style: { textAlign: 'right' } },
|
|
707
888
|
React.createElement(
|
|
708
889
|
'div',
|
|
709
|
-
{
|
|
890
|
+
{ style: { display: 'inline-flex', gap: '6px' } },
|
|
710
891
|
!isAct
|
|
711
892
|
? React.createElement(
|
|
712
893
|
'button',
|
|
@@ -716,7 +897,7 @@ window.__ModuleLoader__.load({
|
|
|
716
897
|
style: { fontSize: '11px', padding: '3px 8px' },
|
|
717
898
|
onClick: () => handleSetActive(p.id)
|
|
718
899
|
},
|
|
719
|
-
t('
|
|
900
|
+
t('pinBtn')
|
|
720
901
|
)
|
|
721
902
|
: null,
|
|
722
903
|
React.createElement(
|
|
@@ -728,10 +909,9 @@ window.__ModuleLoader__.load({
|
|
|
728
909
|
onClick: () => {
|
|
729
910
|
setEditing({ ...p })
|
|
730
911
|
setTestResult(null)
|
|
731
|
-
setBrowserOpen(false)
|
|
732
912
|
}
|
|
733
913
|
},
|
|
734
|
-
'
|
|
914
|
+
t('editBtn')
|
|
735
915
|
),
|
|
736
916
|
React.createElement(
|
|
737
917
|
'button',
|
|
@@ -739,9 +919,9 @@ window.__ModuleLoader__.load({
|
|
|
739
919
|
type: 'button',
|
|
740
920
|
className: 'drw-btn drw-btn-danger',
|
|
741
921
|
style: { fontSize: '11px', padding: '3px 8px' },
|
|
742
|
-
onClick: () => handleDelete(p.id)
|
|
922
|
+
onClick: () => handleDelete(p.id, p.name)
|
|
743
923
|
},
|
|
744
|
-
'
|
|
924
|
+
t('delBtn')
|
|
745
925
|
)
|
|
746
926
|
)
|
|
747
927
|
)
|
|
@@ -759,41 +939,56 @@ window.__ModuleLoader__.load({
|
|
|
759
939
|
? React.createElement(
|
|
760
940
|
'div',
|
|
761
941
|
{ className: 'drw-card' },
|
|
762
|
-
React.createElement(
|
|
942
|
+
React.createElement(
|
|
943
|
+
'div',
|
|
944
|
+
{ className: 'drw-card-title' },
|
|
945
|
+
React.createElement('span', null, t('diagTitle')),
|
|
946
|
+
React.createElement(
|
|
947
|
+
'button',
|
|
948
|
+
{
|
|
949
|
+
type: 'button',
|
|
950
|
+
className: 'drw-btn',
|
|
951
|
+
style: { fontSize: '11px', padding: '3px 9px' },
|
|
952
|
+
disabled: busy === 'testing',
|
|
953
|
+
onClick: () => handleTest(activeProfile)
|
|
954
|
+
},
|
|
955
|
+
busy === 'testing' ? t('testing') : t('diagProbeBtn')
|
|
956
|
+
)
|
|
957
|
+
),
|
|
763
958
|
React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
|
|
764
959
|
React.createElement(
|
|
765
960
|
'div',
|
|
766
|
-
{ className: 'drw-grid-
|
|
961
|
+
{ className: 'drw-grid-4' },
|
|
767
962
|
React.createElement(
|
|
768
963
|
'div',
|
|
769
964
|
{ className: 'drw-stat-box' },
|
|
770
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name),
|
|
771
|
-
React.createElement('div', { className: 'drw-stat-lbl' },
|
|
965
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
|
|
966
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
|
|
772
967
|
),
|
|
773
968
|
React.createElement(
|
|
774
969
|
'div',
|
|
775
970
|
{ className: 'drw-stat-box' },
|
|
776
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—
|
|
777
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace
|
|
971
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
|
|
972
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
|
|
778
973
|
),
|
|
779
974
|
React.createElement(
|
|
780
975
|
'div',
|
|
781
976
|
{ className: 'drw-stat-box' },
|
|
782
|
-
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.
|
|
783
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, '
|
|
977
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
|
|
978
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
|
|
784
979
|
),
|
|
785
980
|
React.createElement(
|
|
786
981
|
'div',
|
|
787
982
|
{ className: 'drw-stat-box' },
|
|
788
|
-
React.createElement('div', { className: 'drw-stat-val' }, 'Ready'),
|
|
789
|
-
React.createElement('div', { className: 'drw-stat-lbl' }, '
|
|
983
|
+
React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
|
|
984
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
|
|
790
985
|
)
|
|
791
986
|
)
|
|
792
987
|
)
|
|
793
988
|
: null,
|
|
794
989
|
|
|
795
990
|
// Section 3: Mirror Sync (ClineBot Action Card style)
|
|
796
|
-
activeProfile
|
|
991
|
+
activeProfile
|
|
797
992
|
? React.createElement(
|
|
798
993
|
'div',
|
|
799
994
|
{ className: 'drw-card' },
|
|
@@ -807,20 +1002,20 @@ window.__ModuleLoader__.load({
|
|
|
807
1002
|
{
|
|
808
1003
|
type: 'button',
|
|
809
1004
|
className: 'drw-btn',
|
|
810
|
-
disabled: !!busy,
|
|
1005
|
+
disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
|
|
811
1006
|
onClick: () => handleSync('pull')
|
|
812
1007
|
},
|
|
813
|
-
busy === 'sync_pull' ? '
|
|
1008
|
+
busy === 'sync_pull' ? t('pulling') : t('pullBtn')
|
|
814
1009
|
),
|
|
815
1010
|
React.createElement(
|
|
816
1011
|
'button',
|
|
817
1012
|
{
|
|
818
1013
|
type: 'button',
|
|
819
1014
|
className: 'drw-btn',
|
|
820
|
-
disabled: !!busy,
|
|
1015
|
+
disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
|
|
821
1016
|
onClick: () => handleSync('push')
|
|
822
1017
|
},
|
|
823
|
-
busy === 'sync_push' ? '
|
|
1018
|
+
busy === 'sync_push' ? t('pushing') : t('pushBtn')
|
|
824
1019
|
)
|
|
825
1020
|
),
|
|
826
1021
|
syncMsg
|
|
@@ -836,11 +1031,126 @@ window.__ModuleLoader__.load({
|
|
|
836
1031
|
)
|
|
837
1032
|
: null
|
|
838
1033
|
)
|
|
839
|
-
: null
|
|
1034
|
+
: null,
|
|
1035
|
+
|
|
1036
|
+
// Section 4: SSH Port Forwarding (Tunnels Manager)
|
|
1037
|
+
React.createElement(
|
|
1038
|
+
'div',
|
|
1039
|
+
{ className: 'drw-card' },
|
|
1040
|
+
React.createElement('div', { className: 'drw-card-title' }, t('tunnelsTitle')),
|
|
1041
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('tunnelsDesc')),
|
|
1042
|
+
|
|
1043
|
+
// Start tunnel inline row
|
|
1044
|
+
activeProfile
|
|
1045
|
+
? React.createElement(
|
|
1046
|
+
'div',
|
|
1047
|
+
{ className: 'drw-row', style: { alignItems: 'flex-end', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px 12px', borderRadius: '8px' } },
|
|
1048
|
+
React.createElement(
|
|
1049
|
+
'div',
|
|
1050
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
|
|
1051
|
+
React.createElement('span', { className: 'drw-label' }, t('tLocalPort')),
|
|
1052
|
+
React.createElement('input', {
|
|
1053
|
+
className: 'drw-input',
|
|
1054
|
+
style: { width: '120px' },
|
|
1055
|
+
type: 'number',
|
|
1056
|
+
value: newTunnelLocal,
|
|
1057
|
+
onChange: (e) => setNewTunnelLocal(e.target.value)
|
|
1058
|
+
})
|
|
1059
|
+
),
|
|
1060
|
+
React.createElement('span', { style: { paddingBottom: '8px', color: 'var(--dsw-alias-label-secondary)' } }, '→'),
|
|
1061
|
+
React.createElement(
|
|
1062
|
+
'div',
|
|
1063
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
|
|
1064
|
+
React.createElement('span', { className: 'drw-label' }, t('tRemotePort')),
|
|
1065
|
+
React.createElement('input', {
|
|
1066
|
+
className: 'drw-input',
|
|
1067
|
+
style: { width: '120px' },
|
|
1068
|
+
type: 'number',
|
|
1069
|
+
value: newTunnelRemote,
|
|
1070
|
+
onChange: (e) => setNewTunnelRemote(e.target.value)
|
|
1071
|
+
})
|
|
1072
|
+
),
|
|
1073
|
+
React.createElement(
|
|
1074
|
+
'button',
|
|
1075
|
+
{
|
|
1076
|
+
type: 'button',
|
|
1077
|
+
className: 'drw-btn drw-btn-primary',
|
|
1078
|
+
disabled: busy === 'tunnel_open',
|
|
1079
|
+
onClick: handleStartTunnel
|
|
1080
|
+
},
|
|
1081
|
+
busy === 'tunnel_open' ? t('tOpening') : t('tOpenBtn')
|
|
1082
|
+
)
|
|
1083
|
+
)
|
|
1084
|
+
: null,
|
|
1085
|
+
|
|
1086
|
+
tunnelMsg
|
|
1087
|
+
? React.createElement(
|
|
1088
|
+
'div',
|
|
1089
|
+
{
|
|
1090
|
+
className: 'drw-preview',
|
|
1091
|
+
style: {
|
|
1092
|
+
borderColor: tunnelMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
tunnelMsg.text
|
|
1096
|
+
)
|
|
1097
|
+
: null,
|
|
1098
|
+
|
|
1099
|
+
// Tunnels Table
|
|
1100
|
+
tunnels.length > 0
|
|
1101
|
+
? React.createElement(
|
|
1102
|
+
'table',
|
|
1103
|
+
{ className: 'drw-table' },
|
|
1104
|
+
React.createElement(
|
|
1105
|
+
'thead',
|
|
1106
|
+
null,
|
|
1107
|
+
React.createElement(
|
|
1108
|
+
'tr',
|
|
1109
|
+
null,
|
|
1110
|
+
React.createElement('th', null, t('tunnelsColId')),
|
|
1111
|
+
React.createElement('th', null, t('tunnelsColRoute')),
|
|
1112
|
+
React.createElement('th', { style: { textAlign: 'right' } }, t('tunnelsColActions'))
|
|
1113
|
+
)
|
|
1114
|
+
),
|
|
1115
|
+
React.createElement(
|
|
1116
|
+
'tbody',
|
|
1117
|
+
null,
|
|
1118
|
+
tunnels.map((tun) =>
|
|
1119
|
+
React.createElement(
|
|
1120
|
+
'tr',
|
|
1121
|
+
{ key: tun.id },
|
|
1122
|
+
React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, tun.id),
|
|
1123
|
+
React.createElement(
|
|
1124
|
+
'td',
|
|
1125
|
+
null,
|
|
1126
|
+
React.createElement('span', { className: 'drw-badge drw-badge-ok' }, `127.0.0.1:${tun.localPort}`),
|
|
1127
|
+
' → ',
|
|
1128
|
+
React.createElement('span', { className: 'drw-badge' }, `${tun.targetHost || '127.0.0.1'}:${tun.remotePort}`)
|
|
1129
|
+
),
|
|
1130
|
+
React.createElement(
|
|
1131
|
+
'td',
|
|
1132
|
+
{ style: { textAlign: 'right' } },
|
|
1133
|
+
React.createElement(
|
|
1134
|
+
'button',
|
|
1135
|
+
{
|
|
1136
|
+
type: 'button',
|
|
1137
|
+
className: 'drw-btn drw-btn-danger',
|
|
1138
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
1139
|
+
onClick: () => handleStopTunnel(tun.id)
|
|
1140
|
+
},
|
|
1141
|
+
t('tStopBtn')
|
|
1142
|
+
)
|
|
1143
|
+
)
|
|
1144
|
+
)
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
)
|
|
1148
|
+
: React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('tNoTunnels'))
|
|
1149
|
+
)
|
|
840
1150
|
)
|
|
841
1151
|
}
|
|
842
1152
|
|
|
843
|
-
// Accordion item for Plugins section
|
|
1153
|
+
// Accordion item for Settings -> Plugins section
|
|
844
1154
|
function PluginCard(props) {
|
|
845
1155
|
const [open, setOpen] = React.useState(false)
|
|
846
1156
|
const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
|
|
@@ -884,7 +1194,9 @@ window.__ModuleLoader__.load({
|
|
|
884
1194
|
)
|
|
885
1195
|
),
|
|
886
1196
|
open
|
|
887
|
-
? React.createElement(
|
|
1197
|
+
? React.createElement(
|
|
1198
|
+
'div',
|
|
1199
|
+
{ style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
|
|
888
1200
|
React.createElement(SettingsView, props)
|
|
889
1201
|
)
|
|
890
1202
|
: null
|
|
@@ -960,7 +1272,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1272
|
}
|
|
961
1273
|
}
|
|
962
1274
|
|
|
963
|
-
module.exports = { apply, inject: ['slots', 'locale'
|
|
1275
|
+
module.exports = { apply, inject: ['slots', 'locale'] }
|
|
964
1276
|
return module.exports
|
|
965
1277
|
}
|
|
966
1278
|
})
|