@iflow-mcp/npcnpc09-remotex 0.4.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/CLAUDE.md +201 -0
- package/bin/cli.js +484 -0
- package/package.json +1 -0
- package/public/index.html +1806 -0
- package/public/status.html +689 -0
- package/public/terminal.html +454 -0
- package/src/bridge.js +1124 -0
- package/src/dashboard-server.js +799 -0
- package/src/mcp-server.js +849 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
6
|
+
<title>RemoteX</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #0a0e17; --bg2: #111827; --bg3: #1a2236;
|
|
14
|
+
--border: #2a3a5c; --text: #e2e8f0; --text2: #94a3b8;
|
|
15
|
+
--accent: #38bdf8; --green: #22c55e; --red: #ef4444; --orange: #f59e0b; --pink: #f472b6;
|
|
16
|
+
}
|
|
17
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
18
|
+
html, body { height:100%; overflow:hidden; -webkit-overflow-scrolling:touch; }
|
|
19
|
+
body { font-family:'Segoe UI',system-ui,sans-serif; background:var(--bg); color:var(--text); display:flex; flex-direction:column; }
|
|
20
|
+
|
|
21
|
+
/* Login */
|
|
22
|
+
.login-panel { display:flex; align-items:center; justify-content:center; flex:1; }
|
|
23
|
+
.login-box { background:var(--bg2); border:1px solid var(--border); border-radius:12px; padding:32px 28px; width:320px; max-width:90vw; }
|
|
24
|
+
.login-box h2 { color:var(--accent); font-size:20px; margin-bottom:6px; }
|
|
25
|
+
.login-box p { color:var(--text2); font-size:13px; margin-bottom:20px; }
|
|
26
|
+
.login-box input { width:100%; padding:10px 12px; margin-bottom:12px; background:var(--bg); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:14px; outline:none; }
|
|
27
|
+
.login-box input:focus { border-color:var(--accent); }
|
|
28
|
+
.login-box .btn-login { width:100%; padding:10px; background:var(--accent); color:#000; border:none; border-radius:6px; font-size:14px; font-weight:600; cursor:pointer; }
|
|
29
|
+
.login-error { color:var(--red); font-size:12px; margin-top:8px; display:none; }
|
|
30
|
+
|
|
31
|
+
/* Header + Nav */
|
|
32
|
+
.header { background:var(--bg2); border-bottom:1px solid var(--border); padding:8px 12px; display:flex; align-items:center; gap:8px; flex-shrink:0; }
|
|
33
|
+
.logo { font-size:16px; font-weight:700; color:var(--accent); }
|
|
34
|
+
.nav-tabs { display:flex; gap:2px; flex:1; overflow-x:auto; }
|
|
35
|
+
.nav-tab { padding:6px 14px; font-size:12px; font-weight:600; color:var(--text2); cursor:pointer; border-radius:4px; white-space:nowrap; }
|
|
36
|
+
.nav-tab.active { background:var(--accent); color:#000; }
|
|
37
|
+
.nav-tab:not(.active):active { background:var(--bg3); }
|
|
38
|
+
|
|
39
|
+
/* Main panels */
|
|
40
|
+
.main-area { flex:1; overflow:hidden; display:flex; flex-direction:column; }
|
|
41
|
+
.panel { display:none; flex:1; flex-direction:column; overflow:hidden; }
|
|
42
|
+
.panel.active { display:flex; }
|
|
43
|
+
|
|
44
|
+
/* Server list */
|
|
45
|
+
.server-list { padding:12px; overflow-y:auto; flex:1; -webkit-overflow-scrolling:touch; }
|
|
46
|
+
.group-name { font-size:12px; font-weight:600; color:var(--accent); margin:12px 0 6px; padding-bottom:4px; border-bottom:1px solid var(--border); }
|
|
47
|
+
.server-card { background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:10px 12px; margin-bottom:6px; cursor:pointer; display:flex; align-items:center; gap:8px; }
|
|
48
|
+
.server-card:active { border-color:var(--accent); }
|
|
49
|
+
.server-card .dot { width:8px; height:8px; border-radius:50%; background:var(--green); flex-shrink:0; }
|
|
50
|
+
.server-card .info { flex:1; min-width:0; }
|
|
51
|
+
.server-card .name { font-weight:600; font-size:13px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
52
|
+
.server-card .host { font-size:11px; color:var(--text2); }
|
|
53
|
+
.server-card .arrow { color:var(--text2); font-size:16px; }
|
|
54
|
+
|
|
55
|
+
/* Terminal tabs */
|
|
56
|
+
.term-tabs { display:flex; background:var(--bg2); border-bottom:1px solid var(--border); overflow-x:auto; flex-shrink:0; }
|
|
57
|
+
.term-tab { padding:6px 12px; font-size:12px; color:var(--text2); cursor:pointer; border-bottom:2px solid transparent; white-space:nowrap; display:flex; align-items:center; gap:6px; }
|
|
58
|
+
.term-tab.active { color:var(--accent); border-bottom-color:var(--accent); }
|
|
59
|
+
.term-tab .close { font-size:14px; color:var(--text2); cursor:pointer; padding:0 2px; }
|
|
60
|
+
.term-tab .close:active { color:var(--red); }
|
|
61
|
+
.term-container { flex:1; position:relative; overflow:hidden; }
|
|
62
|
+
.term-instance { position:absolute; top:0; left:0; right:0; bottom:0; padding:4px; display:none; }
|
|
63
|
+
.term-instance.active { display:block; }
|
|
64
|
+
|
|
65
|
+
/* Quick bar */
|
|
66
|
+
.quick-bar { background:var(--bg2); border-top:1px solid var(--border); padding:6px 8px; display:flex; gap:4px; overflow-x:auto; flex-shrink:0; }
|
|
67
|
+
.quick-btn { background:var(--bg3); border:1px solid var(--border); color:var(--text); padding:5px 10px; border-radius:4px; font-size:11px; white-space:nowrap; cursor:pointer; flex-shrink:0; }
|
|
68
|
+
.quick-btn:active { background:var(--accent); color:#000; }
|
|
69
|
+
|
|
70
|
+
/* Chat */
|
|
71
|
+
.chat-messages { flex:1; overflow-y:auto; padding:12px; -webkit-overflow-scrolling:touch; }
|
|
72
|
+
.chat-msg { margin-bottom:12px; max-width:90%; }
|
|
73
|
+
.chat-msg.user { margin-left:auto; }
|
|
74
|
+
.chat-msg.assistant { margin-right:auto; }
|
|
75
|
+
.chat-bubble { padding:10px 14px; border-radius:12px; font-size:13px; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
|
|
76
|
+
.chat-msg.user .chat-bubble { background:var(--accent); color:#000; border-bottom-right-radius:4px; }
|
|
77
|
+
.chat-msg.assistant .chat-bubble { background:var(--bg2); border:1px solid var(--border); border-bottom-left-radius:4px; }
|
|
78
|
+
.chat-time { font-size:10px; color:var(--text2); margin-top:2px; padding:0 4px; }
|
|
79
|
+
.chat-msg.user .chat-time { text-align:right; }
|
|
80
|
+
.chat-input-bar { background:var(--bg2); border-top:1px solid var(--border); padding:8px; display:flex; gap:8px; flex-shrink:0; }
|
|
81
|
+
.chat-input { flex:1; padding:10px 12px; background:var(--bg); border:1px solid var(--border); border-radius:20px; color:var(--text); font-size:14px; outline:none; }
|
|
82
|
+
.chat-input:focus { border-color:var(--accent); }
|
|
83
|
+
.chat-send { background:var(--accent); color:#000; border:none; border-radius:50%; width:38px; height:38px; font-size:18px; cursor:pointer; flex-shrink:0; }
|
|
84
|
+
.chat-send:disabled { opacity:0.4; }
|
|
85
|
+
.chat-typing { padding:8px 12px; color:var(--text2); font-size:12px; display:none; }
|
|
86
|
+
.chat-typing.active { display:block; }
|
|
87
|
+
|
|
88
|
+
/* Loading / empty */
|
|
89
|
+
.loading { display:flex; align-items:center; justify-content:center; padding:40px; color:var(--text2); font-size:13px; text-align:center; line-height:1.6; }
|
|
90
|
+
.spinner { width:18px; height:18px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin .8s linear infinite; margin-right:8px; }
|
|
91
|
+
@keyframes spin { to { transform:rotate(360deg); } }
|
|
92
|
+
|
|
93
|
+
/* Search */
|
|
94
|
+
.search-bar { padding:8px 12px; flex-shrink:0; }
|
|
95
|
+
.search-input { width:100%; padding:8px 12px; background:var(--bg); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:13px; outline:none; }
|
|
96
|
+
.search-input:focus { border-color:var(--accent); }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
|
|
101
|
+
<!-- Login -->
|
|
102
|
+
<div class="login-panel" id="login-panel">
|
|
103
|
+
<div class="login-box">
|
|
104
|
+
<h2>RemoteX</h2>
|
|
105
|
+
<p>AI-Powered SSH Fleet Manager</p>
|
|
106
|
+
<input type="text" id="login-user" placeholder="Username" value="admin" autocomplete="username">
|
|
107
|
+
<input type="password" id="login-pass" placeholder="Password" autocomplete="current-password">
|
|
108
|
+
<button class="btn-login" onclick="doLogin()">Login</button>
|
|
109
|
+
<div class="login-error" id="login-error"></div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Main App (hidden until login) -->
|
|
114
|
+
<div id="app" style="display:none;flex:1;display:none;flex-direction:column">
|
|
115
|
+
|
|
116
|
+
<!-- Header -->
|
|
117
|
+
<div class="header">
|
|
118
|
+
<span class="logo">RemoteX</span>
|
|
119
|
+
<div class="nav-tabs">
|
|
120
|
+
<div class="nav-tab active" data-panel="servers" onclick="switchPanel('servers')">Servers</div>
|
|
121
|
+
<div class="nav-tab" data-panel="terminals" onclick="switchPanel('terminals')">Terminals</div>
|
|
122
|
+
<div class="nav-tab" data-panel="chat" onclick="switchPanel('chat')">AI Chat</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="main-area">
|
|
127
|
+
|
|
128
|
+
<!-- Panel: Servers -->
|
|
129
|
+
<div class="panel active" id="panel-servers">
|
|
130
|
+
<div class="search-bar">
|
|
131
|
+
<input class="search-input" id="search-input" placeholder="Search servers..." oninput="filterServers()">
|
|
132
|
+
</div>
|
|
133
|
+
<div class="server-list" id="server-list">
|
|
134
|
+
<div class="loading"><div class="spinner"></div>Loading...</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Panel: Terminals -->
|
|
139
|
+
<div class="panel" id="panel-terminals">
|
|
140
|
+
<div class="term-tabs" id="term-tabs">
|
|
141
|
+
<div style="padding:8px 12px;color:var(--text2);font-size:12px" id="no-terms">No open terminals</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="term-container" id="term-container"></div>
|
|
144
|
+
<div class="quick-bar">
|
|
145
|
+
<button class="quick-btn" onclick="sendQuick('ls -la\n')">ls</button>
|
|
146
|
+
<button class="quick-btn" onclick="sendQuick('df -h\n')">df</button>
|
|
147
|
+
<button class="quick-btn" onclick="sendQuick('free -m\n')">free</button>
|
|
148
|
+
<button class="quick-btn" onclick="sendQuick('top -bn1|head -20\n')">top</button>
|
|
149
|
+
<button class="quick-btn" onclick="sendQuick('docker ps\n')">docker</button>
|
|
150
|
+
<button class="quick-btn" onclick="sendQuick('uptime\n')">uptime</button>
|
|
151
|
+
<button class="quick-btn" onclick="sendQuick('pwd\n')">pwd</button>
|
|
152
|
+
<button class="quick-btn" onclick="sendQuick('\x03')">^C</button>
|
|
153
|
+
<button class="quick-btn" onclick="sendQuick('\t')">Tab</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Panel: AI Chat -->
|
|
158
|
+
<div class="panel" id="panel-chat">
|
|
159
|
+
<div class="chat-messages" id="chat-messages">
|
|
160
|
+
<div class="chat-msg assistant">
|
|
161
|
+
<div class="chat-bubble">Hi! I'm RemoteX AI assistant. Tell me what you want to do with your servers in natural language. For example:<br><br>- "Check disk usage on all SMAPP servers"<br>- "Show memory on SMAPP1"<br>- "List running docker containers on all servers"</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="chat-typing" id="chat-typing"><div class="spinner" style="display:inline-block;vertical-align:middle"></div> AI is thinking...</div>
|
|
165
|
+
<div class="chat-input-bar">
|
|
166
|
+
<input class="chat-input" id="chat-input" placeholder="Ask AI to manage servers..." autocomplete="off">
|
|
167
|
+
<button class="chat-send" id="chat-send" onclick="sendChat()">▶</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<script>
|
|
175
|
+
const HTTP_PORT = location.port || 9876;
|
|
176
|
+
const WS_PORT = parseInt(HTTP_PORT) + 1;
|
|
177
|
+
const API = location.protocol + '//' + location.hostname + ':' + HTTP_PORT;
|
|
178
|
+
const WS_URL = 'ws://' + location.hostname + ':' + WS_PORT;
|
|
179
|
+
|
|
180
|
+
let authUser = '', authPass = '';
|
|
181
|
+
let allServers = [];
|
|
182
|
+
let terminals = {}; // name -> { ws, term, fitAddon, el }
|
|
183
|
+
let activeTerminal = null;
|
|
184
|
+
|
|
185
|
+
// ── Login ──
|
|
186
|
+
|
|
187
|
+
async function doLogin() {
|
|
188
|
+
authUser = document.getElementById('login-user').value.trim();
|
|
189
|
+
authPass = document.getElementById('login-pass').value;
|
|
190
|
+
const err = document.getElementById('login-error');
|
|
191
|
+
err.style.display = 'none';
|
|
192
|
+
try {
|
|
193
|
+
const r = await apiFetch('/servers');
|
|
194
|
+
if (!r.ok) { err.textContent = 'Invalid credentials'; err.style.display = 'block'; return; }
|
|
195
|
+
document.getElementById('login-panel').style.display = 'none';
|
|
196
|
+
const app = document.getElementById('app');
|
|
197
|
+
app.style.display = 'flex';
|
|
198
|
+
loadServers();
|
|
199
|
+
loadChatHistory();
|
|
200
|
+
} catch (e) {
|
|
201
|
+
err.textContent = 'Connection failed: ' + e.message;
|
|
202
|
+
err.style.display = 'block';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
document.getElementById('login-pass').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
|
206
|
+
|
|
207
|
+
// ── API ──
|
|
208
|
+
|
|
209
|
+
function apiFetch(path, opts = {}) {
|
|
210
|
+
opts.headers = { ...opts.headers, 'Authorization': 'Basic ' + btoa(authUser + ':' + authPass), 'X-Requested-With': 'XMLHttpRequest' };
|
|
211
|
+
return fetch(API + path, opts);
|
|
212
|
+
}
|
|
213
|
+
async function apiJson(path, opts = {}) {
|
|
214
|
+
const r = await apiFetch(path, opts);
|
|
215
|
+
return r.json();
|
|
216
|
+
}
|
|
217
|
+
async function apiPost(path, body) {
|
|
218
|
+
return apiJson(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Panel switching ──
|
|
222
|
+
|
|
223
|
+
function switchPanel(name) {
|
|
224
|
+
document.querySelectorAll('.nav-tab').forEach(t => t.classList.toggle('active', t.dataset.panel === name));
|
|
225
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + name));
|
|
226
|
+
if (name === 'terminals' && activeTerminal && terminals[activeTerminal]) {
|
|
227
|
+
setTimeout(() => terminals[activeTerminal].fitAddon.fit(), 50);
|
|
228
|
+
}
|
|
229
|
+
if (name === 'chat') {
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
const msgs = document.getElementById('chat-messages');
|
|
232
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
233
|
+
document.getElementById('chat-input').focus();
|
|
234
|
+
}, 50);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Servers ──
|
|
239
|
+
|
|
240
|
+
async function loadServers() {
|
|
241
|
+
const list = document.getElementById('server-list');
|
|
242
|
+
try {
|
|
243
|
+
const data = await apiJson('/servers');
|
|
244
|
+
allServers = data.servers || [];
|
|
245
|
+
renderServers(allServers);
|
|
246
|
+
} catch (e) {
|
|
247
|
+
list.innerHTML = '<div class="loading">Failed: ' + esc(e.message) + '</div>';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderServers(servers) {
|
|
252
|
+
const list = document.getElementById('server-list');
|
|
253
|
+
if (!servers.length) { list.innerHTML = '<div class="loading">No servers configured.</div>'; return; }
|
|
254
|
+
const groups = {};
|
|
255
|
+
servers.forEach(s => { const g = s.group || 'Default'; (groups[g] = groups[g] || []).push(s); });
|
|
256
|
+
let html = '';
|
|
257
|
+
Object.keys(groups).sort().forEach(g => {
|
|
258
|
+
html += '<div class="group-name">' + esc(g) + ' (' + groups[g].length + ')</div>';
|
|
259
|
+
groups[g].forEach(s => {
|
|
260
|
+
const open = terminals[s.name] ? ' style="border-color:var(--green)"' : '';
|
|
261
|
+
html += '<div class="server-card"' + open + ' onclick="openTerminal(\'' + esc(s.name) + '\')">';
|
|
262
|
+
html += '<div class="dot"></div>';
|
|
263
|
+
html += '<div class="info"><div class="name">' + esc(s.name) + '</div>';
|
|
264
|
+
html += '<div class="host">' + esc(s.username + '@' + s.host + ':' + s.port) + '</div></div>';
|
|
265
|
+
html += '<span class="arrow">›</span></div>';
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
list.innerHTML = html;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function filterServers() {
|
|
272
|
+
const q = document.getElementById('search-input').value.trim().toUpperCase();
|
|
273
|
+
if (!q) { renderServers(allServers); return; }
|
|
274
|
+
renderServers(allServers.filter(s => s.name.toUpperCase().includes(q) || s.host.includes(q) || (s.group||'').toUpperCase().includes(q)));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Multi-tab Terminals ──
|
|
278
|
+
|
|
279
|
+
function openTerminal(name) {
|
|
280
|
+
if (terminals[name]) {
|
|
281
|
+
// Already open — switch to it
|
|
282
|
+
switchToTerminal(name);
|
|
283
|
+
switchPanel('terminals');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Create terminal instance
|
|
288
|
+
const el = document.createElement('div');
|
|
289
|
+
el.className = 'term-instance';
|
|
290
|
+
el.id = 'term-' + name;
|
|
291
|
+
document.getElementById('term-container').appendChild(el);
|
|
292
|
+
|
|
293
|
+
const term = new Terminal({
|
|
294
|
+
cursorBlink: true, fontSize: 14,
|
|
295
|
+
fontFamily: "'Menlo','Consolas',monospace",
|
|
296
|
+
theme: { background:'#0a0e17', foreground:'#e2e8f0', cursor:'#38bdf8', selectionBackground:'#38bdf855',
|
|
297
|
+
black:'#1a2236', red:'#ef4444', green:'#22c55e', yellow:'#f59e0b', blue:'#38bdf8', magenta:'#f472b6', cyan:'#22d3ee', white:'#e2e8f0' },
|
|
298
|
+
allowProposedApi: true,
|
|
299
|
+
});
|
|
300
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
301
|
+
term.loadAddon(fitAddon);
|
|
302
|
+
term.loadAddon(new WebLinksAddon.WebLinksAddon());
|
|
303
|
+
term.open(el);
|
|
304
|
+
|
|
305
|
+
const entry = { ws: null, term, fitAddon, el, name };
|
|
306
|
+
terminals[name] = entry;
|
|
307
|
+
|
|
308
|
+
switchToTerminal(name);
|
|
309
|
+
switchPanel('terminals');
|
|
310
|
+
setTimeout(() => fitAddon.fit(), 100);
|
|
311
|
+
|
|
312
|
+
term.writeln('\x1b[36mConnecting to ' + name + '...\x1b[0m');
|
|
313
|
+
|
|
314
|
+
// WebSocket
|
|
315
|
+
const ws = new WebSocket(WS_URL);
|
|
316
|
+
ws.binaryType = 'arraybuffer';
|
|
317
|
+
entry.ws = ws;
|
|
318
|
+
|
|
319
|
+
ws.onopen = () => {
|
|
320
|
+
ws.send(JSON.stringify({ type:'auth', user:authUser, pass:authPass }));
|
|
321
|
+
};
|
|
322
|
+
ws.onmessage = (ev) => {
|
|
323
|
+
if (ev.data instanceof ArrayBuffer) { term.write(new Uint8Array(ev.data)); return; }
|
|
324
|
+
try {
|
|
325
|
+
const msg = JSON.parse(ev.data);
|
|
326
|
+
if (msg.type === 'auth_ok') {
|
|
327
|
+
const d = fitAddon ? { cols:term.cols, rows:term.rows } : { cols:120, rows:30 };
|
|
328
|
+
ws.send(JSON.stringify({ type:'connect', server:name, cols:d.cols, rows:d.rows }));
|
|
329
|
+
} else if (msg.type === 'connected') {
|
|
330
|
+
term.writeln('\x1b[32mConnected to ' + msg.server + ' (' + msg.host + ')\x1b[0m\r\n');
|
|
331
|
+
term.focus();
|
|
332
|
+
} else if (msg.type === 'error') {
|
|
333
|
+
term.writeln('\x1b[31mError: ' + msg.message + '\x1b[0m');
|
|
334
|
+
}
|
|
335
|
+
} catch (e) { term.write(ev.data); }
|
|
336
|
+
};
|
|
337
|
+
ws.onclose = () => { term.writeln('\r\n\x1b[33m--- Disconnected ---\x1b[0m'); };
|
|
338
|
+
ws.onerror = () => { term.writeln('\r\n\x1b[31m--- Connection error ---\x1b[0m'); };
|
|
339
|
+
|
|
340
|
+
term.onData(data => {
|
|
341
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type:'input', data }));
|
|
342
|
+
});
|
|
343
|
+
term.onResize(({ cols, rows }) => {
|
|
344
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type:'resize', cols, rows }));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
renderTermTabs();
|
|
348
|
+
renderServers(allServers);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function switchToTerminal(name) {
|
|
352
|
+
activeTerminal = name;
|
|
353
|
+
document.querySelectorAll('.term-instance').forEach(el => el.classList.toggle('active', el.id === 'term-' + name));
|
|
354
|
+
document.querySelectorAll('.term-tab').forEach(t => t.classList.toggle('active', t.dataset.name === name));
|
|
355
|
+
if (terminals[name]) {
|
|
356
|
+
setTimeout(() => { terminals[name].fitAddon.fit(); terminals[name].term.focus(); }, 50);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function closeTerminal(name) {
|
|
361
|
+
const t = terminals[name];
|
|
362
|
+
if (!t) return;
|
|
363
|
+
if (t.ws) t.ws.close();
|
|
364
|
+
t.term.dispose();
|
|
365
|
+
t.el.remove();
|
|
366
|
+
delete terminals[name];
|
|
367
|
+
if (activeTerminal === name) {
|
|
368
|
+
const keys = Object.keys(terminals);
|
|
369
|
+
activeTerminal = keys.length ? keys[keys.length - 1] : null;
|
|
370
|
+
if (activeTerminal) switchToTerminal(activeTerminal);
|
|
371
|
+
}
|
|
372
|
+
renderTermTabs();
|
|
373
|
+
renderServers(allServers);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function renderTermTabs() {
|
|
377
|
+
const bar = document.getElementById('term-tabs');
|
|
378
|
+
const names = Object.keys(terminals);
|
|
379
|
+
const noTerms = document.getElementById('no-terms');
|
|
380
|
+
if (!names.length) { bar.innerHTML = ''; bar.appendChild(noTerms); noTerms.style.display = ''; return; }
|
|
381
|
+
noTerms.style.display = 'none';
|
|
382
|
+
let html = '';
|
|
383
|
+
names.forEach(n => {
|
|
384
|
+
const cls = n === activeTerminal ? ' active' : '';
|
|
385
|
+
html += '<div class="term-tab' + cls + '" data-name="' + esc(n) + '" onclick="switchToTerminal(\'' + esc(n) + '\')">';
|
|
386
|
+
html += esc(n) + ' <span class="close" onclick="event.stopPropagation();closeTerminal(\'' + esc(n) + '\')">×</span></div>';
|
|
387
|
+
});
|
|
388
|
+
bar.innerHTML = html;
|
|
389
|
+
bar.appendChild(noTerms);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sendQuick(data) {
|
|
393
|
+
if (activeTerminal && terminals[activeTerminal]) {
|
|
394
|
+
const ws = terminals[activeTerminal].ws;
|
|
395
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
396
|
+
ws.send(JSON.stringify({ type:'input', data }));
|
|
397
|
+
terminals[activeTerminal].term.focus();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
window.addEventListener('resize', () => {
|
|
403
|
+
if (activeTerminal && terminals[activeTerminal]) terminals[activeTerminal].fitAddon.fit();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ── AI Chat ──
|
|
407
|
+
|
|
408
|
+
async function loadChatHistory() {
|
|
409
|
+
try {
|
|
410
|
+
const data = await apiPost('/chat/history', {});
|
|
411
|
+
const msgs = data.history || [];
|
|
412
|
+
msgs.forEach(m => appendChatMsg(m.role, m.content, m.timestamp));
|
|
413
|
+
} catch (e) {}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function appendChatMsg(role, text, time) {
|
|
417
|
+
const container = document.getElementById('chat-messages');
|
|
418
|
+
const div = document.createElement('div');
|
|
419
|
+
div.className = 'chat-msg ' + role;
|
|
420
|
+
div.innerHTML = '<div class="chat-bubble">' + esc(text) + '</div>' + (time ? '<div class="chat-time">' + esc(time) + '</div>' : '');
|
|
421
|
+
container.appendChild(div);
|
|
422
|
+
container.scrollTop = container.scrollHeight;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function sendChat() {
|
|
426
|
+
const input = document.getElementById('chat-input');
|
|
427
|
+
const msg = input.value.trim();
|
|
428
|
+
if (!msg) return;
|
|
429
|
+
input.value = '';
|
|
430
|
+
|
|
431
|
+
appendChatMsg('user', msg, new Date().toLocaleTimeString());
|
|
432
|
+
document.getElementById('chat-typing').classList.add('active');
|
|
433
|
+
document.getElementById('chat-send').disabled = true;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const data = await apiPost('/chat', { message: msg });
|
|
437
|
+
const reply = data.reply || data.error || 'No response';
|
|
438
|
+
appendChatMsg('assistant', reply);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
appendChatMsg('assistant', 'Connection error: ' + e.message);
|
|
441
|
+
} finally {
|
|
442
|
+
document.getElementById('chat-typing').classList.remove('active');
|
|
443
|
+
document.getElementById('chat-send').disabled = false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
document.getElementById('chat-input').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } });
|
|
448
|
+
|
|
449
|
+
// ── Helpers ──
|
|
450
|
+
|
|
451
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
452
|
+
</script>
|
|
453
|
+
</body>
|
|
454
|
+
</html>
|