@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/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-style')) return
13
+ if (document.getElementById('drw-clinebot-full-css')) return
13
14
  const style = document.createElement('style')
14
- style.id = 'drw-clinebot-style'
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 20px;max-width:960px}
18
- .drw-header{display:flex;flex-direction:column;gap:6px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
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.4}
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:12px;list-style:none}
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:-4px;line-height:1.4}
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 8px;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}
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:4px 12px;font-size:12px;font-weight:500;border-radius:6px;color:var(--dsw-alias-label-secondary);transition:all .15s ease}
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:6px}
51
- .drw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
52
- .drw-table td{padding:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
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:15px;font-weight:700;color:var(--dsw-alias-label-primary)}
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:260px}
61
- .drw-browser-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px;max-height:180px;padding-right:4px}
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: 'Manage SSH connections, SFTP sync, and tunnels',
76
+ subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
71
77
  status: 'Ready',
72
- activeBadge: 'Active',
73
- setActiveBtn: 'Set Active',
74
- pinned: 'Pinned Active Host',
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 hosts for code execution, SFTP file access, and mirror syncing.',
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 Path',
89
+ pathCol: 'Remote Workspace',
82
90
  actionsCol: 'Actions',
83
- noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create one.',
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 Project Directory',
97
- fRemotePathHint: '(Optional now, can be chosen later while working)',
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 Current Directory',
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: 'Live connection probe to the currently selected remote machine.',
108
- syncTitle: '🔄 Mirror Sync & Port Tunnels',
109
- syncDesc: 'Synchronize local workspace mirror or check forwarded background port tunnels.',
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
- syncSuccess: 'Sync completed successfully',
113
- syncFailed: 'Sync encountered conflicts or errors: '
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: 'Управление подключениями по SSH, синхронизацией SFTP и туннелями',
152
+ subtitle: 'SSH-подключения, доступ к файлам по SFTP, 3-way синхронизация зеркала и проброс портов.',
119
153
  status: 'Готово',
120
- activeBadge: 'Активен',
121
- setActiveBtn: 'Сделать активным',
122
- pinned: 'Активный хост',
154
+ badgeOnline: 'Активный хост: {name} ({latency} мс)',
155
+ badgeOffline: 'Хост не выбран',
156
+ badgeTunnels: 'Туннелей: {count}',
157
+ badgeKey: '🔑 Авторизация по ключу',
158
+ badgePassword: '🔒 Авторизация по паролю',
123
159
  profilesTitle: '🖥️ Профили SSH серверов',
124
- profilesDesc: 'Настройка удалённых хостов для выполнения команд, SFTP доступа и синхронизации.',
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
- btnBrowse: '📁 Выбрать на сервере…',
147
- btnSelectThis: 'Выбрать эту папку',
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
- syncTitle: '🔄 Синхронизация зеркала и туннели',
157
- syncDesc: 'Ручная синхронизация файлов с удалённым сервером и статус проброса портов.',
200
+ diagDesc: 'Телеметрия в реальном времени и состояние подсистемы для выбранного сервера.',
201
+ diagProbeBtn: '⚡ Опросить сервер',
202
+ syncTitle: '🔄 Двусторонняя синхронизация зеркала',
203
+ syncDesc: 'Бесконфликтная 3-way синхронизация файлов между локальным зеркалом и сервером.',
158
204
  pullBtn: '⬇️ Скачать с сервера (Pull)',
159
205
  pushBtn: '⬆️ Загрузить на сервер (Push)',
160
- syncSuccess: 'Синхронизация успешно выполнена',
161
- syncFailed: 'Ошибка синхронизации: '
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
- return (dict && dict[key]) || (fallback && fallback[key]) || key
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 [editing, setEditing] = React.useState(null) // null or object
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({ profileId: activeId, direction })
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
- setSyncMsg({ ok: true, text: t('syncSuccess') })
421
+ setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
422
+ await loadState()
313
423
  } else {
314
- setSyncMsg({ ok: false, text: t('syncFailed') + (data.error || 'Conflicts detected') })
424
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
315
425
  }
316
426
  } catch (err) {
317
- setSyncMsg({ ok: false, text: err.message })
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
- // Section 1: Profiles Management (ClineBot style)
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: '14px',
370
- borderRadius: '8px',
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: '12px'
523
+ gap: '14px'
376
524
  }
377
525
  },
378
- React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, editing.name ? t('formTitleEdit') : t('formTitleNew')),
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('input', {
462
- className: 'drw-input',
463
- type: 'password',
464
- placeholder: '••••••••••••',
465
- value: editing.password || '',
466
- onChange: (e) => setEditing({ ...editing, password: e.target.value })
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
- // Remote Project Directory (Optional, with live server browse)
664
+ // Workspace Paths (Remote Workspace & Local Mirror)
498
665
  React.createElement(
499
666
  'div',
500
- { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
667
+ { className: 'drw-grid-2' },
501
668
  React.createElement(
502
669
  'div',
503
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
670
+ { className: 'drw-field' },
504
671
  React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
505
- React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, t('fRemotePathHint'))
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
- { style: { display: 'flex', gap: '8px' } },
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: '/var/www/my-project or ~/projects/app (can leave empty for now)',
513
- value: editing.remoteWorkspace || '',
514
- onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
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
- // Remote Directory Browser Modal/Inline Tree
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
- ? `✅ Connection OK! Latency: ${testResult.latencyMs} ms | OS: ${testResult.remoteOs || 'Unknown'}`
656
- : `❌ Connection Failed: ${testResult.error || 'Connection timed out'}`
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', null, `${p.username}@${p.host}`),
702
- React.createElement('td', null, p.port || 22),
703
- React.createElement('td', null, p.remoteWorkspace || '— (not set)'),
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
- { className: 'drw-row', style: { justifyContent: 'flex-end' } },
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('setActiveBtn')
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('div', { className: 'drw-card-title' }, t('diagTitle')),
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-2' },
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' }, 'Host: ' + activeProfile.host)
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 || '— (select during work)'),
777
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace Directory')
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.authType === 'password' ? 'Password Auth' : 'SSH Key Auth'),
783
- React.createElement('div', { className: 'drw-stat-lbl' }, 'User: ' + activeProfile.username)
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' }, 'DSH Remote Engine Status')
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 && activeProfile.remoteWorkspace
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' ? 'Pulling...' : t('pullBtn')
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' ? 'Pushing...' : t('pushBtn')
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('div', { style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
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', 'settingsScope'] }
1275
+ module.exports = { apply, inject: ['slots', 'locale'] }
964
1276
  return module.exports
965
1277
  }
966
1278
  })