mcp-for-i 0.1.13 → 0.3.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/README.md +27 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +52 -11
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +178 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +147 -100
- package/dist/controlplane/ui.js +1783 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +334 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +93 -0
- package/dist/ibmi/tn5250/ebcdicTables.js +27 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +1033 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +79 -24
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +800 -0
- package/dist/mcp/modules/tn5250App.js +194 -0
- package/dist/mcp/modules/tn5250Font.js +8 -0
- package/dist/mcp/modules/tn5250Live.js +221 -0
- package/dist/mcp/modules/tn5250LivePage.js +480 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1719 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +264 -220
- package/docs/configuration.md +193 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +367 -188
- package/package.json +5 -2
package/dist/controlplane/ui.js
CHANGED
|
@@ -1,923 +1,1786 @@
|
|
|
1
|
-
export function renderControlPlaneHtml() {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--accent: #
|
|
36
|
-
--
|
|
37
|
-
--ok:
|
|
38
|
-
--warn: #
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
--
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
.
|
|
126
|
-
.
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
border:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
.
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
1
|
+
export function renderControlPlaneHtml(token = "") {
|
|
2
|
+
// The token is hex, but escape it anyway since it lands inside an attribute.
|
|
3
|
+
const tokenAttr = escapeAttribute(token);
|
|
4
|
+
return `<!doctype html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<meta name="mcp-for-i-token" content="${tokenAttr}" />
|
|
10
|
+
<title>MCP for i · Control Plane</title>
|
|
11
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%232563eb'/%3E%3Cpath d='M16 9.5v1.2M16 14v8.5' stroke='white' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E" />
|
|
12
|
+
<script>
|
|
13
|
+
(function () {
|
|
14
|
+
var t = null;
|
|
15
|
+
try { t = localStorage.getItem("mcp_for_i_theme"); } catch (e) {}
|
|
16
|
+
if (t !== "light" && t !== "dark") t = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
17
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
18
|
+
})();
|
|
19
|
+
</script>
|
|
20
|
+
<style>
|
|
21
|
+
:root {
|
|
22
|
+
color-scheme: light;
|
|
23
|
+
--bg: #f5f6f8;
|
|
24
|
+
--surface: #ffffff;
|
|
25
|
+
--surface-2: #f9fafb;
|
|
26
|
+
--hover: #f1f3f6;
|
|
27
|
+
--line: rgba(15, 23, 42, 0.09);
|
|
28
|
+
--line-strong: rgba(15, 23, 42, 0.16);
|
|
29
|
+
--text: #0f172a;
|
|
30
|
+
--text-2: #475569;
|
|
31
|
+
--text-3: #64748b;
|
|
32
|
+
--accent: #2563eb;
|
|
33
|
+
--accent-hover: #1d4ed8;
|
|
34
|
+
--accent-soft: rgba(37, 99, 235, 0.09);
|
|
35
|
+
--on-accent: #ffffff;
|
|
36
|
+
--ok: #15803d;
|
|
37
|
+
--ok-soft: rgba(22, 163, 74, 0.1);
|
|
38
|
+
--warn: #b45309;
|
|
39
|
+
--warn-soft: rgba(217, 119, 6, 0.11);
|
|
40
|
+
--danger: #dc2626;
|
|
41
|
+
--danger-hover: #b91c1c;
|
|
42
|
+
--danger-soft: rgba(220, 38, 38, 0.09);
|
|
43
|
+
--shadow-float: 0 1px 2px rgba(15, 23, 42, 0.06), 0 8px 24px rgba(15, 23, 42, 0.1), 0 24px 48px rgba(15, 23, 42, 0.08);
|
|
44
|
+
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
45
|
+
--ring: 0 0 0 3px rgba(37, 99, 235, 0.22);
|
|
46
|
+
--backdrop: rgba(15, 23, 42, 0.38);
|
|
47
|
+
--r-sm: 6px;
|
|
48
|
+
--r-md: 8px;
|
|
49
|
+
--r-lg: 10px;
|
|
50
|
+
--r-xl: 13px;
|
|
51
|
+
--font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
52
|
+
--mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Consolas, monospace;
|
|
53
|
+
}
|
|
54
|
+
:root[data-theme="dark"] {
|
|
55
|
+
color-scheme: dark;
|
|
56
|
+
--bg: #0b0d12;
|
|
57
|
+
--surface: #12151c;
|
|
58
|
+
--surface-2: #161a22;
|
|
59
|
+
--hover: #1c212b;
|
|
60
|
+
--line: rgba(255, 255, 255, 0.08);
|
|
61
|
+
--line-strong: rgba(255, 255, 255, 0.15);
|
|
62
|
+
--text: #e7eaf0;
|
|
63
|
+
--text-2: #a6afbd;
|
|
64
|
+
--text-3: #7c8696;
|
|
65
|
+
--accent: #4f8cff;
|
|
66
|
+
--accent-hover: #6b9fff;
|
|
67
|
+
--accent-soft: rgba(79, 140, 255, 0.13);
|
|
68
|
+
--on-accent: #ffffff;
|
|
69
|
+
--ok: #3fb950;
|
|
70
|
+
--ok-soft: rgba(63, 185, 80, 0.13);
|
|
71
|
+
--warn: #d29922;
|
|
72
|
+
--warn-soft: rgba(210, 153, 34, 0.14);
|
|
73
|
+
--danger: #f85149;
|
|
74
|
+
--danger-hover: #ff6a62;
|
|
75
|
+
--danger-soft: rgba(248, 81, 73, 0.13);
|
|
76
|
+
--shadow-float: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.45);
|
|
77
|
+
--shadow-card: none;
|
|
78
|
+
--ring: 0 0 0 3px rgba(79, 140, 255, 0.3);
|
|
79
|
+
--backdrop: rgba(0, 0, 0, 0.6);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
* { box-sizing: border-box; }
|
|
83
|
+
[hidden] { display: none !important; }
|
|
84
|
+
html, body { height: 100%; }
|
|
85
|
+
body {
|
|
86
|
+
margin: 0;
|
|
87
|
+
font: 400 13.5px/1.5 var(--font);
|
|
88
|
+
color: var(--text);
|
|
89
|
+
background: var(--bg);
|
|
90
|
+
-webkit-font-smoothing: antialiased;
|
|
91
|
+
}
|
|
92
|
+
svg { flex: none; }
|
|
93
|
+
.icon { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
|
|
94
|
+
.mono { font-family: var(--mono); }
|
|
95
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
96
|
+
|
|
97
|
+
/* Shell */
|
|
98
|
+
.shell { display: grid; grid-template-columns: 232px minmax(0, 1fr); min-height: 100%; }
|
|
99
|
+
.sidebar {
|
|
100
|
+
position: sticky; top: 0; height: 100vh;
|
|
101
|
+
display: flex; flex-direction: column; gap: 24px;
|
|
102
|
+
padding: 16px 12px;
|
|
103
|
+
border-right: 1px solid var(--line);
|
|
104
|
+
background: var(--surface);
|
|
105
|
+
}
|
|
106
|
+
.brand { display: flex; align-items: center; gap: 10px; padding: 4px 8px; }
|
|
107
|
+
.brand-mark {
|
|
108
|
+
width: 28px; height: 28px; border-radius: var(--r-md);
|
|
109
|
+
display: grid; place-items: center;
|
|
110
|
+
background: var(--accent); color: var(--on-accent);
|
|
111
|
+
}
|
|
112
|
+
.brand-name { font-weight: 600; letter-spacing: -0.01em; }
|
|
113
|
+
.brand-sub { font: 500 10.5px/1.2 var(--mono); color: var(--text-3); letter-spacing: 0.08em; text-transform: uppercase; }
|
|
114
|
+
.nav { display: flex; flex-direction: column; gap: 2px; }
|
|
115
|
+
.nav-label { font: 500 10.5px/1 var(--mono); color: var(--text-3); letter-spacing: 0.1em; text-transform: uppercase; padding: 0 8px 8px; }
|
|
116
|
+
.nav-item {
|
|
117
|
+
display: flex; align-items: center; gap: 10px;
|
|
118
|
+
width: 100%; padding: 7px 8px;
|
|
119
|
+
border: 0; border-radius: var(--r-md);
|
|
120
|
+
background: transparent; color: var(--text-2);
|
|
121
|
+
font: 500 13.5px/1.4 var(--font); text-align: left;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
}
|
|
124
|
+
.nav-item:hover { background: var(--hover); color: var(--text); }
|
|
125
|
+
.nav-item:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
126
|
+
.nav-item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }
|
|
127
|
+
.nav-item .count {
|
|
128
|
+
margin-left: auto; min-width: 20px; padding: 0 6px;
|
|
129
|
+
border-radius: 999px; background: var(--hover); color: var(--text-3);
|
|
130
|
+
font: 500 11px/18px var(--mono); text-align: center;
|
|
131
|
+
}
|
|
132
|
+
.nav-item[aria-current="page"] .count { background: transparent; color: inherit; }
|
|
133
|
+
.sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 8px; }
|
|
134
|
+
.service {
|
|
135
|
+
display: flex; align-items: center; gap: 10px;
|
|
136
|
+
padding: 10px; border: 1px solid var(--line); border-radius: var(--r-lg);
|
|
137
|
+
background: var(--surface-2);
|
|
138
|
+
}
|
|
139
|
+
.service-text { min-width: 0; }
|
|
140
|
+
.service-title { font-weight: 500; font-size: 12.5px; }
|
|
141
|
+
.service-detail { color: var(--text-3); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
142
|
+
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--text-3); flex: none; }
|
|
143
|
+
.service[data-state="online"] .pulse { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
144
|
+
.service[data-state="reconnecting"] .pulse { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); animation: blink 1.2s ease-in-out infinite; }
|
|
145
|
+
.service[data-state="offline"] .pulse { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-soft); }
|
|
146
|
+
@keyframes blink { 50% { opacity: 0.35; } }
|
|
147
|
+
|
|
148
|
+
/* Main */
|
|
149
|
+
.main { min-width: 0; padding: 32px 40px 64px; }
|
|
150
|
+
.page { display: none; max-width: 1080px; margin: 0 auto; }
|
|
151
|
+
.page.active { display: block; animation: fade 0.18s ease-out; }
|
|
152
|
+
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
|
153
|
+
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
|
|
154
|
+
.page-head h1 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.25; }
|
|
155
|
+
.page-head p { margin: 4px 0 0; color: var(--text-2); max-width: 64ch; }
|
|
156
|
+
.head-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
157
|
+
.stack { display: grid; gap: 16px; }
|
|
158
|
+
|
|
159
|
+
/* Cards */
|
|
160
|
+
.card {
|
|
161
|
+
background: var(--surface);
|
|
162
|
+
border: 1px solid var(--line);
|
|
163
|
+
border-radius: var(--r-xl);
|
|
164
|
+
box-shadow: var(--shadow-card);
|
|
165
|
+
}
|
|
166
|
+
.card-head {
|
|
167
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
168
|
+
padding: 14px 16px; border-bottom: 1px solid var(--line);
|
|
169
|
+
}
|
|
170
|
+
.card-head h2 { margin: 0; font-size: 14px; font-weight: 600; letter-spacing: -0.01em; }
|
|
171
|
+
.card-head .sub { color: var(--text-3); font-size: 12px; margin-top: 2px; }
|
|
172
|
+
.card-body { padding: 16px; }
|
|
173
|
+
.eyebrow { font: 500 10.5px/1.2 var(--mono); color: var(--text-3); letter-spacing: 0.1em; text-transform: uppercase; }
|
|
174
|
+
|
|
175
|
+
.stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
|
|
176
|
+
.stat { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
177
|
+
.stat-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
178
|
+
.stat-value { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; word-break: break-word; }
|
|
179
|
+
.stat-meta { color: var(--text-3); font-size: 12px; min-height: 18px; word-break: break-word; }
|
|
180
|
+
|
|
181
|
+
/* Chips */
|
|
182
|
+
.chip {
|
|
183
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
184
|
+
padding: 2px 8px; border-radius: 999px;
|
|
185
|
+
font: 500 11.5px/18px var(--font); white-space: nowrap;
|
|
186
|
+
background: var(--hover); color: var(--text-2);
|
|
187
|
+
}
|
|
188
|
+
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.85; }
|
|
189
|
+
.chip.plain::before { display: none; }
|
|
190
|
+
.chip.ok { background: var(--ok-soft); color: var(--ok); }
|
|
191
|
+
.chip.warn { background: var(--warn-soft); color: var(--warn); }
|
|
192
|
+
.chip.danger { background: var(--danger-soft); color: var(--danger); }
|
|
193
|
+
.chip.info { background: var(--accent-soft); color: var(--accent); }
|
|
194
|
+
.chip.running::before { animation: blink 1s ease-in-out infinite; }
|
|
195
|
+
.tag {
|
|
196
|
+
display: inline-block; padding: 1px 6px; margin: 1px 4px 1px 0;
|
|
197
|
+
border: 1px solid var(--line); border-radius: var(--r-sm);
|
|
198
|
+
font: 500 11.5px/18px var(--mono); color: var(--text-2); background: var(--surface-2);
|
|
199
|
+
}
|
|
200
|
+
.tag.strong { color: var(--text); border-color: var(--line-strong); }
|
|
201
|
+
|
|
202
|
+
/* Banner */
|
|
203
|
+
.banner {
|
|
204
|
+
display: flex; gap: 12px; align-items: flex-start;
|
|
205
|
+
padding: 12px 16px; border-radius: var(--r-xl);
|
|
206
|
+
border: 1px solid var(--line); background: var(--surface);
|
|
207
|
+
}
|
|
208
|
+
.banner .b-icon { width: 28px; height: 28px; border-radius: var(--r-md); display: grid; place-items: center; background: var(--hover); color: var(--text-3); flex: none; }
|
|
209
|
+
.banner.ok .b-icon { background: var(--ok-soft); color: var(--ok); }
|
|
210
|
+
.banner.warn .b-icon { background: var(--warn-soft); color: var(--warn); }
|
|
211
|
+
.banner.danger .b-icon { background: var(--danger-soft); color: var(--danger); }
|
|
212
|
+
.banner-title { font-weight: 500; }
|
|
213
|
+
.banner-text { color: var(--text-2); font-size: 12.5px; }
|
|
214
|
+
|
|
215
|
+
/* Buttons */
|
|
216
|
+
.btn {
|
|
217
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
218
|
+
height: 32px; padding: 0 12px;
|
|
219
|
+
border: 1px solid transparent; border-radius: var(--r-md);
|
|
220
|
+
font: 500 13px/1 var(--font); white-space: nowrap;
|
|
221
|
+
cursor: pointer; transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
222
|
+
}
|
|
223
|
+
.btn:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
224
|
+
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
225
|
+
.btn-primary { background: var(--accent); color: var(--on-accent); }
|
|
226
|
+
.btn-primary:not([disabled]):hover { background: var(--accent-hover); }
|
|
227
|
+
.btn-secondary { background: var(--surface); color: var(--text); border-color: var(--line-strong); box-shadow: var(--shadow-card); }
|
|
228
|
+
.btn-secondary:not([disabled]):hover { background: var(--hover); }
|
|
229
|
+
.btn-ghost { background: transparent; color: var(--text-2); }
|
|
230
|
+
.btn-ghost:not([disabled]):hover { background: var(--hover); color: var(--text); }
|
|
231
|
+
.btn-danger { background: var(--danger); color: #fff; }
|
|
232
|
+
.btn-danger:not([disabled]):hover { background: var(--danger-hover); }
|
|
233
|
+
.btn-icon { width: 30px; height: 30px; padding: 0; }
|
|
234
|
+
.btn-icon.danger-hover:not([disabled]):hover { color: var(--danger); background: var(--danger-soft); }
|
|
235
|
+
.btn.loading { position: relative; color: transparent !important; pointer-events: none; }
|
|
236
|
+
.btn.loading::after {
|
|
237
|
+
content: ""; position: absolute; width: 14px; height: 14px; border-radius: 50%;
|
|
238
|
+
border: 2px solid currentColor; border-right-color: transparent;
|
|
239
|
+
animation: spin 0.7s linear infinite;
|
|
240
|
+
}
|
|
241
|
+
.btn-primary.loading::after, .btn-danger.loading::after { border-color: #fff; border-right-color: transparent; }
|
|
242
|
+
.btn-secondary.loading::after, .btn-ghost.loading::after { border-color: var(--text-2); border-right-color: transparent; }
|
|
243
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
244
|
+
|
|
245
|
+
/* Tables */
|
|
246
|
+
.table-wrap { overflow-x: auto; }
|
|
247
|
+
table { width: 100%; border-collapse: collapse; }
|
|
248
|
+
th {
|
|
249
|
+
padding: 8px 16px; text-align: left; white-space: nowrap;
|
|
250
|
+
font: 500 10.5px/1.4 var(--mono); color: var(--text-3); letter-spacing: 0.08em; text-transform: uppercase;
|
|
251
|
+
border-bottom: 1px solid var(--line); background: var(--surface-2);
|
|
252
|
+
}
|
|
253
|
+
td { padding: 12px 16px; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
|
254
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
255
|
+
tbody tr:hover td { background: var(--surface-2); }
|
|
256
|
+
.cell-primary { font-weight: 500; }
|
|
257
|
+
.cell-sub { color: var(--text-3); font-size: 12px; }
|
|
258
|
+
.row-actions { display: flex; justify-content: flex-end; gap: 2px; }
|
|
259
|
+
th:last-child.right, td.right { text-align: right; }
|
|
260
|
+
|
|
261
|
+
/* Empty states */
|
|
262
|
+
.empty { padding: 48px 24px; text-align: center; color: var(--text-2); }
|
|
263
|
+
.empty-icon { width: 40px; height: 40px; margin: 0 auto 12px; border-radius: var(--r-lg); display: grid; place-items: center; background: var(--hover); color: var(--text-3); }
|
|
264
|
+
.empty h3 { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--text); }
|
|
265
|
+
.empty p { margin: 0 auto 16px; max-width: 44ch; font-size: 12.5px; }
|
|
266
|
+
.empty.compact { padding: 32px 24px; }
|
|
267
|
+
.empty.compact p { margin-bottom: 0; }
|
|
268
|
+
|
|
269
|
+
/* Lists */
|
|
270
|
+
.list-row { display: flex; align-items: center; gap: 16px; padding: 16px; border-bottom: 1px solid var(--line); }
|
|
271
|
+
.list-row:last-child { border-bottom: 0; }
|
|
272
|
+
.list-main { flex: 1; min-width: 0; }
|
|
273
|
+
.list-title { font-weight: 500; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
274
|
+
.list-sub { color: var(--text-3); font-size: 12.5px; margin-top: 2px; word-break: break-word; }
|
|
275
|
+
.list-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
|
276
|
+
|
|
277
|
+
/* Forms */
|
|
278
|
+
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
|
279
|
+
.form-grid .full { grid-column: 1 / -1; }
|
|
280
|
+
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
281
|
+
.field label { font-weight: 500; font-size: 12.5px; color: var(--text); }
|
|
282
|
+
.field label .opt { color: var(--text-3); font-weight: 400; }
|
|
283
|
+
.help { color: var(--text-3); font-size: 12px; }
|
|
284
|
+
.err { color: var(--danger); font-size: 12px; display: none; }
|
|
285
|
+
.field.invalid .err { display: block; }
|
|
286
|
+
.field.invalid input { border-color: var(--danger); }
|
|
287
|
+
input, select, textarea {
|
|
288
|
+
width: 100%; height: 34px; padding: 0 10px;
|
|
289
|
+
border: 1px solid var(--line-strong); border-radius: var(--r-md);
|
|
290
|
+
background: var(--surface); color: var(--text);
|
|
291
|
+
font: inherit; outline: none;
|
|
292
|
+
transition: border-color 0.12s, box-shadow 0.12s;
|
|
293
|
+
}
|
|
294
|
+
textarea { height: auto; min-height: 72px; padding: 8px 10px; resize: vertical; font-family: var(--mono); font-size: 12.5px; }
|
|
295
|
+
input::placeholder, textarea::placeholder { color: var(--text-3); opacity: 0.8; }
|
|
296
|
+
input:hover, select:hover, textarea:hover { border-color: var(--text-3); }
|
|
297
|
+
input:focus, select:focus, textarea:focus { border-color: var(--accent); box-shadow: var(--ring); }
|
|
298
|
+
input:disabled { background: var(--surface-2); color: var(--text-3); cursor: not-allowed; }
|
|
299
|
+
.input-suffix { position: relative; }
|
|
300
|
+
.input-suffix input { padding-right: 56px; }
|
|
301
|
+
.input-suffix span { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); color: var(--text-3); font-size: 12px; pointer-events: none; }
|
|
302
|
+
.section-title { margin: 0 0 12px; display: flex; align-items: center; gap: 8px; }
|
|
303
|
+
.form-section + .form-section { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--line); }
|
|
304
|
+
.segmented { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
|
305
|
+
.segmented label { position: relative; display: block; }
|
|
306
|
+
.segmented input { position: absolute; top: 0; left: 0; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
|
307
|
+
.seg-opt {
|
|
308
|
+
display: block; padding: 10px 12px; cursor: pointer;
|
|
309
|
+
border: 1px solid var(--line-strong); border-radius: var(--r-lg); background: var(--surface);
|
|
310
|
+
transition: border-color 0.12s, background 0.12s;
|
|
311
|
+
}
|
|
312
|
+
.seg-opt:hover { background: var(--hover); }
|
|
313
|
+
.seg-opt .t { font-weight: 500; display: flex; align-items: center; gap: 6px; }
|
|
314
|
+
.seg-opt .d { color: var(--text-3); font-size: 12px; margin-top: 2px; }
|
|
315
|
+
.segmented input:checked + .seg-opt { border-color: var(--accent); background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
316
|
+
.segmented input:focus-visible + .seg-opt { box-shadow: var(--ring); }
|
|
317
|
+
details.disclosure > summary {
|
|
318
|
+
list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px;
|
|
319
|
+
color: var(--text-2); font-weight: 500; font-size: 12.5px; padding: 4px 0;
|
|
320
|
+
}
|
|
321
|
+
details.disclosure > summary::-webkit-details-marker { display: none; }
|
|
322
|
+
details.disclosure > summary:hover { color: var(--text); }
|
|
323
|
+
details.disclosure > summary .chev { transition: transform 0.15s; }
|
|
324
|
+
details.disclosure[open] > summary .chev { transform: rotate(90deg); }
|
|
325
|
+
details.disclosure > .disclosure-body { margin-top: 12px; }
|
|
326
|
+
.form-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--line); background: var(--surface-2); border-radius: 0 0 var(--r-xl) var(--r-xl); }
|
|
327
|
+
.dirty-hint { color: var(--text-3); font-size: 12px; }
|
|
328
|
+
|
|
329
|
+
/* Logs */
|
|
330
|
+
.log-list { display: flex; flex-direction: column; }
|
|
331
|
+
.log-item { padding: 12px 16px; border-bottom: 1px solid var(--line); }
|
|
332
|
+
.log-item:last-child { border-bottom: 0; }
|
|
333
|
+
.log-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
334
|
+
.log-name { font-weight: 500; }
|
|
335
|
+
.log-time { color: var(--text-3); font-size: 12px; margin-left: auto; }
|
|
336
|
+
pre.console {
|
|
337
|
+
margin: 8px 0 0; padding: 10px 12px; max-height: 260px; overflow: auto;
|
|
338
|
+
border-radius: var(--r-md); border: 1px solid var(--line); background: var(--surface-2);
|
|
339
|
+
font: 12px/1.55 var(--mono); color: var(--text-2); white-space: pre-wrap; word-break: break-word;
|
|
340
|
+
}
|
|
341
|
+
pre.console .e { color: var(--danger); }
|
|
342
|
+
|
|
343
|
+
/* Clients */
|
|
344
|
+
.client-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
|
345
|
+
.client { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
346
|
+
.client-top { display: flex; align-items: flex-start; gap: 12px; }
|
|
347
|
+
.client-logo { width: 32px; height: 32px; border-radius: var(--r-md); display: grid; place-items: center; background: var(--hover); color: var(--text-2); font: 600 12px/1 var(--mono); flex: none; }
|
|
348
|
+
.client-name { font-weight: 600; letter-spacing: -0.01em; }
|
|
349
|
+
.client-desc { color: var(--text-2); font-size: 12.5px; flex: 1; }
|
|
350
|
+
.client-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
351
|
+
|
|
352
|
+
/* Dialog */
|
|
353
|
+
dialog {
|
|
354
|
+
width: min(720px, calc(100vw - 32px)); max-height: calc(100vh - 48px);
|
|
355
|
+
padding: 0; border: 1px solid var(--line); border-radius: var(--r-xl);
|
|
356
|
+
background: var(--surface); color: var(--text); box-shadow: var(--shadow-float);
|
|
357
|
+
}
|
|
358
|
+
dialog[open] { display: flex; overflow: hidden; flex-direction: column; animation: pop 0.16s ease-out; }
|
|
359
|
+
dialog::backdrop { background: var(--backdrop); backdrop-filter: blur(2px); }
|
|
360
|
+
@keyframes pop { from { opacity: 0; transform: translateY(6px) scale(0.985); } to { opacity: 1; transform: none; } }
|
|
361
|
+
dialog form { display: flex; flex-direction: column; min-height: 0; margin: 0; }
|
|
362
|
+
.dialog-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--line); }
|
|
363
|
+
.dialog-head h2 { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; }
|
|
364
|
+
.dialog-head p { margin: 2px 0 0; color: var(--text-3); font-size: 12.5px; }
|
|
365
|
+
.dialog-body { padding: 20px; overflow-y: auto; }
|
|
366
|
+
.dialog-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--line); background: var(--surface-2); }
|
|
367
|
+
.dialog-error { display: none; margin-bottom: 16px; padding: 10px 12px; border-radius: var(--r-md); background: var(--danger-soft); color: var(--danger); font-size: 12.5px; word-break: break-word; }
|
|
368
|
+
.dialog-error.show { display: block; }
|
|
369
|
+
.test-result { display: none; margin-bottom: 16px; padding: 10px 12px; border-radius: var(--r-md); border: 1px solid var(--line); background: var(--surface-2); font-size: 12.5px; }
|
|
370
|
+
.test-result.show { display: block; }
|
|
371
|
+
.test-result .tr-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
|
|
372
|
+
.test-result.ok .tr-title { color: var(--ok); }
|
|
373
|
+
.test-result.warn .tr-title { color: var(--warn); }
|
|
374
|
+
.test-result.danger .tr-title { color: var(--danger); }
|
|
375
|
+
.test-result .tr-msg { margin-top: 4px; color: var(--text-2); word-break: break-word; }
|
|
376
|
+
.test-result ul { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 3px; }
|
|
377
|
+
.test-result li { display: flex; gap: 8px; color: var(--text-3); }
|
|
378
|
+
.test-result li .mark { flex: none; width: 14px; text-align: center; }
|
|
379
|
+
.test-result li.ok .mark { color: var(--ok); }
|
|
380
|
+
.test-result li.fail .mark { color: var(--danger); }
|
|
381
|
+
.test-result li .detail { word-break: break-all; }
|
|
382
|
+
.dialog-foot .spacer { flex: 1; }
|
|
383
|
+
#confirmDialog { width: min(420px, calc(100vw - 32px)); }
|
|
384
|
+
#confirmDialog .dialog-body { display: flex; gap: 12px; }
|
|
385
|
+
.confirm-icon { width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; background: var(--danger-soft); color: var(--danger); flex: none; }
|
|
386
|
+
|
|
387
|
+
/* Toasts */
|
|
388
|
+
.toasts { position: fixed; right: 24px; bottom: 24px; z-index: 50; display: flex; flex-direction: column; gap: 8px; width: min(360px, calc(100vw - 32px)); }
|
|
389
|
+
.toast {
|
|
390
|
+
display: flex; gap: 10px; align-items: flex-start;
|
|
391
|
+
padding: 12px 14px; border-radius: var(--r-lg);
|
|
392
|
+
background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-float);
|
|
393
|
+
animation: slide 0.2s ease-out;
|
|
394
|
+
}
|
|
395
|
+
.toast.leaving { opacity: 0; transform: translateY(6px); transition: opacity 0.2s, transform 0.2s; }
|
|
396
|
+
.toast .t-icon { color: var(--text-3); margin-top: 1px; }
|
|
397
|
+
.toast.ok .t-icon { color: var(--ok); }
|
|
398
|
+
.toast.error .t-icon { color: var(--danger); }
|
|
399
|
+
.toast-title { font-weight: 500; }
|
|
400
|
+
.toast-text { color: var(--text-2); font-size: 12.5px; word-break: break-word; }
|
|
401
|
+
@keyframes slide { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
402
|
+
|
|
403
|
+
.skeleton { display: inline-block; height: 1em; width: 8em; border-radius: var(--r-sm); background: linear-gradient(90deg, var(--hover), var(--surface-2), var(--hover)); background-size: 200% 100%; animation: shimmer 1.2s linear infinite; vertical-align: middle; }
|
|
404
|
+
@keyframes shimmer { to { background-position: -200% 0; } }
|
|
405
|
+
.offline-overlay .requires-online { pointer-events: none; }
|
|
406
|
+
|
|
407
|
+
@media (max-width: 1000px) {
|
|
408
|
+
.stats { grid-template-columns: 1fr; }
|
|
409
|
+
}
|
|
410
|
+
@media (max-width: 860px) {
|
|
411
|
+
.shell { grid-template-columns: minmax(0, 1fr); }
|
|
412
|
+
.sidebar {
|
|
413
|
+
position: sticky; top: 0; z-index: 10; height: auto; min-width: 0;
|
|
414
|
+
flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 10px 16px 0;
|
|
415
|
+
border-right: 0; border-bottom: 1px solid var(--line);
|
|
416
|
+
}
|
|
417
|
+
.brand { padding: 0; }
|
|
418
|
+
.brand-name { white-space: nowrap; }
|
|
419
|
+
.brand-sub, .nav-label, .service-detail, #themeLabel { display: none; }
|
|
420
|
+
.sidebar-foot { order: 2; margin: 0 0 0 auto; flex-direction: row; align-items: center; gap: 4px; }
|
|
421
|
+
.service { padding: 4px 10px; border-radius: 999px; }
|
|
422
|
+
#themeBtn { width: 32px; padding: 0; }
|
|
423
|
+
.nav { order: 3; flex: 1 0 100%; flex-direction: row; gap: 4px; overflow-x: auto; margin: 0 -16px; padding: 0 16px 8px; scrollbar-width: none; }
|
|
424
|
+
.nav-item { width: auto; white-space: nowrap; }
|
|
425
|
+
.nav-item .count { display: none; }
|
|
426
|
+
.main { padding: 24px 16px 48px; }
|
|
427
|
+
.form-grid, .segmented { grid-template-columns: 1fr; }
|
|
428
|
+
.list-row { flex-direction: column; align-items: stretch; }
|
|
429
|
+
.list-actions { justify-content: flex-start; }
|
|
430
|
+
.toasts { right: 16px; bottom: 16px; }
|
|
431
|
+
}
|
|
432
|
+
@media (prefers-reduced-motion: reduce) {
|
|
433
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
434
|
+
}
|
|
435
|
+
</style>
|
|
436
|
+
</head>
|
|
437
|
+
<body>
|
|
438
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
439
|
+
<defs>
|
|
440
|
+
<symbol id="i-overview" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></symbol>
|
|
441
|
+
<symbol id="i-server" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/></symbol>
|
|
442
|
+
<symbol id="i-runtime" viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4v5h-5"/></symbol>
|
|
443
|
+
<symbol id="i-plug" viewBox="0 0 24 24"><path d="M9 2v6M15 2v6"/><path d="M6 8h12v4a6 6 0 0 1-12 0z"/><path d="M12 18v4"/></symbol>
|
|
444
|
+
<symbol id="i-settings" viewBox="0 0 24 24"><path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/></symbol>
|
|
445
|
+
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
|
446
|
+
<symbol id="i-edit" viewBox="0 0 24 24"><path d="M4 20h4L19 9l-4-4L4 16z"/><path d="M13.5 6.5l4 4"/></symbol>
|
|
447
|
+
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16M10 11v6M14 11v6"/><path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"/><path d="M9 7V4h6v3"/></symbol>
|
|
448
|
+
<symbol id="i-x" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></symbol>
|
|
449
|
+
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></symbol>
|
|
450
|
+
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></symbol>
|
|
451
|
+
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></symbol>
|
|
452
|
+
<symbol id="i-alert" viewBox="0 0 24 24"><path d="M12 3l10 18H2z"/><path d="M12 10v4M12 17.5h.01"/></symbol>
|
|
453
|
+
<symbol id="i-check" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M8 12l3 3 5-6"/></symbol>
|
|
454
|
+
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></symbol>
|
|
455
|
+
<symbol id="i-chev" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></symbol>
|
|
456
|
+
<symbol id="i-pulse" viewBox="0 0 24 24"><path d="M3 12h4l3-8 4 16 3-8h4"/></symbol>
|
|
457
|
+
<symbol id="i-terminal" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M13 15h4"/></symbol>
|
|
458
|
+
</defs>
|
|
459
|
+
</svg>
|
|
460
|
+
|
|
461
|
+
<div class="shell">
|
|
462
|
+
<aside class="sidebar">
|
|
463
|
+
<div class="brand">
|
|
464
|
+
<div class="brand-mark" aria-hidden="true">
|
|
465
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round"><path d="M12 4v.5M12 10v10"/></svg>
|
|
466
|
+
</div>
|
|
467
|
+
<div>
|
|
468
|
+
<div class="brand-name">MCP for i</div>
|
|
469
|
+
<div class="brand-sub">Control plane</div>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<nav class="nav" aria-label="Sections">
|
|
474
|
+
<div class="nav-label">Workspace</div>
|
|
475
|
+
<button class="nav-item" type="button" data-tab="overview"><svg class="icon"><use href="#i-overview"/></svg>Overview</button>
|
|
476
|
+
<button class="nav-item" type="button" data-tab="connections"><svg class="icon"><use href="#i-server"/></svg>Connections<span class="count" id="navConnCount">0</span></button>
|
|
477
|
+
<button class="nav-item" type="button" data-tab="runtime"><svg class="icon"><use href="#i-runtime"/></svg>Runtime</button>
|
|
478
|
+
<button class="nav-item" type="button" data-tab="clients"><svg class="icon"><use href="#i-plug"/></svg>Clients</button>
|
|
479
|
+
<button class="nav-item" type="button" data-tab="settings"><svg class="icon"><use href="#i-settings"/></svg>Settings</button>
|
|
480
|
+
</nav>
|
|
481
|
+
|
|
482
|
+
<div class="sidebar-foot">
|
|
483
|
+
<div class="service" id="servicePill" data-state="offline" role="status" aria-live="polite">
|
|
484
|
+
<span class="pulse"></span>
|
|
485
|
+
<div class="service-text">
|
|
486
|
+
<div class="service-title" id="serviceTitle">Connecting</div>
|
|
487
|
+
<div class="service-detail" id="serviceText">Checking local service...</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
<button id="themeBtn" class="btn btn-ghost" type="button" aria-label="Toggle theme">
|
|
491
|
+
<svg class="icon" id="themeIcon"><use href="#i-moon"/></svg><span id="themeLabel">Dark mode</span>
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
</aside>
|
|
495
|
+
|
|
496
|
+
<main class="main">
|
|
497
|
+
<!-- Overview -->
|
|
498
|
+
<section class="page" id="panel-overview" aria-labelledby="h-overview">
|
|
499
|
+
<header class="page-head">
|
|
500
|
+
<div>
|
|
501
|
+
<h1 id="h-overview">Overview</h1>
|
|
502
|
+
<p id="runtimeSummary">Health of your local MCP server, skills and IBM i sessions.</p>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="head-actions">
|
|
505
|
+
<button id="refreshOverviewBtn" class="btn btn-secondary requires-online" type="button"><svg class="icon"><use href="#i-runtime"/></svg>Refresh</button>
|
|
506
|
+
</div>
|
|
507
|
+
</header>
|
|
508
|
+
|
|
509
|
+
<div class="stack">
|
|
510
|
+
<div class="stats">
|
|
511
|
+
<div class="card stat">
|
|
512
|
+
<div class="stat-top"><span class="eyebrow">MCP server</span><span class="chip" id="mcpVersionBadge">Checking</span></div>
|
|
513
|
+
<div class="stat-value mono" id="mcpVersionText"><span class="skeleton"></span></div>
|
|
514
|
+
<div class="stat-meta" id="mcpVersionMeta"> </div>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="card stat">
|
|
517
|
+
<div class="stat-top"><span class="eyebrow">Skills</span><span class="chip" id="skillsVersionBadge">Checking</span></div>
|
|
518
|
+
<div class="stat-value mono" id="skillsVersionText"><span class="skeleton"></span></div>
|
|
519
|
+
<div class="stat-meta" id="skillsVersionMeta"> </div>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="card stat">
|
|
522
|
+
<div class="stat-top"><span class="eyebrow">Background service</span><span class="chip" id="startupBadge">Checking</span></div>
|
|
523
|
+
<div class="stat-value" id="startupText"><span class="skeleton"></span></div>
|
|
524
|
+
<div class="stat-meta" id="startupMeta"> </div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<div class="banner" id="credentialNotice">
|
|
529
|
+
<div class="b-icon"><svg class="icon"><use href="#i-shield"/></svg></div>
|
|
530
|
+
<div>
|
|
531
|
+
<div class="banner-title" id="credentialTitle">Credential store</div>
|
|
532
|
+
<div class="banner-text" id="credentialText">Checking...</div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div class="card">
|
|
537
|
+
<div class="card-head">
|
|
538
|
+
<div>
|
|
539
|
+
<h2>Active sessions</h2>
|
|
540
|
+
<div class="sub" id="sessionsUpdated">Warm IBM i sessions held by the MCP server.</div>
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="table-wrap">
|
|
544
|
+
<table>
|
|
545
|
+
<thead><tr><th>Connection</th><th>State</th><th>Policy</th><th>Idle remaining</th></tr></thead>
|
|
546
|
+
<tbody id="sessionsBody"></tbody>
|
|
547
|
+
</table>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="card">
|
|
551
|
+
<div class="card-head">
|
|
552
|
+
<div>
|
|
553
|
+
<h2>Live 5250 screens</h2>
|
|
554
|
+
<div class="sub">Green screens your coding assistant is driving. Open one to watch, type and click alongside it.</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="table-wrap">
|
|
558
|
+
<table>
|
|
559
|
+
<thead><tr><th>Connection</th><th>Host</th><th>State</th><th>Last input</th><th></th></tr></thead>
|
|
560
|
+
<tbody id="liveBody"></tbody>
|
|
561
|
+
</table>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
<div class="help" id="versionCheckedAt"></div>
|
|
565
|
+
</div>
|
|
566
|
+
</section>
|
|
567
|
+
|
|
568
|
+
<!-- Connections -->
|
|
569
|
+
<section class="page" id="panel-connections" aria-labelledby="h-connections">
|
|
570
|
+
<header class="page-head">
|
|
571
|
+
<div>
|
|
572
|
+
<h1 id="h-connections">Connections</h1>
|
|
573
|
+
<p>IBM i systems the MCP server can reach. Passwords are stored in your OS keychain, never in config files.</p>
|
|
574
|
+
</div>
|
|
575
|
+
<div class="head-actions">
|
|
576
|
+
<button id="addConnectionBtn" class="btn btn-primary requires-online" type="button"><svg class="icon"><use href="#i-plus"/></svg>Add connection</button>
|
|
577
|
+
</div>
|
|
578
|
+
</header>
|
|
579
|
+
<div class="card">
|
|
580
|
+
<div class="card-head">
|
|
581
|
+
<div><h2>Saved connections</h2><div class="sub" id="connectionCount">0 connections</div></div>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="table-wrap" id="connectionsTableWrap">
|
|
584
|
+
<table>
|
|
585
|
+
<thead><tr><th>Name</th><th>Host</th><th>User</th><th>Status</th><th>Policy</th><th>Library context</th><th class="right"><span class="sr-only">Actions</span></th></tr></thead>
|
|
586
|
+
<tbody id="connectionsBody"></tbody>
|
|
587
|
+
</table>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="empty" id="connectionsEmpty" hidden>
|
|
590
|
+
<div class="empty-icon"><svg class="icon"><use href="#i-server"/></svg></div>
|
|
591
|
+
<h3>No connections yet</h3>
|
|
592
|
+
<p>Add an IBM i system to let your coding assistant query libraries, run SQL and manage source.</p>
|
|
593
|
+
<button class="btn btn-primary requires-online" type="button" id="emptyAddConnectionBtn"><svg class="icon"><use href="#i-plus"/></svg>Add connection</button>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
</section>
|
|
597
|
+
|
|
598
|
+
<!-- Runtime -->
|
|
599
|
+
<section class="page" id="panel-runtime" aria-labelledby="h-runtime">
|
|
600
|
+
<header class="page-head">
|
|
601
|
+
<div>
|
|
602
|
+
<h1 id="h-runtime">Runtime</h1>
|
|
603
|
+
<p>Keep the MCP server and skills current, and control whether the control plane starts with your machine.</p>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="head-actions">
|
|
606
|
+
<button id="refreshRuntimeBtn" class="btn btn-secondary requires-online" type="button"><svg class="icon"><use href="#i-runtime"/></svg>Refresh</button>
|
|
607
|
+
</div>
|
|
608
|
+
</header>
|
|
609
|
+
<div class="stack">
|
|
610
|
+
<div class="card">
|
|
611
|
+
<div class="card-head"><div><h2>Updates</h2><div class="sub">Updating the MCP server restarts the control plane. This page reconnects on its own.</div></div></div>
|
|
612
|
+
<div class="list-row">
|
|
613
|
+
<div class="list-main">
|
|
614
|
+
<div class="list-title">MCP server <span class="chip" id="rtMcpBadge">Checking</span></div>
|
|
615
|
+
<div class="list-sub mono" id="rtMcpText">Checking...</div>
|
|
616
|
+
</div>
|
|
617
|
+
<div class="list-actions">
|
|
618
|
+
<button id="updateMcpBtn" class="btn btn-primary requires-online" type="button">Update MCP</button>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="list-row" style="align-items: flex-start;">
|
|
622
|
+
<div class="list-main">
|
|
623
|
+
<div class="list-title">Skills <span class="chip" id="rtSkillsBadge">Checking</span></div>
|
|
624
|
+
<div class="list-sub mono" id="rtSkillsText">Checking...</div>
|
|
625
|
+
<details class="disclosure" style="margin-top: 8px;">
|
|
626
|
+
<summary><svg class="icon chev" style="width:14px;height:14px"><use href="#i-chev"/></svg>Skills source</summary>
|
|
627
|
+
<div class="disclosure-body form-grid">
|
|
628
|
+
<div class="field">
|
|
629
|
+
<label for="skillsRepoUrl">Repository URL</label>
|
|
630
|
+
<input id="skillsRepoUrl" class="mono" placeholder="https://github.com/org/mcp-for-i-skills.git" spellcheck="false" />
|
|
631
|
+
</div>
|
|
632
|
+
<div class="field">
|
|
633
|
+
<label for="skillsBranch">Branch</label>
|
|
634
|
+
<input id="skillsBranch" class="mono" placeholder="main" spellcheck="false" />
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
</details>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="list-actions">
|
|
640
|
+
<button id="updateSkillsBtn" class="btn btn-secondary requires-online" type="button">Update skills</button>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<div class="card">
|
|
646
|
+
<div class="card-head"><div><h2>Background service</h2><div class="sub">Start the control plane automatically when you sign in.</div></div><span class="chip" id="rtServiceBadge">Checking</span></div>
|
|
647
|
+
<div class="list-row">
|
|
648
|
+
<div class="list-main">
|
|
649
|
+
<div class="list-title" id="rtServiceTitle">Checking...</div>
|
|
650
|
+
<div class="list-sub" id="autostartInfo">Background service: checking...</div>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="list-actions">
|
|
653
|
+
<button id="enableServiceBtn" class="btn btn-secondary requires-online" type="button">Enable</button>
|
|
654
|
+
<button id="disableServiceBtn" class="btn btn-secondary requires-online" type="button">Disable</button>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div class="card">
|
|
660
|
+
<div class="card-head"><div><h2>Activity</h2><div class="sub">Output from update and service jobs.</div></div></div>
|
|
661
|
+
<div class="log-list" id="logs"></div>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</section>
|
|
665
|
+
|
|
666
|
+
<!-- Clients -->
|
|
667
|
+
<section class="page" id="panel-clients" aria-labelledby="h-clients">
|
|
668
|
+
<header class="page-head">
|
|
669
|
+
<div>
|
|
670
|
+
<h1 id="h-clients">Clients</h1>
|
|
671
|
+
<p id="clientSummary">Register MCP for i with the coding assistants installed on this machine.</p>
|
|
672
|
+
</div>
|
|
673
|
+
<div class="head-actions">
|
|
674
|
+
<button id="refreshClientsBtn" class="btn btn-secondary requires-online" type="button"><svg class="icon"><use href="#i-runtime"/></svg>Refresh</button>
|
|
675
|
+
</div>
|
|
676
|
+
</header>
|
|
677
|
+
<div class="stack">
|
|
678
|
+
<div class="client-grid" id="clientButtons"></div>
|
|
679
|
+
<div class="card" id="clientLogsCard" hidden>
|
|
680
|
+
<div class="card-head"><div><h2 id="clientLogsTitle">Result</h2></div><button class="btn btn-ghost btn-icon" type="button" id="clientLogsClose" aria-label="Dismiss"><svg class="icon"><use href="#i-x"/></svg></button></div>
|
|
681
|
+
<div class="card-body"><pre class="console" id="clientLogs" style="margin:0"></pre></div>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
</section>
|
|
685
|
+
|
|
686
|
+
<!-- Settings -->
|
|
687
|
+
<section class="page" id="panel-settings" aria-labelledby="h-settings">
|
|
688
|
+
<header class="page-head">
|
|
689
|
+
<div>
|
|
690
|
+
<h1 id="h-settings">Settings</h1>
|
|
691
|
+
<p>Session defaults apply to every connection unless a connection sets its own override.</p>
|
|
692
|
+
</div>
|
|
693
|
+
</header>
|
|
694
|
+
<form class="card" id="settingsForm" novalidate>
|
|
695
|
+
<div class="card-head"><div><h2>Session lifecycle</h2><div class="sub">How long sessions stay warm and how they recover from drops.</div></div></div>
|
|
696
|
+
<div class="card-body">
|
|
697
|
+
<div class="form-grid">
|
|
698
|
+
<div class="field">
|
|
699
|
+
<label for="sessionIdleMinutes">Idle timeout</label>
|
|
700
|
+
<div class="input-suffix"><input id="sessionIdleMinutes" type="number" min="1" max="1440" required /><span>minutes</span></div>
|
|
701
|
+
<div class="help">Close a session after this long without activity. 1 to 1440.</div>
|
|
702
|
+
<div class="err">Enter a whole number from 1 to 1440.</div>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="field">
|
|
705
|
+
<label for="sessionPingSeconds">Health check interval</label>
|
|
706
|
+
<div class="input-suffix"><input id="sessionPingSeconds" type="number" min="5" max="300" required /><span>seconds</span></div>
|
|
707
|
+
<div class="help">How often an open session is pinged. 5 to 300.</div>
|
|
708
|
+
<div class="err">Enter a whole number from 5 to 300.</div>
|
|
709
|
+
</div>
|
|
710
|
+
<div class="field">
|
|
711
|
+
<label for="sessionReconnectAttempts">Reconnect attempts</label>
|
|
712
|
+
<div class="input-suffix"><input id="sessionReconnectAttempts" type="number" min="1" max="5" required /><span>tries</span></div>
|
|
713
|
+
<div class="help">Retries before a dropped session is reported as failed. 1 to 5.</div>
|
|
714
|
+
<div class="err">Enter a whole number from 1 to 5.</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
<div class="form-foot">
|
|
719
|
+
<span class="dirty-hint" id="settingsDirty">No unsaved changes</span>
|
|
720
|
+
<div class="head-actions">
|
|
721
|
+
<button id="reloadSettingsBtn" class="btn btn-ghost requires-online" type="button">Discard</button>
|
|
722
|
+
<button id="saveSettingsBtn" class="btn btn-primary requires-online" type="submit">Save changes</button>
|
|
723
|
+
</div>
|
|
724
|
+
</div>
|
|
725
|
+
</form>
|
|
726
|
+
</section>
|
|
727
|
+
</main>
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<!-- Connection dialog -->
|
|
731
|
+
<dialog id="connectionDialog" aria-labelledby="connectionDialogTitle">
|
|
732
|
+
<form id="connectionForm" method="dialog" novalidate>
|
|
733
|
+
<div class="dialog-head">
|
|
734
|
+
<div>
|
|
735
|
+
<h2 id="connectionDialogTitle">Add connection</h2>
|
|
736
|
+
<p id="connectionDialogSub">Connect over SSH to an IBM i system.</p>
|
|
737
|
+
</div>
|
|
738
|
+
<button id="closeConnectionDialogBtn" class="btn btn-ghost btn-icon" type="button" aria-label="Close"><svg class="icon"><use href="#i-x"/></svg></button>
|
|
739
|
+
</div>
|
|
740
|
+
<div class="dialog-body">
|
|
741
|
+
<div class="dialog-error" id="connectionError" role="alert"></div>
|
|
742
|
+
<div class="test-result" id="connectionTestResult" role="status" aria-live="polite"></div>
|
|
743
|
+
|
|
744
|
+
<div class="form-section">
|
|
745
|
+
<div class="section-title eyebrow">Server</div>
|
|
746
|
+
<div class="form-grid">
|
|
747
|
+
<div class="field">
|
|
748
|
+
<label for="connName">Name</label>
|
|
749
|
+
<input id="connName" required autocomplete="off" placeholder="DEV400" />
|
|
750
|
+
<div class="err">A name is required.</div>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="field">
|
|
753
|
+
<label for="connUser">Username</label>
|
|
754
|
+
<input id="connUser" required autocomplete="off" spellcheck="false" />
|
|
755
|
+
<div class="err">A username is required.</div>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="field">
|
|
758
|
+
<label for="connHost">Host</label>
|
|
759
|
+
<input id="connHost" required autocomplete="off" spellcheck="false" placeholder="ibmi.example.com" />
|
|
760
|
+
<div class="err">A host is required.</div>
|
|
761
|
+
</div>
|
|
762
|
+
<div class="field">
|
|
763
|
+
<label for="connPort">SSH port</label>
|
|
764
|
+
<input id="connPort" type="number" min="1" max="65535" value="22" />
|
|
765
|
+
<div class="err">Port must be between 1 and 65535.</div>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<div class="form-section">
|
|
771
|
+
<div class="section-title eyebrow">Authentication</div>
|
|
772
|
+
<div class="form-grid">
|
|
773
|
+
<div class="field">
|
|
774
|
+
<label for="connPassword">Password <span class="opt">(optional)</span></label>
|
|
775
|
+
<input id="connPassword" type="password" autocomplete="new-password" placeholder="Stored in OS keychain" />
|
|
776
|
+
<div class="help" id="connPasswordHelp">Saved to the OS keychain only.</div>
|
|
777
|
+
</div>
|
|
778
|
+
<div class="field">
|
|
779
|
+
<label for="connKeyPath">Private key path <span class="opt">(optional)</span></label>
|
|
780
|
+
<input id="connKeyPath" class="mono" spellcheck="false" placeholder="~/.ssh/id_ed25519" />
|
|
781
|
+
<div class="help">Used instead of a password when set.</div>
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
|
|
786
|
+
<div class="form-section">
|
|
787
|
+
<div class="section-title eyebrow">Access policy</div>
|
|
788
|
+
<div class="segmented" role="radiogroup" aria-label="Policy">
|
|
789
|
+
<label><input type="radio" name="connPolicy" value="read-only" /><span class="seg-opt"><span class="t">Read-only</span><span class="d">Blocks every change to the system.</span></span></label>
|
|
790
|
+
<label><input type="radio" name="connPolicy" value="guarded" /><span class="seg-opt"><span class="t">Guarded</span><span class="d">Risky operations need explicit approval.</span></span></label>
|
|
791
|
+
<label><input type="radio" name="connPolicy" value="power-user" /><span class="seg-opt"><span class="t">Power user</span><span class="d">No approval prompts. Use with care.</span></span></label>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
<div class="form-section">
|
|
796
|
+
<div class="section-title eyebrow">Library context</div>
|
|
797
|
+
<div class="form-grid">
|
|
798
|
+
<div class="field">
|
|
799
|
+
<label for="connCurrentLibrary">Current library <span class="opt">(optional)</span></label>
|
|
800
|
+
<input id="connCurrentLibrary" class="mono" spellcheck="false" placeholder="QGPL" style="text-transform: uppercase;" />
|
|
801
|
+
</div>
|
|
802
|
+
<div class="field full">
|
|
803
|
+
<label for="connLibraryList">Library list <span class="opt">(optional)</span></label>
|
|
804
|
+
<textarea id="connLibraryList" spellcheck="false" placeholder="MYLIB, QGPL, QTEMP"></textarea>
|
|
805
|
+
<div class="help">Separate with commas, spaces or new lines.</div>
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<div class="form-section">
|
|
811
|
+
<details class="disclosure" id="overridesDisclosure">
|
|
812
|
+
<summary><svg class="icon chev" style="width:14px;height:14px"><use href="#i-chev"/></svg>Session overrides</summary>
|
|
813
|
+
<div class="disclosure-body">
|
|
814
|
+
<div class="help" style="margin-bottom: 12px;">Leave blank to use the defaults from Settings.</div>
|
|
815
|
+
<div class="form-grid">
|
|
816
|
+
<div class="field">
|
|
817
|
+
<label for="connSessionIdleMinutes">Idle timeout</label>
|
|
818
|
+
<div class="input-suffix"><input id="connSessionIdleMinutes" type="number" min="1" max="1440" /><span>minutes</span></div>
|
|
819
|
+
</div>
|
|
820
|
+
<div class="field">
|
|
821
|
+
<label for="connSessionPingSeconds">Health check interval</label>
|
|
822
|
+
<div class="input-suffix"><input id="connSessionPingSeconds" type="number" min="5" max="300" /><span>seconds</span></div>
|
|
823
|
+
</div>
|
|
824
|
+
<div class="field">
|
|
825
|
+
<label for="connSessionReconnectAttempts">Reconnect attempts</label>
|
|
826
|
+
<div class="input-suffix"><input id="connSessionReconnectAttempts" type="number" min="1" max="5" /><span>tries</span></div>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
</details>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
<div class="dialog-foot">
|
|
834
|
+
<button id="testConnectionBtn" class="btn btn-secondary" type="button" title="Sign on once and sign off, without saving">Test connection</button>
|
|
835
|
+
<span class="spacer"></span>
|
|
836
|
+
<button id="cancelConnectionBtn" class="btn btn-secondary" type="button">Cancel</button>
|
|
837
|
+
<button id="saveConnectionBtn" class="btn btn-primary" type="submit">Save connection</button>
|
|
282
838
|
</div>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
function
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
function
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
$("
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
for (const
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
"<
|
|
582
|
-
"<
|
|
583
|
-
"<
|
|
584
|
-
"
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
function
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
$("
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
$("
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
$("
|
|
724
|
-
$("
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
839
|
+
</form>
|
|
840
|
+
</dialog>
|
|
841
|
+
|
|
842
|
+
<!-- Confirm dialog -->
|
|
843
|
+
<dialog id="confirmDialog" aria-labelledby="confirmTitle">
|
|
844
|
+
<div class="dialog-body">
|
|
845
|
+
<div class="confirm-icon"><svg class="icon"><use href="#i-alert"/></svg></div>
|
|
846
|
+
<div>
|
|
847
|
+
<h2 id="confirmTitle" style="margin:0 0 4px;font-size:15px;font-weight:600">Are you sure?</h2>
|
|
848
|
+
<div id="confirmText" style="color:var(--text-2);font-size:13px"></div>
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
851
|
+
<div class="dialog-foot">
|
|
852
|
+
<button class="btn btn-secondary" type="button" id="confirmCancel">Cancel</button>
|
|
853
|
+
<button class="btn btn-danger" type="button" id="confirmOk">Delete</button>
|
|
854
|
+
</div>
|
|
855
|
+
</dialog>
|
|
856
|
+
|
|
857
|
+
<div class="toasts" id="toasts" aria-live="polite" aria-atomic="false"></div>
|
|
858
|
+
|
|
859
|
+
<script>
|
|
860
|
+
const THEME_KEY = "mcp_for_i_theme";
|
|
861
|
+
const TAB_KEY = "mcp_for_i_tab";
|
|
862
|
+
const SKILLS_REPO_KEY = "mcp_for_i_skills_repo";
|
|
863
|
+
const SKILLS_BRANCH_KEY = "mcp_for_i_skills_branch";
|
|
864
|
+
const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas2202/mcp-for-i-skills.git";
|
|
865
|
+
const DEFAULT_SKILLS_BRANCH = "main";
|
|
866
|
+
const LEGACY_SKILLS_REPO = "https://github.com/ashishthomas-pcr/mcp-for-i-skills.git";
|
|
867
|
+
const TABS = ["overview", "connections", "runtime", "clients", "settings"];
|
|
868
|
+
const JOB_LABELS = {
|
|
869
|
+
updateMcp: "MCP update",
|
|
870
|
+
updateSkills: "Skills update",
|
|
871
|
+
enableService: "Enable background service",
|
|
872
|
+
disableService: "Disable background service"
|
|
873
|
+
};
|
|
874
|
+
const POLICY_CHIP = { "read-only": ["ok", "Read-only"], "guarded": ["info", "Guarded"], "power-user": ["warn", "Power user"] };
|
|
875
|
+
const state = { online: false, busy: false, jobs: {}, connections: [], editingName: "", service: null, settings: null, testing: {} };
|
|
876
|
+
const $ = id => document.getElementById(id);
|
|
877
|
+
const TOKEN_HEADER = "X-MCP-For-I-Token";
|
|
878
|
+
const API_TOKEN = (document.querySelector("meta[name='mcp-for-i-token']") || {}).content || "";
|
|
879
|
+
const NL = String.fromCharCode(10);
|
|
880
|
+
|
|
881
|
+
function escapeHtml(s) {
|
|
882
|
+
return String(s ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
883
|
+
}
|
|
884
|
+
function icon(name, size) {
|
|
885
|
+
const s = size ? (" style='width:" + size + "px;height:" + size + "px'") : "";
|
|
886
|
+
return "<svg class='icon'" + s + "><use href='#i-" + name + "'/></svg>";
|
|
887
|
+
}
|
|
888
|
+
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
889
|
+
function errorMessage(err) {
|
|
890
|
+
const raw = err && err.message ? err.message : String(err);
|
|
891
|
+
try { const parsed = JSON.parse(raw); if (parsed && parsed.error) return parsed.error; } catch {}
|
|
892
|
+
return raw;
|
|
893
|
+
}
|
|
894
|
+
function formatTime(value) {
|
|
895
|
+
if (!value) return "";
|
|
896
|
+
const d = new Date(value);
|
|
897
|
+
if (Number.isNaN(d.getTime())) return String(value);
|
|
898
|
+
return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
899
|
+
}
|
|
900
|
+
function relativeTime(value) {
|
|
901
|
+
const t = Date.parse(value || "");
|
|
902
|
+
if (!t) return "";
|
|
903
|
+
const diff = Math.round((Date.now() - t) / 1000);
|
|
904
|
+
if (diff < 5) return "just now";
|
|
905
|
+
if (diff < 60) return diff + "s ago";
|
|
906
|
+
if (diff < 3600) return Math.floor(diff / 60) + "m ago";
|
|
907
|
+
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
|
908
|
+
return formatTime(value);
|
|
909
|
+
}
|
|
910
|
+
function formatDuration(seconds) {
|
|
911
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return "-";
|
|
912
|
+
if (seconds < 60) return seconds + "s";
|
|
913
|
+
const m = Math.floor(seconds / 60), s = seconds % 60;
|
|
914
|
+
return m + "m " + String(s).padStart(2, "0") + "s";
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/* Toasts */
|
|
918
|
+
function toast(kind, title, text) {
|
|
919
|
+
const host = $("toasts");
|
|
920
|
+
const el = document.createElement("div");
|
|
921
|
+
el.className = "toast " + kind;
|
|
922
|
+
el.setAttribute("role", kind === "error" ? "alert" : "status");
|
|
923
|
+
const ic = kind === "ok" ? "check" : kind === "error" ? "alert" : "info";
|
|
924
|
+
el.innerHTML = "<span class='t-icon'>" + icon(ic) + "</span><div><div class='toast-title'>" + escapeHtml(title) + "</div>" +
|
|
925
|
+
(text ? "<div class='toast-text'>" + escapeHtml(text) + "</div>" : "") + "</div>";
|
|
926
|
+
host.appendChild(el);
|
|
927
|
+
const ttl = kind === "error" ? 7000 : 3500;
|
|
928
|
+
setTimeout(() => { el.classList.add("leaving"); setTimeout(() => el.remove(), 220); }, ttl);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/* Confirm */
|
|
932
|
+
function confirmAction(title, text, okLabel) {
|
|
933
|
+
return new Promise(resolve => {
|
|
934
|
+
const dlg = $("confirmDialog");
|
|
935
|
+
$("confirmTitle").textContent = title;
|
|
936
|
+
$("confirmText").textContent = text;
|
|
937
|
+
$("confirmOk").textContent = okLabel || "Confirm";
|
|
938
|
+
const done = value => { cleanup(); if (dlg.open) dlg.close(); resolve(value); };
|
|
939
|
+
const onOk = () => done(true);
|
|
940
|
+
const onCancel = () => done(false);
|
|
941
|
+
const onClose = () => { cleanup(); resolve(false); };
|
|
942
|
+
function cleanup() {
|
|
943
|
+
$("confirmOk").removeEventListener("click", onOk);
|
|
944
|
+
$("confirmCancel").removeEventListener("click", onCancel);
|
|
945
|
+
dlg.removeEventListener("close", onClose);
|
|
946
|
+
}
|
|
947
|
+
$("confirmOk").addEventListener("click", onOk);
|
|
948
|
+
$("confirmCancel").addEventListener("click", onCancel);
|
|
949
|
+
dlg.addEventListener("close", onClose);
|
|
950
|
+
dlg.showModal();
|
|
951
|
+
$("confirmCancel").focus();
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* Busy / online */
|
|
956
|
+
function syncControls() {
|
|
957
|
+
const disabled = !state.online || state.busy;
|
|
958
|
+
document.querySelectorAll(".requires-online").forEach(el => { el.disabled = disabled; });
|
|
959
|
+
document.querySelectorAll("#clientButtons button, #connectionsBody button").forEach(el => { el.disabled = disabled; });
|
|
960
|
+
syncServiceButtons();
|
|
961
|
+
}
|
|
962
|
+
function setBusy(busy) { state.busy = busy; syncControls(); }
|
|
963
|
+
async function withLoading(btn, fn) {
|
|
964
|
+
if (btn) btn.classList.add("loading");
|
|
965
|
+
try { return await fn(); }
|
|
966
|
+
finally { if (btn) btn.classList.remove("loading"); }
|
|
967
|
+
}
|
|
968
|
+
function setServiceState(mode, title, text) {
|
|
969
|
+
const pill = $("servicePill");
|
|
970
|
+
pill.dataset.state = mode;
|
|
971
|
+
$("serviceTitle").textContent = title;
|
|
972
|
+
$("serviceText").textContent = text;
|
|
973
|
+
$("serviceText").title = text;
|
|
974
|
+
state.online = mode === "online";
|
|
975
|
+
syncControls();
|
|
976
|
+
}
|
|
977
|
+
async function api(path, options = {}) {
|
|
978
|
+
const headers = { "Content-Type": "application/json", ...(options.headers || {}) };
|
|
979
|
+
headers[TOKEN_HEADER] = API_TOKEN;
|
|
980
|
+
const res = await fetch(path, { ...options, headers });
|
|
981
|
+
if (!res.ok) {
|
|
982
|
+
const text = await res.text();
|
|
983
|
+
throw new Error(text || ("HTTP " + res.status));
|
|
984
|
+
}
|
|
985
|
+
if (res.status === 204) return null;
|
|
986
|
+
return res.json();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/* Theme */
|
|
990
|
+
function currentTheme() { return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light"; }
|
|
991
|
+
function applyTheme(theme, persist) {
|
|
992
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
993
|
+
$("themeIcon").innerHTML = "<use href='#i-" + (theme === "dark" ? "sun" : "moon") + "'/>";
|
|
994
|
+
$("themeLabel").textContent = theme === "dark" ? "Light mode" : "Dark mode";
|
|
995
|
+
if (persist) { try { localStorage.setItem(THEME_KEY, theme); } catch {} }
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/* Tabs */
|
|
999
|
+
function activateTab(name, focus) {
|
|
1000
|
+
if (!TABS.includes(name)) name = "overview";
|
|
1001
|
+
document.querySelectorAll(".nav-item").forEach(btn => {
|
|
1002
|
+
if (btn.dataset.tab === name) btn.setAttribute("aria-current", "page"); else btn.removeAttribute("aria-current");
|
|
1003
|
+
});
|
|
1004
|
+
document.querySelectorAll(".page").forEach(panel => panel.classList.toggle("active", panel.id === "panel-" + name));
|
|
1005
|
+
try { localStorage.setItem(TAB_KEY, name); } catch {}
|
|
1006
|
+
if (location.hash !== "#" + name) history.replaceState(null, "", "#" + name);
|
|
1007
|
+
if (focus) { const h = document.querySelector("#panel-" + name + " h1"); if (h) { h.setAttribute("tabindex", "-1"); h.focus({ preventScroll: true }); } }
|
|
1008
|
+
}
|
|
1009
|
+
function initialTab() {
|
|
1010
|
+
const fromHash = location.hash.replace("#", "");
|
|
1011
|
+
if (TABS.includes(fromHash)) return fromHash;
|
|
1012
|
+
try { const t = localStorage.getItem(TAB_KEY); if (t && TABS.includes(t)) return t; } catch {}
|
|
1013
|
+
return "overview";
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function setChip(id, text, kind) {
|
|
1017
|
+
const el = $(id);
|
|
1018
|
+
if (!el) return;
|
|
1019
|
+
el.textContent = text;
|
|
1020
|
+
el.className = "chip" + (kind ? " " + kind : "");
|
|
1021
|
+
}
|
|
1022
|
+
function normalizeLibraries(value) {
|
|
1023
|
+
const list = String(value || "").split(/[,;]|\\s+/).map(v => v.trim().toUpperCase()).filter(Boolean);
|
|
1024
|
+
return Array.from(new Set(list));
|
|
1025
|
+
}
|
|
1026
|
+
function parseOptionalNumber(value, min, max) {
|
|
1027
|
+
const raw = String(value || "").trim();
|
|
1028
|
+
if (!raw) return undefined;
|
|
1029
|
+
const n = Number(raw);
|
|
1030
|
+
if (!Number.isFinite(n)) return undefined;
|
|
1031
|
+
const normalized = Math.floor(n);
|
|
1032
|
+
if (normalized < min || normalized > max) return undefined;
|
|
1033
|
+
return normalized;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/* Skills source */
|
|
1037
|
+
function loadSkillsUpdateSettings() {
|
|
1038
|
+
let repo = DEFAULT_SKILLS_REPO;
|
|
1039
|
+
let branch = DEFAULT_SKILLS_BRANCH;
|
|
1040
|
+
try {
|
|
1041
|
+
const r = localStorage.getItem(SKILLS_REPO_KEY);
|
|
1042
|
+
const b = localStorage.getItem(SKILLS_BRANCH_KEY);
|
|
1043
|
+
if (r && r.trim()) repo = r.trim();
|
|
1044
|
+
if (b && b.trim()) branch = b.trim();
|
|
1045
|
+
} catch {}
|
|
1046
|
+
if (repo === LEGACY_SKILLS_REPO) repo = DEFAULT_SKILLS_REPO;
|
|
1047
|
+
$("skillsRepoUrl").value = repo;
|
|
1048
|
+
$("skillsBranch").value = branch;
|
|
1049
|
+
persistSkillsUpdateSettings();
|
|
1050
|
+
}
|
|
1051
|
+
function persistSkillsUpdateSettings() {
|
|
1052
|
+
const repo = ($("skillsRepoUrl").value || "").trim() || DEFAULT_SKILLS_REPO;
|
|
1053
|
+
const branch = ($("skillsBranch").value || "").trim() || DEFAULT_SKILLS_BRANCH;
|
|
1054
|
+
const normalizedRepo = repo === LEGACY_SKILLS_REPO ? DEFAULT_SKILLS_REPO : repo;
|
|
1055
|
+
$("skillsRepoUrl").value = normalizedRepo;
|
|
1056
|
+
$("skillsBranch").value = branch;
|
|
1057
|
+
try {
|
|
1058
|
+
localStorage.setItem(SKILLS_REPO_KEY, normalizedRepo);
|
|
1059
|
+
localStorage.setItem(SKILLS_BRANCH_KEY, branch);
|
|
1060
|
+
} catch {}
|
|
1061
|
+
return { repoUrl: normalizedRepo, branch };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/* Renderers */
|
|
1065
|
+
function jobLabel(id) { return JOB_LABELS[id] || id; }
|
|
1066
|
+
function renderRuntimeSummary() {
|
|
1067
|
+
const entries = Object.entries(state.jobs || {}).filter(([, job]) => job.status !== "idle");
|
|
1068
|
+
const running = entries.find(([, job]) => job.status === "running");
|
|
1069
|
+
const el = $("runtimeSummary");
|
|
1070
|
+
if (running) { el.textContent = jobLabel(running[0]) + " in progress..."; return; }
|
|
1071
|
+
if (entries.length === 0) { el.textContent = "Health of your local MCP server, skills and IBM i sessions."; return; }
|
|
1072
|
+
const latest = entries.map(([id, job]) => ({ id, at: job.finishedAt || job.startedAt || "", status: job.status }))
|
|
1073
|
+
.sort((a, b) => (Date.parse(b.at) || 0) - (Date.parse(a.at) || 0))[0];
|
|
1074
|
+
el.textContent = "Last action: " + jobLabel(latest.id) + " " + (latest.status === "success" ? "succeeded" : latest.status) + (latest.at ? " " + relativeTime(latest.at) : "") + ".";
|
|
1075
|
+
}
|
|
1076
|
+
function renderLogs(jobs) {
|
|
1077
|
+
state.jobs = jobs || {};
|
|
1078
|
+
const host = $("logs");
|
|
1079
|
+
const entries = Object.entries(state.jobs).filter(([, j]) => j.status !== "idle")
|
|
1080
|
+
.sort((a, b) => (Date.parse(b[1].startedAt || "") || 0) - (Date.parse(a[1].startedAt || "") || 0));
|
|
1081
|
+
if (entries.length === 0) {
|
|
1082
|
+
host.innerHTML = "<div class='empty compact'><div class='empty-icon'>" + icon("terminal") + "</div><h3>No activity yet</h3><p>Job output appears here when you run an update or change the background service.</p></div>";
|
|
1083
|
+
} else {
|
|
1084
|
+
host.innerHTML = entries.map(([id, j]) => {
|
|
1085
|
+
const chip = j.status === "running" ? "<span class='chip info running'>Running</span>"
|
|
1086
|
+
: j.status === "success" ? "<span class='chip ok'>Succeeded</span>"
|
|
1087
|
+
: j.status === "failed" ? "<span class='chip danger'>Failed</span>"
|
|
1088
|
+
: "<span class='chip'>" + escapeHtml(j.status) + "</span>";
|
|
1089
|
+
const lines = (j.output || []).map(escapeHtml);
|
|
1090
|
+
if (j.error) lines.push("<span class='e'>" + escapeHtml(j.error) + "</span>");
|
|
1091
|
+
const when = j.finishedAt ? ("Finished " + formatTime(j.finishedAt)) : (j.startedAt ? ("Started " + formatTime(j.startedAt)) : "");
|
|
1092
|
+
return "<div class='log-item'><div class='log-head'><span class='log-name'>" + escapeHtml(jobLabel(id)) + "</span>" + chip +
|
|
1093
|
+
"<span class='log-time'>" + escapeHtml(when) + "</span></div>" +
|
|
1094
|
+
(lines.length ? "<pre class='console'>" + lines.join(NL) + "</pre>" : "") + "</div>";
|
|
1095
|
+
}).join("");
|
|
1096
|
+
}
|
|
1097
|
+
renderRuntimeSummary();
|
|
1098
|
+
}
|
|
1099
|
+
function renderConnections(list) {
|
|
1100
|
+
state.connections = list || [];
|
|
1101
|
+
const n = state.connections.length;
|
|
1102
|
+
$("connectionCount").textContent = n + (n === 1 ? " connection" : " connections");
|
|
1103
|
+
$("navConnCount").textContent = String(n);
|
|
1104
|
+
$("connectionsTableWrap").hidden = n === 0;
|
|
1105
|
+
$("connectionsEmpty").hidden = n !== 0;
|
|
1106
|
+
const body = $("connectionsBody");
|
|
1107
|
+
body.innerHTML = "";
|
|
1108
|
+
for (const conn of state.connections) {
|
|
1109
|
+
const policy = (conn.policy && conn.policy.profile) || "guarded";
|
|
1110
|
+
const pc = POLICY_CHIP[policy] || ["", policy];
|
|
1111
|
+
const s = conn.settings || {};
|
|
1112
|
+
const current = s.currentLibrary || "";
|
|
1113
|
+
const libs = Array.isArray(s.libraryList) ? s.libraryList : [];
|
|
1114
|
+
const overrides = [
|
|
1115
|
+
Number.isFinite(Number(s.sessionIdleMinutes)) && s.sessionIdleMinutes ? ("idle " + s.sessionIdleMinutes + "m") : "",
|
|
1116
|
+
Number.isFinite(Number(s.sessionPingSeconds)) && s.sessionPingSeconds ? ("ping " + s.sessionPingSeconds + "s") : "",
|
|
1117
|
+
Number.isFinite(Number(s.sessionReconnectAttempts)) && s.sessionReconnectAttempts ? ("retry " + s.sessionReconnectAttempts) : ""
|
|
1118
|
+
].filter(Boolean);
|
|
1119
|
+
let ctx = "";
|
|
1120
|
+
if (current) ctx += "<span class='tag strong' title='Current library'>" + escapeHtml(current) + "</span>";
|
|
1121
|
+
const rest = libs.filter(l => l !== current);
|
|
1122
|
+
const shown = rest.slice(0, 4);
|
|
1123
|
+
ctx += shown.map(l => "<span class='tag'>" + escapeHtml(l) + "</span>").join("");
|
|
1124
|
+
if (rest.length > shown.length) ctx += "<span class='cell-sub' title='" + escapeHtml(rest.join(", ")) + "'>+" + (rest.length - shown.length) + " more</span>";
|
|
1125
|
+
if (!ctx) ctx = "<span class='cell-sub'>Default</span>";
|
|
1126
|
+
if (overrides.length) ctx += "<div class='cell-sub' style='margin-top:4px'>Overrides: " + escapeHtml(overrides.join(", ")) + "</div>";
|
|
1127
|
+
const enc = encodeURIComponent(conn.name);
|
|
1128
|
+
const tr = document.createElement("tr");
|
|
1129
|
+
tr.innerHTML =
|
|
1130
|
+
"<td><div class='cell-primary'>" + escapeHtml(conn.name) + "</div>" + (conn.privateKeyPath ? "<div class='cell-sub'>SSH key</div>" : "") + "</td>" +
|
|
1131
|
+
"<td class='mono' style='font-size:12.5px'>" + escapeHtml(conn.host || "") + "<span class='cell-sub'>:" + escapeHtml(String(conn.port || 22)) + "</span></td>" +
|
|
1132
|
+
"<td class='mono' style='font-size:12.5px'>" + escapeHtml(conn.username || "") + "</td>" +
|
|
1133
|
+
"<td>" + testStatusChip(conn.name, conn.lastTest) + "</td>" +
|
|
1134
|
+
"<td><span class='chip " + pc[0] + "'>" + escapeHtml(pc[1]) + "</span></td>" +
|
|
1135
|
+
"<td>" + ctx + "</td>" +
|
|
1136
|
+
"<td class='right'><div class='row-actions'>" +
|
|
1137
|
+
"<button class='btn btn-ghost btn-icon' data-action='test' data-name='" + enc + "' type='button' title='Test connection' aria-label='Test " + escapeHtml(conn.name) + "'>" + icon("pulse") + "</button>" +
|
|
1138
|
+
"<button class='btn btn-ghost btn-icon' data-action='edit' data-name='" + enc + "' type='button' title='Edit' aria-label='Edit " + escapeHtml(conn.name) + "'>" + icon("edit") + "</button>" +
|
|
1139
|
+
"<button class='btn btn-ghost btn-icon danger-hover' data-action='delete' data-name='" + enc + "' type='button' title='Delete' aria-label='Delete " + escapeHtml(conn.name) + "'>" + icon("trash") + "</button>" +
|
|
1140
|
+
"</div></td>";
|
|
1141
|
+
body.appendChild(tr);
|
|
1142
|
+
}
|
|
1143
|
+
body.querySelectorAll("button").forEach(btn => btn.addEventListener("click", onConnectionAction));
|
|
1144
|
+
syncControls();
|
|
1145
|
+
}
|
|
1146
|
+
const TEST_LABELS = {
|
|
1147
|
+
"ok": "Connected", "invalid-input": "Incomplete", "host-not-found": "Host not found",
|
|
1148
|
+
"host-unreachable": "Unreachable", "port-closed": "SSH port closed", "port-timeout": "No answer",
|
|
1149
|
+
"not-ssh": "Not SSH", "host-key-changed": "Host key changed", "no-credentials": "No password",
|
|
1150
|
+
"key-unreadable": "Key problem", "auth-failed": "Sign-on rejected", "password-expired": "Password expired",
|
|
1151
|
+
"password-already-rejected": "Password rejected", "timeout": "Timed out", "error": "Failed"
|
|
1152
|
+
};
|
|
1153
|
+
function testTone(result) {
|
|
1154
|
+
if (result.ok) return result.warnings && result.warnings.length ? "warn" : "ok";
|
|
1155
|
+
return result.code === "no-credentials" ? "warn" : "danger";
|
|
1156
|
+
}
|
|
1157
|
+
function testSummary(result) {
|
|
1158
|
+
return [result.message].concat(result.warnings || []).join(" ");
|
|
1159
|
+
}
|
|
1160
|
+
function testStatusChip(name, result) {
|
|
1161
|
+
if (state.testing[name]) return "<span class='chip info running'>Testing</span>";
|
|
1162
|
+
if (!result) return "<span class='chip plain'>Not tested</span>";
|
|
1163
|
+
const when = result.testedAt ? (" Tested " + formatTime(result.testedAt) + ".") : "";
|
|
1164
|
+
return "<span class='chip " + testTone(result) + "' title='" + escapeHtml(testSummary(result) + when) + "'>" +
|
|
1165
|
+
escapeHtml(TEST_LABELS[result.code] || result.code) + "</span>";
|
|
1166
|
+
}
|
|
1167
|
+
function renderTestResult(result) {
|
|
1168
|
+
const box = $("connectionTestResult");
|
|
1169
|
+
if (!result) { box.className = "test-result"; box.innerHTML = ""; return; }
|
|
1170
|
+
const tone = testTone(result);
|
|
1171
|
+
const steps = (result.steps || []).map(s =>
|
|
1172
|
+
"<li class='" + (s.ok ? "ok" : "fail") + "'><span class='mark'>" + (s.ok ? "✓" : "✗") + "</span>" +
|
|
1173
|
+
"<span><strong>" + escapeHtml(s.name) + "</strong> <span class='detail'>" + escapeHtml(s.detail || "") + "</span></span></li>").join("");
|
|
1174
|
+
const warnings = (result.warnings || []).map(w => "<li class='fail'><span class='mark'>!</span><span>" + escapeHtml(w) + "</span></li>").join("");
|
|
1175
|
+
box.className = "test-result show " + tone;
|
|
1176
|
+
box.innerHTML = "<div class='tr-title'>" + icon(tone === "ok" ? "check" : "alert", 14) + escapeHtml(TEST_LABELS[result.code] || result.code) + "</div>" +
|
|
1177
|
+
"<div class='tr-msg'>" + escapeHtml(result.message) + "</div>" +
|
|
1178
|
+
(steps || warnings ? "<ul>" + steps + warnings + "</ul>" : "");
|
|
1179
|
+
}
|
|
1180
|
+
async function testSavedConnection(name, btn) {
|
|
1181
|
+
if (state.testing[name]) return;
|
|
1182
|
+
state.testing[name] = true;
|
|
1183
|
+
renderConnections(state.connections);
|
|
1184
|
+
let result = null;
|
|
1185
|
+
try {
|
|
1186
|
+
result = (await api("/api/connections/" + encodeURIComponent(name) + "/test", { method: "POST", body: "{}" })).result;
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
toast("error", "Could not test " + name, errorMessage(err));
|
|
1189
|
+
} finally {
|
|
1190
|
+
delete state.testing[name];
|
|
1191
|
+
}
|
|
1192
|
+
try { await loadConnections(); } catch { renderConnections(state.connections); }
|
|
1193
|
+
if (result) {
|
|
1194
|
+
if (result.ok && !(result.warnings || []).length) toast("ok", name + ": connected", result.message);
|
|
1195
|
+
else toast(result.ok ? "info" : "error", name + ": " + (TEST_LABELS[result.code] || result.code), testSummary(result));
|
|
1196
|
+
}
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
function versionChip(status) {
|
|
1200
|
+
if (status === "latest") return ["Up to date", "ok"];
|
|
1201
|
+
if (status === "update-available") return ["Update available", "warn"];
|
|
1202
|
+
if (status === "not-installed") return ["Not installed", ""];
|
|
1203
|
+
return ["Unknown", ""];
|
|
1204
|
+
}
|
|
1205
|
+
function renderVersions(data) {
|
|
1206
|
+
const mcp = data?.mcp || {};
|
|
1207
|
+
const skills = data?.skills || {};
|
|
1208
|
+
const mc = versionChip(mcp.status);
|
|
1209
|
+
$("mcpVersionText").textContent = mcp.installedVersion ? ("v" + String(mcp.installedVersion).replace(/^v/, "")) : "Unknown";
|
|
1210
|
+
$("mcpVersionMeta").textContent = mcp.latestVersion ? ("Latest release v" + String(mcp.latestVersion).replace(/^v/, "")) : "Latest release unknown";
|
|
1211
|
+
setChip("mcpVersionBadge", mc[0], mc[1]);
|
|
1212
|
+
setChip("rtMcpBadge", mc[0], mc[1]);
|
|
1213
|
+
$("rtMcpText").textContent = "Installed " + (mcp.installedVersion || "unknown") + " · Latest " + (mcp.latestVersion || "unknown");
|
|
1214
|
+
$("updateMcpBtn").className = "btn requires-online " + (mcp.status === "update-available" ? "btn-primary" : "btn-secondary");
|
|
1215
|
+
|
|
1216
|
+
const sc = versionChip(skills.status);
|
|
1217
|
+
const short = c => String(c || "").slice(0, 7);
|
|
1218
|
+
if (skills.localCommit) {
|
|
1219
|
+
$("skillsVersionText").textContent = short(skills.localCommit);
|
|
1220
|
+
$("skillsVersionMeta").textContent = (skills.localBranch || "branch") + (skills.latestCommit ? (" · latest " + short(skills.latestCommit)) : "");
|
|
1221
|
+
} else {
|
|
1222
|
+
$("skillsVersionText").textContent = skills.status === "not-installed" ? "Not installed" : "Unknown";
|
|
1223
|
+
$("skillsVersionMeta").textContent = skills.latestCommit ? ("Latest " + short(skills.latestCommit)) : "";
|
|
1224
|
+
}
|
|
1225
|
+
setChip("skillsVersionBadge", sc[0], sc[1]);
|
|
1226
|
+
setChip("rtSkillsBadge", sc[0], sc[1]);
|
|
1227
|
+
$("rtSkillsText").textContent = skills.localCommit
|
|
1228
|
+
? ((skills.localBranch || "branch") + " @ " + short(skills.localCommit) + (skills.latestCommit ? (" · Latest " + short(skills.latestCommit)) : ""))
|
|
1229
|
+
: (skills.status === "not-installed" ? "Not installed yet. Run an update to install." : "Unknown");
|
|
1230
|
+
$("updateSkillsBtn").textContent = skills.status === "not-installed" ? "Install skills" : "Update skills";
|
|
1231
|
+
$("updateSkillsBtn").className = "btn requires-online " + (skills.status === "update-available" || skills.status === "not-installed" ? "btn-primary" : "btn-secondary");
|
|
1232
|
+
|
|
1233
|
+
if (data?.checkedAt) $("versionCheckedAt").textContent = "Versions checked " + formatTime(data.checkedAt) + ".";
|
|
1234
|
+
syncControls();
|
|
1235
|
+
}
|
|
1236
|
+
function syncServiceButtons() {
|
|
1237
|
+
const s = state.service;
|
|
1238
|
+
const disabled = !state.online || state.busy;
|
|
1239
|
+
$("enableServiceBtn").hidden = Boolean(s && s.enabled);
|
|
1240
|
+
$("disableServiceBtn").hidden = Boolean(s && !s.enabled);
|
|
1241
|
+
$("enableServiceBtn").disabled = disabled;
|
|
1242
|
+
$("disableServiceBtn").disabled = disabled;
|
|
1243
|
+
}
|
|
1244
|
+
function renderService(status) {
|
|
1245
|
+
state.service = status || null;
|
|
1246
|
+
if (!status) {
|
|
1247
|
+
$("autostartInfo").textContent = "Background service status is unavailable.";
|
|
1248
|
+
$("rtServiceTitle").textContent = "Unavailable";
|
|
1249
|
+
$("startupText").textContent = "Unavailable";
|
|
1250
|
+
$("startupMeta").textContent = "";
|
|
1251
|
+
setChip("startupBadge", "Unknown", "");
|
|
1252
|
+
setChip("rtServiceBadge", "Unknown", "");
|
|
1253
|
+
syncServiceButtons();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
let chip;
|
|
1257
|
+
if (status.enabled && status.running) chip = ["Running", "ok"];
|
|
1258
|
+
else if (status.enabled) chip = ["Enabled, stopped", "warn"];
|
|
1259
|
+
else chip = ["Disabled", ""];
|
|
1260
|
+
setChip("startupBadge", chip[0], chip[1]);
|
|
1261
|
+
setChip("rtServiceBadge", chip[0], chip[1]);
|
|
1262
|
+
$("startupText").textContent = status.enabled ? "Starts at sign-in" : "Manual start";
|
|
1263
|
+
$("startupMeta").textContent = (status.label || "") + (status.arch ? (" · " + status.arch) : "") + (status.managedBy ? (" · " + status.managedBy) : "");
|
|
1264
|
+
$("rtServiceTitle").textContent = status.enabled ? ("Enabled via " + (status.managedBy || "OS service manager")) : "Not enabled";
|
|
1265
|
+
const detail = [status.detail || "", status.target ? ("Target: " + status.target) : ""].filter(Boolean).join(" ");
|
|
1266
|
+
$("autostartInfo").textContent = detail || ((status.label || "This platform") + " (" + (status.arch || "") + ")");
|
|
1267
|
+
syncServiceButtons();
|
|
1268
|
+
}
|
|
1269
|
+
function renderCredentialStore(status) {
|
|
1270
|
+
const el = $("credentialNotice");
|
|
1271
|
+
if (!status) {
|
|
1272
|
+
el.className = "banner warn";
|
|
1273
|
+
$("credentialTitle").textContent = "Credential store unavailable";
|
|
1274
|
+
$("credentialText").textContent = "Passwords cannot be saved until a credential store is reachable.";
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
el.className = "banner " + (status.persistent ? "ok" : "warn");
|
|
1278
|
+
el.querySelector(".b-icon").innerHTML = icon(status.persistent ? "shield" : "alert");
|
|
1279
|
+
$("credentialTitle").textContent = status.persistent ? "Credentials are stored securely" : "Credentials are not persisted";
|
|
1280
|
+
$("credentialText").textContent = status.message || "";
|
|
1281
|
+
}
|
|
1282
|
+
function clientInitials(label) {
|
|
1283
|
+
return String(label || "?").split(/[^A-Za-z0-9]+/).filter(Boolean).slice(0, 2).map(w => w[0].toUpperCase()).join("") || "?";
|
|
1284
|
+
}
|
|
1285
|
+
function renderClients(targets) {
|
|
1286
|
+
const list = Array.isArray(targets) ? targets : [];
|
|
1287
|
+
const ready = list.filter(t => t.autoInstall && t.available).length;
|
|
1288
|
+
const configured = list.filter(t => t.configured).length;
|
|
1289
|
+
$("clientSummary").textContent = list.length
|
|
1290
|
+
? ("Register MCP for i with the coding assistants on this machine. " + ready + " detected, " + configured + " configured.")
|
|
1291
|
+
: "No client integrations detected.";
|
|
1292
|
+
const host = $("clientButtons");
|
|
1293
|
+
host.innerHTML = "";
|
|
1294
|
+
for (const target of list) {
|
|
1295
|
+
let chip;
|
|
1296
|
+
if (target.configured) chip = "<span class='chip ok'>Configured</span>";
|
|
1297
|
+
else if (!target.autoInstall) chip = "<span class='chip plain'>Manual setup</span>";
|
|
1298
|
+
else if (target.available) chip = "<span class='chip info'>Detected</span>";
|
|
1299
|
+
else chip = "<span class='chip plain'>Not detected</span>";
|
|
1300
|
+
const card = document.createElement("div");
|
|
1301
|
+
card.className = "card client";
|
|
1302
|
+
card.innerHTML =
|
|
1303
|
+
"<div class='client-top'><div class='client-logo'>" + escapeHtml(clientInitials(target.label)) + "</div>" +
|
|
1304
|
+
"<div style='min-width:0;flex:1'><div class='client-name'>" + escapeHtml(target.label) + "</div></div>" + chip + "</div>" +
|
|
1305
|
+
"<div class='client-desc'>" + escapeHtml(target.description || "") + (target.details ? ("<div class='help' style='margin-top:4px'>" + escapeHtml(target.details) + "</div>") : "") + "</div>" +
|
|
1306
|
+
"<div class='client-foot'><span></span></div>";
|
|
1307
|
+
const btn = document.createElement("button");
|
|
1308
|
+
btn.type = "button";
|
|
1309
|
+
const primary = target.autoInstall && target.available && !target.configured;
|
|
1310
|
+
btn.className = "btn " + (primary ? "btn-primary" : "btn-secondary");
|
|
1311
|
+
btn.textContent = target.autoInstall ? (target.configured ? "Re-apply" : "Add to " + target.label.split(" /")[0]) : "Show instructions";
|
|
1312
|
+
btn.dataset.clientId = target.id;
|
|
1313
|
+
btn.disabled = !state.online || state.busy;
|
|
1314
|
+
btn.addEventListener("click", () => installClient(target.id, btn));
|
|
1315
|
+
card.querySelector(".client-foot").appendChild(btn);
|
|
1316
|
+
host.appendChild(card);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function renderSessions(snapshot) {
|
|
1320
|
+
const body = $("sessionsBody");
|
|
1321
|
+
body.innerHTML = "";
|
|
1322
|
+
const sessions = snapshot && Array.isArray(snapshot.sessions) ? snapshot.sessions : [];
|
|
1323
|
+
$("sessionsUpdated").textContent = snapshot && snapshot.updatedAt
|
|
1324
|
+
? ("Warm IBM i sessions held by the MCP server. Updated " + relativeTime(snapshot.updatedAt) + ".")
|
|
1325
|
+
: "Warm IBM i sessions held by the MCP server.";
|
|
1326
|
+
if (sessions.length === 0) {
|
|
1327
|
+
const tr = document.createElement("tr");
|
|
1328
|
+
tr.innerHTML = "<td colspan='4' style='padding:0'><div class='empty compact'><div class='empty-icon'>" + icon("pulse") + "</div><h3>No active sessions</h3><p>Sessions open when your coding assistant first uses a connection.</p></div></td>";
|
|
1329
|
+
body.appendChild(tr);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
for (const s of sessions) {
|
|
1333
|
+
const st = String(s.state || "-");
|
|
1334
|
+
const kind = /connected|ready|active|open/i.test(st) ? "ok" : /reconnect|connecting|idle/i.test(st) ? "warn" : /fail|error|closed|disconnect/i.test(st) ? "danger" : "";
|
|
1335
|
+
const pc = POLICY_CHIP[s.policy] || ["", s.policy || "-"];
|
|
1336
|
+
const tr = document.createElement("tr");
|
|
1337
|
+
tr.innerHTML = "<td><span class='cell-primary'>" + escapeHtml(s.name || "") + "</span>" + (s.active ? " <span class='chip info plain' style='margin-left:6px'>Active</span>" : "") + "</td>" +
|
|
1338
|
+
"<td><span class='chip " + kind + "'>" + escapeHtml(st) + "</span></td>" +
|
|
1339
|
+
"<td><span class='chip plain " + pc[0] + "'>" + escapeHtml(pc[1]) + "</span></td>" +
|
|
1340
|
+
"<td class='mono' style='font-size:12.5px'>" + escapeHtml(formatDuration(s.idleSecondsRemaining)) + "</td>";
|
|
1341
|
+
body.appendChild(tr);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function renderLiveViews(views) {
|
|
1345
|
+
const body = $("liveBody");
|
|
1346
|
+
body.innerHTML = "";
|
|
1347
|
+
const rows = [];
|
|
1348
|
+
for (const view of Array.isArray(views) ? views : []) {
|
|
1349
|
+
for (const s of view.sessions || []) rows.push({ view, s });
|
|
1350
|
+
}
|
|
1351
|
+
if (rows.length === 0) {
|
|
1352
|
+
const tr = document.createElement("tr");
|
|
1353
|
+
tr.innerHTML = "<td colspan='5' style='padding:0'><div class='empty compact'><div class='empty-icon'>" + icon("pulse") + "</div><h3>No 5250 session</h3><p>A screen appears here when your coding assistant opens one with ibmi.tn5250.connect.</p></div></td>";
|
|
1354
|
+
body.appendChild(tr);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
for (const { view, s } of rows) {
|
|
1358
|
+
const url = new URL(view.url);
|
|
1359
|
+
url.searchParams.set("s", s.connectionName);
|
|
1360
|
+
const tr = document.createElement("tr");
|
|
1361
|
+
tr.innerHTML = "<td><span class='cell-primary'>" + escapeHtml(s.connectionName) + "</span></td>" +
|
|
1362
|
+
"<td class='mono' style='font-size:12.5px'>" + escapeHtml(s.host || "") + "</td>" +
|
|
1363
|
+
"<td><span class='chip " + (s.connected ? "ok" : "danger") + "'>" + (s.connected ? (s.signedOn ? "Signed on" : "Connected") : "Closed") + "</span></td>" +
|
|
1364
|
+
"<td>" + escapeHtml(s.lastActor === "agent" ? "Agent" : s.lastActor === "person" ? "You" : "-") + "</td>" +
|
|
1365
|
+
"<td style='text-align:right'><a class='btn' href='" + escapeHtml(url.toString()) + "' target='_blank' rel='noopener'>Open</a></td>";
|
|
1366
|
+
body.appendChild(tr);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async function loadLiveViews() { renderLiveViews((await api("/api/tn5250/live")).views || []); }
|
|
1370
|
+
const SETTINGS_FIELDS = [
|
|
1371
|
+
["sessionIdleMinutes", 1, 1440, 30],
|
|
1372
|
+
["sessionPingSeconds", 5, 300, 15],
|
|
1373
|
+
["sessionReconnectAttempts", 1, 5, 2]
|
|
1374
|
+
];
|
|
1375
|
+
function renderSettings(settings) {
|
|
1376
|
+
state.settings = {};
|
|
1377
|
+
for (const [key, , , def] of SETTINGS_FIELDS) {
|
|
1378
|
+
const v = settings?.[key] ?? def;
|
|
1379
|
+
state.settings[key] = String(v);
|
|
1380
|
+
$(key).value = String(v);
|
|
1381
|
+
$(key).closest(".field").classList.remove("invalid");
|
|
1382
|
+
}
|
|
1383
|
+
updateSettingsDirty();
|
|
1384
|
+
}
|
|
1385
|
+
function settingsDirty() {
|
|
1386
|
+
if (!state.settings) return false;
|
|
1387
|
+
return SETTINGS_FIELDS.some(([key]) => String($(key).value) !== state.settings[key]);
|
|
1388
|
+
}
|
|
1389
|
+
function updateSettingsDirty() {
|
|
1390
|
+
const dirty = settingsDirty();
|
|
1391
|
+
$("settingsDirty").textContent = dirty ? "You have unsaved changes" : "No unsaved changes";
|
|
1392
|
+
$("settingsDirty").style.color = dirty ? "var(--warn)" : "";
|
|
1393
|
+
}
|
|
1394
|
+
function validateSettings() {
|
|
1395
|
+
let ok = true;
|
|
1396
|
+
for (const [key, min, max] of SETTINGS_FIELDS) {
|
|
1397
|
+
const raw = $(key).value.trim();
|
|
1398
|
+
const n = Number(raw);
|
|
1399
|
+
const valid = raw !== "" && Number.isInteger(n) && n >= min && n <= max;
|
|
1400
|
+
$(key).closest(".field").classList.toggle("invalid", !valid);
|
|
1401
|
+
if (!valid) ok = false;
|
|
1402
|
+
}
|
|
1403
|
+
return ok;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/* Loaders */
|
|
1407
|
+
async function pingHealth() {
|
|
1408
|
+
try {
|
|
1409
|
+
const res = await fetch("/api/health", { cache: "no-store", headers: { [TOKEN_HEADER]: API_TOKEN } });
|
|
1410
|
+
if (!res.ok) return false;
|
|
1411
|
+
const data = await res.json();
|
|
1412
|
+
renderCredentialStore(data?.credentialStore || null);
|
|
1413
|
+
return Boolean(data?.ok);
|
|
1414
|
+
} catch {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
async function loadConnections() { renderConnections((await api("/api/connections")).connections || []); }
|
|
1419
|
+
async function loadRuntimeStatus() { renderLogs((await api("/api/runtime/status")).jobs || {}); }
|
|
1420
|
+
async function loadServiceStatus() { renderService((await api("/api/service/status")).status || null); }
|
|
1421
|
+
async function loadClients() { renderClients((await api("/api/clients")).targets || []); }
|
|
1422
|
+
async function loadSessions() { renderSessions((await api("/api/sessions")).snapshot || null); }
|
|
1423
|
+
async function loadSettings() { renderSettings((await api("/api/settings")).settings || null); }
|
|
1424
|
+
async function loadVersions() {
|
|
1425
|
+
const s = persistSkillsUpdateSettings();
|
|
1426
|
+
const query = new URLSearchParams({ repoUrl: s.repoUrl, branch: s.branch });
|
|
1427
|
+
renderVersions(await api("/api/runtime/versions?" + query.toString()));
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/* Connection dialog */
|
|
1431
|
+
function setPolicy(value) {
|
|
1432
|
+
document.querySelectorAll("input[name='connPolicy']").forEach(r => { r.checked = r.value === value; });
|
|
1433
|
+
}
|
|
1434
|
+
function getPolicy() {
|
|
1435
|
+
const r = document.querySelector("input[name='connPolicy']:checked");
|
|
1436
|
+
return r ? r.value : "guarded";
|
|
1437
|
+
}
|
|
1438
|
+
function openConnectionDialog(mode, conn) {
|
|
1439
|
+
const edit = mode === "edit";
|
|
1440
|
+
const s = (edit && conn.settings) || {};
|
|
1441
|
+
state.editingName = edit ? String(conn.name || "") : "";
|
|
1442
|
+
$("connectionDialogTitle").textContent = edit ? "Edit connection" : "Add connection";
|
|
1443
|
+
$("connectionDialogSub").textContent = edit ? ("Update settings for " + conn.name + ".") : "Connect over SSH to an IBM i system.";
|
|
1444
|
+
$("connName").disabled = edit;
|
|
1445
|
+
$("connName").value = edit ? (conn.name || "") : "";
|
|
1446
|
+
$("connHost").value = edit ? (conn.host || "") : "";
|
|
1447
|
+
$("connPort").value = edit ? String(conn.port || 22) : "22";
|
|
1448
|
+
$("connUser").value = edit ? (conn.username || "") : "";
|
|
1449
|
+
$("connKeyPath").value = edit ? (conn.privateKeyPath || "") : "";
|
|
1450
|
+
$("connPassword").value = "";
|
|
1451
|
+
$("connPassword").placeholder = edit ? "Leave blank to keep current" : "Stored in OS keychain";
|
|
1452
|
+
setPolicy(edit ? ((conn.policy && conn.policy.profile) || "guarded") : "guarded");
|
|
1453
|
+
$("connCurrentLibrary").value = s.currentLibrary || "";
|
|
1454
|
+
$("connLibraryList").value = Array.isArray(s.libraryList) ? s.libraryList.join(", ") : "";
|
|
1455
|
+
$("connSessionIdleMinutes").value = s.sessionIdleMinutes ? String(s.sessionIdleMinutes) : "";
|
|
1456
|
+
$("connSessionPingSeconds").value = s.sessionPingSeconds ? String(s.sessionPingSeconds) : "";
|
|
1457
|
+
$("connSessionReconnectAttempts").value = s.sessionReconnectAttempts ? String(s.sessionReconnectAttempts) : "";
|
|
1458
|
+
$("overridesDisclosure").open = Boolean(s.sessionIdleMinutes || s.sessionPingSeconds || s.sessionReconnectAttempts);
|
|
1459
|
+
$("connectionError").classList.remove("show");
|
|
1460
|
+
renderTestResult(null);
|
|
1461
|
+
document.querySelectorAll("#connectionForm .field").forEach(f => f.classList.remove("invalid"));
|
|
1462
|
+
const dialog = $("connectionDialog");
|
|
1463
|
+
if (typeof dialog.showModal === "function") dialog.showModal(); else dialog.setAttribute("open", "open");
|
|
1464
|
+
(edit ? $("connHost") : $("connName")).focus();
|
|
1465
|
+
}
|
|
1466
|
+
function closeConnectionDialog() {
|
|
1467
|
+
const dialog = $("connectionDialog");
|
|
1468
|
+
if (typeof dialog.close === "function") dialog.close(); else dialog.removeAttribute("open");
|
|
1469
|
+
state.editingName = "";
|
|
1470
|
+
}
|
|
1471
|
+
async function onConnectionAction(e) {
|
|
1472
|
+
const btn = e.currentTarget;
|
|
1473
|
+
const action = btn.dataset.action;
|
|
1474
|
+
const name = decodeURIComponent(btn.dataset.name || "");
|
|
1475
|
+
const conn = state.connections.find(c => c.name === name);
|
|
1476
|
+
if (!conn) return;
|
|
1477
|
+
if (action === "edit") {
|
|
1478
|
+
openConnectionDialog("edit", conn);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (action === "test") {
|
|
1482
|
+
await testSavedConnection(name, btn);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (action === "delete") {
|
|
1486
|
+
const ok = await confirmAction("Delete " + name + "?", "This removes the saved connection and its stored password. Active sessions for it will close.", "Delete connection");
|
|
1487
|
+
if (!ok) return;
|
|
1488
|
+
await withLoading(btn, async () => {
|
|
1489
|
+
try {
|
|
1490
|
+
await api("/api/connections/" + encodeURIComponent(name), { method: "DELETE" });
|
|
1491
|
+
toast("ok", "Connection deleted", name);
|
|
1492
|
+
await loadConnections();
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
toast("error", "Could not delete connection", errorMessage(err));
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function validateConnectionForm() {
|
|
1500
|
+
let first = null;
|
|
1501
|
+
const check = (id, valid) => {
|
|
1502
|
+
$(id).closest(".field").classList.toggle("invalid", !valid);
|
|
1503
|
+
if (!valid && !first) first = $(id);
|
|
1504
|
+
};
|
|
1505
|
+
check("connName", Boolean($("connName").value.trim()));
|
|
1506
|
+
check("connUser", Boolean($("connUser").value.trim()));
|
|
1507
|
+
check("connHost", Boolean($("connHost").value.trim()));
|
|
1508
|
+
const port = Number($("connPort").value || 22);
|
|
1509
|
+
check("connPort", Number.isInteger(port) && port >= 1 && port <= 65535);
|
|
1510
|
+
if (first) first.focus();
|
|
1511
|
+
return !first;
|
|
1512
|
+
}
|
|
1513
|
+
async function saveConnectionFromDialog(e) {
|
|
1514
|
+
e.preventDefault();
|
|
1515
|
+
$("connectionError").classList.remove("show");
|
|
1516
|
+
if (!validateConnectionForm()) return;
|
|
1517
|
+
const name = $("connName").value.trim();
|
|
1518
|
+
const password = $("connPassword").value || "";
|
|
1519
|
+
const payload = {
|
|
1520
|
+
name,
|
|
1521
|
+
host: $("connHost").value.trim(),
|
|
1522
|
+
port: Number($("connPort").value || 22),
|
|
1523
|
+
username: $("connUser").value.trim(),
|
|
1524
|
+
privateKeyPath: $("connKeyPath").value.trim() || undefined,
|
|
1525
|
+
password: password || undefined,
|
|
1526
|
+
storePassword: Boolean(password),
|
|
1527
|
+
policy: { profile: getPolicy() },
|
|
1528
|
+
settings: {
|
|
1529
|
+
currentLibrary: $("connCurrentLibrary").value.trim().toUpperCase(),
|
|
1530
|
+
libraryList: normalizeLibraries($("connLibraryList").value),
|
|
1531
|
+
sessionIdleMinutes: parseOptionalNumber($("connSessionIdleMinutes").value, 1, 1440),
|
|
1532
|
+
sessionPingSeconds: parseOptionalNumber($("connSessionPingSeconds").value, 5, 300),
|
|
1533
|
+
sessionReconnectAttempts: parseOptionalNumber($("connSessionReconnectAttempts").value, 1, 5)
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
const editing = state.editingName;
|
|
1537
|
+
await withLoading($("saveConnectionBtn"), async () => {
|
|
1538
|
+
try {
|
|
1539
|
+
if (editing) {
|
|
1540
|
+
await api("/api/connections/" + encodeURIComponent(editing), { method: "PUT", body: JSON.stringify(payload) });
|
|
1541
|
+
} else {
|
|
1542
|
+
await api("/api/connections", { method: "POST", body: JSON.stringify(payload) });
|
|
1543
|
+
}
|
|
1544
|
+
closeConnectionDialog();
|
|
1545
|
+
toast("ok", editing ? "Connection updated" : "Connection added", name);
|
|
1546
|
+
await loadConnections();
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
$("connectionError").textContent = errorMessage(err);
|
|
1549
|
+
$("connectionError").classList.add("show");
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
// One check after saving, so the list shows whether the details work.
|
|
1553
|
+
testSavedConnection(name);
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
async function testConnectionFromDialog() {
|
|
1557
|
+
$("connectionError").classList.remove("show");
|
|
1558
|
+
let first = null;
|
|
1559
|
+
const check = (id, valid) => {
|
|
1560
|
+
$(id).closest(".field").classList.toggle("invalid", !valid);
|
|
1561
|
+
if (!valid && !first) first = $(id);
|
|
1562
|
+
};
|
|
1563
|
+
check("connUser", Boolean($("connUser").value.trim()));
|
|
1564
|
+
check("connHost", Boolean($("connHost").value.trim()));
|
|
1565
|
+
const port = Number($("connPort").value || 22);
|
|
1566
|
+
check("connPort", Number.isInteger(port) && port >= 1 && port <= 65535);
|
|
1567
|
+
if (first) { first.focus(); return; }
|
|
1568
|
+
const payload = {
|
|
1569
|
+
name: state.editingName || undefined,
|
|
1570
|
+
host: $("connHost").value.trim(),
|
|
1571
|
+
port,
|
|
1572
|
+
username: $("connUser").value.trim(),
|
|
1573
|
+
privateKeyPath: $("connKeyPath").value.trim() || undefined,
|
|
1574
|
+
password: $("connPassword").value || undefined
|
|
1575
|
+
};
|
|
1576
|
+
renderTestResult(null);
|
|
1577
|
+
await withLoading($("testConnectionBtn"), async () => {
|
|
1578
|
+
try {
|
|
1579
|
+
const result = (await api("/api/connections/test", { method: "POST", body: JSON.stringify(payload) })).result;
|
|
1580
|
+
renderTestResult(result);
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
$("connectionError").textContent = errorMessage(err);
|
|
1583
|
+
$("connectionError").classList.add("show");
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
$("connectionTestResult").scrollIntoView({ block: "nearest" });
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/* Settings */
|
|
1590
|
+
async function saveSettings(e) {
|
|
1591
|
+
if (e) e.preventDefault();
|
|
1592
|
+
if (!validateSettings()) return;
|
|
1593
|
+
await withLoading($("saveSettingsBtn"), async () => {
|
|
1594
|
+
try {
|
|
1595
|
+
await api("/api/settings", {
|
|
1596
|
+
method: "PUT",
|
|
1597
|
+
body: JSON.stringify({
|
|
1598
|
+
sessionIdleMinutes: Number($("sessionIdleMinutes").value),
|
|
1599
|
+
sessionPingSeconds: Number($("sessionPingSeconds").value),
|
|
1600
|
+
sessionReconnectAttempts: Number($("sessionReconnectAttempts").value)
|
|
1601
|
+
})
|
|
1602
|
+
});
|
|
1603
|
+
await loadSettings();
|
|
1604
|
+
toast("ok", "Settings saved");
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
toast("error", "Could not save settings", errorMessage(err));
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/* Jobs */
|
|
1612
|
+
async function beginReconnectFlow(label, jobKey) {
|
|
1613
|
+
setServiceState("reconnecting", "Restarting", label + " running. Waiting for restart...");
|
|
1614
|
+
const deadline = Date.now() + 600000;
|
|
1615
|
+
while (Date.now() < deadline) {
|
|
1616
|
+
await sleep(1800);
|
|
1617
|
+
if (await pingHealth()) {
|
|
1618
|
+
// Jobs run in the background: wait until this one is no longer running.
|
|
1619
|
+
// After a restart the new process reports it as idle.
|
|
1620
|
+
let job = null;
|
|
1621
|
+
if (jobKey) {
|
|
1622
|
+
try { job = ((await api("/api/runtime/status")).jobs || {})[jobKey] || null; } catch { continue; }
|
|
1623
|
+
if (job && job.status === "running") continue;
|
|
1624
|
+
}
|
|
1625
|
+
setServiceState("online", "Online", "Local control service");
|
|
1626
|
+
await refreshAll();
|
|
1627
|
+
if (job && job.status === "failed") toast("error", label + " failed", job.error || "");
|
|
1628
|
+
else toast("ok", label + " finished", "The control plane is back online.");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
setServiceState("offline", "Offline", "Run mcp-for-i control to start it");
|
|
1633
|
+
toast("error", "Service did not come back", "Start it again with mcp-for-i control.");
|
|
1634
|
+
}
|
|
1635
|
+
async function triggerJob(path, payload, options = {}, btn) {
|
|
1636
|
+
if (!state.online) {
|
|
1637
|
+
toast("error", "Control plane is offline", "Start it with mcp-for-i control.");
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (options.confirm) {
|
|
1641
|
+
const ok = await confirmAction(options.confirm.title, options.confirm.text, options.confirm.ok);
|
|
1642
|
+
if (!ok) return;
|
|
1643
|
+
}
|
|
1644
|
+
setBusy(true);
|
|
1645
|
+
if (btn) btn.classList.add("loading");
|
|
1646
|
+
try {
|
|
1647
|
+
const started = await api(path, { method: "POST", body: payload ? JSON.stringify(payload) : undefined });
|
|
1648
|
+
await loadRuntimeStatus();
|
|
1649
|
+
if (options.expectRestart) await beginReconnectFlow(options.label || "Update", started && started.job ? started.job.id : "");
|
|
1650
|
+
else {
|
|
1651
|
+
await Promise.allSettled([loadServiceStatus(), loadVersions(), loadSessions()]);
|
|
1652
|
+
startJobPolling();
|
|
1653
|
+
}
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
toast("error", (options.label || "Action") + " failed", errorMessage(err));
|
|
1656
|
+
if (!await pingHealth()) setServiceState("offline", "Offline", "Waiting for reconnect...");
|
|
1657
|
+
} finally {
|
|
1658
|
+
if (btn) btn.classList.remove("loading");
|
|
1659
|
+
setBusy(false);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function installClient(id, btn) {
|
|
1663
|
+
if (!state.online) {
|
|
1664
|
+
toast("error", "Control plane is offline", "Start it with mcp-for-i control.");
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
setBusy(true);
|
|
1668
|
+
if (btn) btn.classList.add("loading");
|
|
1669
|
+
try {
|
|
1670
|
+
const response = await api("/api/clients/install", { method: "POST", body: JSON.stringify({ id }) });
|
|
1671
|
+
const result = response?.result || {};
|
|
1672
|
+
const lines = [result.message || ""].concat(result.output || []);
|
|
1673
|
+
$("clientLogsTitle").textContent = result.label || id;
|
|
1674
|
+
$("clientLogs").textContent = lines.filter(Boolean).join(NL) || "Done.";
|
|
1675
|
+
$("clientLogsCard").hidden = false;
|
|
1676
|
+
if (result.ok === false) toast("error", "Could not configure " + (result.label || id), result.message || "");
|
|
1677
|
+
else toast("ok", result.label || id, result.message || "Done");
|
|
1678
|
+
await loadClients();
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
$("clientLogsTitle").textContent = "Client setup failed";
|
|
1681
|
+
$("clientLogs").textContent = errorMessage(err);
|
|
1682
|
+
$("clientLogsCard").hidden = false;
|
|
1683
|
+
toast("error", "Client setup failed", errorMessage(err));
|
|
1684
|
+
} finally {
|
|
1685
|
+
if (btn) btn.classList.remove("loading");
|
|
1686
|
+
setBusy(false);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
let pollId = 0;
|
|
1690
|
+
function startJobPolling() {
|
|
1691
|
+
if (pollId) return;
|
|
1692
|
+
pollId = setInterval(async () => {
|
|
1693
|
+
if (!await pingHealth()) return;
|
|
1694
|
+
try {
|
|
1695
|
+
const before = JSON.stringify(Object.fromEntries(Object.entries(state.jobs).map(([k, j]) => [k, j.status])));
|
|
1696
|
+
await loadRuntimeStatus();
|
|
1697
|
+
const running = Object.values(state.jobs || {}).some(j => j.status === "running");
|
|
1698
|
+
if (!running) {
|
|
1699
|
+
clearInterval(pollId);
|
|
1700
|
+
pollId = 0;
|
|
1701
|
+
const prev = JSON.parse(before);
|
|
1702
|
+
for (const [k, j] of Object.entries(state.jobs)) {
|
|
1703
|
+
if (prev[k] === "running" && j.status === "success") toast("ok", jobLabel(k) + " succeeded");
|
|
1704
|
+
if (prev[k] === "running" && j.status === "failed") toast("error", jobLabel(k) + " failed", j.error || "");
|
|
1705
|
+
}
|
|
1706
|
+
await Promise.allSettled([loadServiceStatus(), loadVersions(), loadSessions()]);
|
|
1707
|
+
}
|
|
1708
|
+
} catch {}
|
|
1709
|
+
}, 1800);
|
|
1710
|
+
}
|
|
1711
|
+
let heartbeatId = 0;
|
|
1712
|
+
function startHeartbeat() {
|
|
1713
|
+
if (heartbeatId) clearInterval(heartbeatId);
|
|
1714
|
+
heartbeatId = setInterval(async () => {
|
|
1715
|
+
const ok = await pingHealth();
|
|
1716
|
+
if (ok && state.online && !state.busy) loadLiveViews().catch(() => {});
|
|
1717
|
+
if (ok && !state.online) {
|
|
1718
|
+
setServiceState("online", "Online", "Local control service");
|
|
1719
|
+
await refreshAll();
|
|
1720
|
+
} else if (!ok && state.online && !state.busy) {
|
|
1721
|
+
setServiceState("offline", "Offline", "Waiting for reconnect...");
|
|
1722
|
+
}
|
|
1723
|
+
}, 2500);
|
|
1724
|
+
}
|
|
1725
|
+
async function refreshAll(btn) {
|
|
1726
|
+
return withLoading(btn, async () => {
|
|
1727
|
+
const healthy = await pingHealth();
|
|
1728
|
+
if (!healthy) {
|
|
1729
|
+
setServiceState("offline", "Offline", "Run mcp-for-i control to start it");
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
setServiceState("online", "Online", "Local control service");
|
|
1733
|
+
await Promise.allSettled([loadConnections(), loadRuntimeStatus(), loadServiceStatus(), loadVersions(), loadSessions(), loadLiveViews(), loadSettings(), loadClients()]);
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/* Wiring */
|
|
1738
|
+
document.querySelectorAll(".nav-item").forEach(btn => {
|
|
1739
|
+
btn.addEventListener("click", () => activateTab(btn.dataset.tab || "overview", true));
|
|
1740
|
+
});
|
|
1741
|
+
window.addEventListener("hashchange", () => activateTab(location.hash.replace("#", "")));
|
|
1742
|
+
$("themeBtn").addEventListener("click", () => applyTheme(currentTheme() === "dark" ? "light" : "dark", true));
|
|
1743
|
+
$("addConnectionBtn").addEventListener("click", () => openConnectionDialog("add", {}));
|
|
1744
|
+
$("emptyAddConnectionBtn").addEventListener("click", () => openConnectionDialog("add", {}));
|
|
1745
|
+
$("connectionForm").addEventListener("submit", saveConnectionFromDialog);
|
|
1746
|
+
$("cancelConnectionBtn").addEventListener("click", closeConnectionDialog);
|
|
1747
|
+
$("testConnectionBtn").addEventListener("click", testConnectionFromDialog);
|
|
1748
|
+
$("closeConnectionDialogBtn").addEventListener("click", closeConnectionDialog);
|
|
1749
|
+
["connName", "connUser", "connHost", "connPort"].forEach(id => $(id).addEventListener("input", () => $(id).closest(".field").classList.remove("invalid")));
|
|
1750
|
+
$("connectionDialog").addEventListener("click", e => { if (e.target === e.currentTarget) closeConnectionDialog(); });
|
|
1751
|
+
$("refreshOverviewBtn").addEventListener("click", e => refreshAll(e.currentTarget));
|
|
1752
|
+
$("refreshRuntimeBtn").addEventListener("click", e => withLoading(e.currentTarget, () => Promise.allSettled([loadRuntimeStatus(), loadServiceStatus(), loadVersions(), loadSessions()])));
|
|
1753
|
+
$("refreshClientsBtn").addEventListener("click", e => withLoading(e.currentTarget, () => loadClients().catch(err => toast("error", "Could not load clients", errorMessage(err)))));
|
|
1754
|
+
$("settingsForm").addEventListener("submit", saveSettings);
|
|
1755
|
+
$("settingsForm").addEventListener("input", () => { updateSettingsDirty(); });
|
|
1756
|
+
$("reloadSettingsBtn").addEventListener("click", () => loadSettings().catch(err => toast("error", "Could not load settings", errorMessage(err))));
|
|
1757
|
+
$("updateMcpBtn").addEventListener("click", e => triggerJob("/api/runtime/update/mcp", null, {
|
|
1758
|
+
expectRestart: true, label: "MCP update",
|
|
1759
|
+
confirm: { title: "Update the MCP server?", text: "The control plane restarts during the update. Coding assistants using MCP for i may need to reconnect.", ok: "Update and restart" }
|
|
1760
|
+
}, e.currentTarget));
|
|
1761
|
+
$("updateSkillsBtn").addEventListener("click", e => triggerJob("/api/runtime/update/skills", persistSkillsUpdateSettings(), { label: "Skills update" }, e.currentTarget));
|
|
1762
|
+
$("enableServiceBtn").addEventListener("click", e => triggerJob("/api/service/enable", null, { label: "Enable service" }, e.currentTarget));
|
|
1763
|
+
$("disableServiceBtn").addEventListener("click", e => triggerJob("/api/service/disable", null, { label: "Disable service" }, e.currentTarget));
|
|
1764
|
+
$("clientLogsClose").addEventListener("click", () => { $("clientLogsCard").hidden = true; });
|
|
1765
|
+
$("skillsRepoUrl").addEventListener("change", () => { persistSkillsUpdateSettings(); loadVersions().catch(() => {}); });
|
|
1766
|
+
$("skillsBranch").addEventListener("change", () => { persistSkillsUpdateSettings(); loadVersions().catch(() => {}); });
|
|
1767
|
+
window.addEventListener("beforeunload", e => { if (settingsDirty()) { e.preventDefault(); e.returnValue = ""; } });
|
|
1768
|
+
|
|
1769
|
+
applyTheme(currentTheme(), false);
|
|
1770
|
+
loadSkillsUpdateSettings();
|
|
1771
|
+
activateTab(initialTab());
|
|
1772
|
+
renderLogs({});
|
|
1773
|
+
setServiceState("offline", "Connecting", "Checking local service...");
|
|
1774
|
+
refreshAll().catch(err => toast("error", "Startup error", errorMessage(err)));
|
|
1775
|
+
startHeartbeat();
|
|
1776
|
+
</script>
|
|
1777
|
+
</body>
|
|
922
1778
|
</html>`;
|
|
923
1779
|
}
|
|
1780
|
+
function escapeAttribute(value) {
|
|
1781
|
+
return String(value)
|
|
1782
|
+
.replaceAll("&", "&")
|
|
1783
|
+
.replaceAll('"', """)
|
|
1784
|
+
.replaceAll("<", "<")
|
|
1785
|
+
.replaceAll(">", ">");
|
|
1786
|
+
}
|