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
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live 5250 view page. One self-contained HTML document: it reads the
|
|
3
|
+
* token and session from its own URL, follows the screen over server-sent
|
|
4
|
+
* events and sends the person's keys and clicks back.
|
|
5
|
+
*
|
|
6
|
+
* The screen fills the available space (fit to width or height), so the page
|
|
7
|
+
* works embedded in a product at any size. URL params configure the chrome:
|
|
8
|
+
* view=screen | bare=1 show only the screen
|
|
9
|
+
* chrome=full show every control
|
|
10
|
+
* header=0 keys=0 hint=0 status=0 hide individual parts
|
|
11
|
+
* The choice is remembered per browser; a param always wins.
|
|
12
|
+
*
|
|
13
|
+
* The client script below must not contain backticks or "${", because it
|
|
14
|
+
* sits inside a template literal.
|
|
15
|
+
*/
|
|
16
|
+
export function renderLivePage() {
|
|
17
|
+
return `<!doctype html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="utf-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
22
|
+
<title>Live 5250</title>
|
|
23
|
+
<style>
|
|
24
|
+
:root {
|
|
25
|
+
/* Chrome neutrals (a tuned ramp, not pure grey). */
|
|
26
|
+
--bg: #000000; --panel: #0c110e; --elevated: #141b16; --border: rgba(255,255,255,.09);
|
|
27
|
+
--text: #dfe9e2; --text-2: #94a89c; --text-3: #5f7268;
|
|
28
|
+
--ui-accent: #35e06a; --warn: #ffc24d; --danger: #ff6a60;
|
|
29
|
+
/* 5250 base colours, tuned to a real green-screen / ACS palette. */
|
|
30
|
+
--green: #2fe02f; --white: #ffffff; --red: #ff564d; --turquoise: #2fd4d4; --yellow: #ffd93b;
|
|
31
|
+
--pink: #ff5fd0; --blue: #6f8dff;
|
|
32
|
+
--cursor: #ffffff;
|
|
33
|
+
--r-sm: 6px; --r-md: 8px;
|
|
34
|
+
--lh: 1.25; /* line spacing (row height as a multiple of the font size) */
|
|
35
|
+
}
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
html, body { height: 100%; }
|
|
38
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 13px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; overflow: hidden; }
|
|
39
|
+
|
|
40
|
+
#app { height: 100%; display: flex; flex-direction: column; }
|
|
41
|
+
|
|
42
|
+
/* Top bar (chrome). */
|
|
43
|
+
header { flex: none; display: flex; align-items: center; gap: 10px; height: 38px; padding: 0 12px; background: var(--panel); border-bottom: 1px solid var(--border); }
|
|
44
|
+
.eyebrow { font: 600 11px/1 ui-monospace, "Cascadia Mono", Consolas, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--ui-accent); }
|
|
45
|
+
.spacer { flex: 1 1 auto; }
|
|
46
|
+
select { font: 500 12px/1 system-ui, sans-serif; color: var(--text); background: var(--elevated); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 4px 8px; cursor: pointer; }
|
|
47
|
+
select:focus-visible { outline: none; border-color: var(--ui-accent); }
|
|
48
|
+
.badge { padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border); color: var(--text-3); font: 500 11px/1.5 system-ui, sans-serif; white-space: nowrap; }
|
|
49
|
+
.badge.agent { color: var(--yellow); border-color: color-mix(in srgb, var(--yellow) 45%, transparent); }
|
|
50
|
+
.badge.person { color: var(--ui-accent); border-color: color-mix(in srgb, var(--ui-accent) 45%, transparent); }
|
|
51
|
+
#conn { color: var(--text-3); font: 500 11px/1 ui-monospace, "Cascadia Mono", Consolas, monospace; }
|
|
52
|
+
|
|
53
|
+
/* The stage holds the screen and scales it to fill. */
|
|
54
|
+
#stage { flex: 1 1 auto; min-height: 0; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 16px; background: var(--bg); }
|
|
55
|
+
#wrap { position: relative; outline: none; border: 1px solid var(--border); border-radius: var(--r-md); background: #000; padding: 4px 6px; }
|
|
56
|
+
#wrap:focus-visible { border-color: color-mix(in srgb, var(--ui-accent) 55%, var(--border)); }
|
|
57
|
+
/* No padding on #screen: the cursor is positioned from its edge, so text and cursor must share that origin. */
|
|
58
|
+
#screen { position: relative; font-family: "Cascadia Mono", Consolas, "DejaVu Sans Mono", Menlo, monospace; font-size: 16px; line-height: var(--lh); white-space: pre; -webkit-user-select: text; user-select: text; }
|
|
59
|
+
.row { height: calc(1em * var(--lh)); }
|
|
60
|
+
/* Cells are inline-block the full line height so reverse-video blocks fill solidly. */
|
|
61
|
+
.row span { cursor: text; display: inline-block; height: calc(1em * var(--lh)); line-height: calc(1em * var(--lh)); vertical-align: top; }
|
|
62
|
+
.c-green { color: var(--green); } .c-white { color: var(--white); } .c-red { color: var(--red); }
|
|
63
|
+
.c-turquoise { color: var(--turquoise); } .c-yellow { color: var(--yellow); } .c-pink { color: var(--pink); } .c-blue { color: var(--blue); }
|
|
64
|
+
.rev { background: currentColor; }
|
|
65
|
+
.rev > i { color: #000; font-style: normal; }
|
|
66
|
+
.ul { text-decoration: underline; }
|
|
67
|
+
/* Cursor: white underline (overwrite) or white block (insert), blinking from script. */
|
|
68
|
+
#cursor { position: absolute; width: .6em; height: 2px; margin-top: calc(1em * var(--lh) - 4px); background: var(--cursor); pointer-events: none; z-index: 3; box-shadow: 0 0 4px var(--cursor); }
|
|
69
|
+
#cursor.insert { height: calc(1em * var(--lh)); margin-top: 0; background: #fff; mix-blend-mode: difference; box-shadow: none; }
|
|
70
|
+
#cursor.blink-off { opacity: 0; }
|
|
71
|
+
|
|
72
|
+
/* Bottom bar (chrome): status, keys, hint. */
|
|
73
|
+
footer { flex: none; background: var(--panel); border-top: 1px solid var(--border); padding: 8px 12px 10px; display: flex; flex-direction: column; gap: 8px; }
|
|
74
|
+
#status { display: flex; gap: 6px 14px; flex-wrap: wrap; justify-content: center; color: var(--text-3); font: 500 11px/1.4 ui-monospace, "Cascadia Mono", Consolas, monospace; }
|
|
75
|
+
#status .on { color: var(--warn); }
|
|
76
|
+
#error { color: var(--danger); min-height: 0; font-size: 11px; text-align: center; }
|
|
77
|
+
#error:empty { display: none; }
|
|
78
|
+
#keys { display: flex; flex-direction: column; gap: 5px; align-items: center; }
|
|
79
|
+
.keyrow { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; max-width: 1180px; }
|
|
80
|
+
#keys button { font: 500 12px/1 ui-monospace, "Cascadia Mono", Consolas, monospace; color: var(--text-2); background: var(--elevated); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 5px 9px; min-width: 42px; cursor: pointer; transition: border-color .1s, color .1s, background .1s, transform .04s; }
|
|
81
|
+
#keys button:hover { border-color: color-mix(in srgb, var(--ui-accent) 55%, var(--border)); color: var(--text); }
|
|
82
|
+
#keys button:active { transform: translateY(1px); }
|
|
83
|
+
#keys button:focus-visible { outline: none; border-color: var(--ui-accent); }
|
|
84
|
+
.keyrow.special button { color: var(--text); background: #18211b; }
|
|
85
|
+
#keys button.pulse { border-color: var(--warn); color: var(--warn); animation: pulse 1s ease-in-out infinite; }
|
|
86
|
+
@keyframes pulse { 50% { background: #3a2e14; } }
|
|
87
|
+
.hint { color: var(--text-3); font-size: 11.5px; line-height: 1.5; text-align: center; max-width: 960px; margin: 2px auto 0; }
|
|
88
|
+
.hint b { color: var(--text-2); font-weight: 600; }
|
|
89
|
+
|
|
90
|
+
/* Chrome toggles (a param or the Controls button). */
|
|
91
|
+
body.hide-header header { display: none; }
|
|
92
|
+
body.hide-keys #keys { display: none; }
|
|
93
|
+
body.hide-hint .hint { display: none; }
|
|
94
|
+
body.hide-status #status { display: none; }
|
|
95
|
+
body.bare #stage { padding: 6px; }
|
|
96
|
+
body.bare #wrap { border: none; border-radius: 0; padding: 0; }
|
|
97
|
+
footer:empty, body.hide-keys.hide-hint.hide-status footer { display: none; }
|
|
98
|
+
|
|
99
|
+
/* Floating controls toggle. */
|
|
100
|
+
#display-toggle { position: fixed; top: 8px; right: 8px; z-index: 30; font: 500 12px/1 system-ui, sans-serif; color: var(--text-2); background: color-mix(in srgb, var(--panel) 85%, transparent); border: 1px solid var(--border); border-radius: 999px; padding: 5px 11px; cursor: pointer; backdrop-filter: blur(6px); opacity: .5; transition: opacity .12s, border-color .12s, color .12s; }
|
|
101
|
+
#display-toggle:hover { opacity: 1; border-color: color-mix(in srgb, var(--ui-accent) 55%, var(--border)); color: var(--text); }
|
|
102
|
+
|
|
103
|
+
/* Floating notices (they float above the plane, so a soft shadow is earned). */
|
|
104
|
+
#state-chip { position: fixed; top: 8px; left: 8px; z-index: 22; padding: 4px 11px; border-radius: 999px; font: 500 12px/1.4 system-ui, sans-serif; background: #241a06; color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); display: none; box-shadow: 0 6px 20px rgba(0,0,0,.4); }
|
|
105
|
+
#state-chip.show { display: block; }
|
|
106
|
+
#working { position: fixed; left: 50%; bottom: 54px; transform: translateX(-50%); z-index: 24; display: none; padding: 6px 13px; border-radius: 999px; font: 500 12px/1.4 system-ui, sans-serif; background: #0f2419; color: var(--ui-accent); border: 1px solid color-mix(in srgb, var(--ui-accent) 45%, transparent); box-shadow: 0 6px 20px rgba(0,0,0,.4); }
|
|
107
|
+
#working.show { display: block; }
|
|
108
|
+
#working::before { content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--ui-accent); margin-right: 8px; vertical-align: middle; animation: pulse2 .8s ease-in-out infinite; }
|
|
109
|
+
@keyframes pulse2 { 50% { opacity: .2; } }
|
|
110
|
+
#toast { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 26; max-width: 92%; padding: 8px 15px; border-radius: var(--r-md); font: 500 13px/1.45 system-ui, sans-serif; display: none; box-shadow: 0 8px 26px rgba(0,0,0,.45); }
|
|
111
|
+
#toast.show { display: block; }
|
|
112
|
+
#toast.err { background: #221212; color: #ffdede; border: 1px solid color-mix(in srgb, var(--danger) 55%, transparent); }
|
|
113
|
+
#toast.info { background: #0f2419; color: #d7f5e2; border: 1px solid color-mix(in srgb, var(--ui-accent) 45%, transparent); }
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<button id="display-toggle" title="Show or hide the controls">Controls</button>
|
|
118
|
+
<div id="state-chip" aria-live="polite"></div>
|
|
119
|
+
<div id="working">Waiting for the host…</div>
|
|
120
|
+
<div id="toast" role="status"></div>
|
|
121
|
+
<div id="app">
|
|
122
|
+
<header>
|
|
123
|
+
<span class="eyebrow">5250</span>
|
|
124
|
+
<select id="session" aria-label="Session" style="display:none"></select>
|
|
125
|
+
<span class="spacer"></span>
|
|
126
|
+
<span id="actor" class="badge">Waiting</span>
|
|
127
|
+
<span id="conn" class="badge"></span>
|
|
128
|
+
</header>
|
|
129
|
+
<div id="stage">
|
|
130
|
+
<div id="wrap" tabindex="0" aria-label="5250 screen. Click here and type.">
|
|
131
|
+
<div id="screen"><div id="cursor"></div></div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<footer>
|
|
135
|
+
<div id="status"></div>
|
|
136
|
+
<div id="error" role="status"></div>
|
|
137
|
+
<div id="keys"></div>
|
|
138
|
+
<p class="hint">Click an underlined field and type. The arrows move the cursor; <b>Page Up / Page Down (or the mouse wheel) scroll</b>. Enter, F1-F24, Tab, Home, End, Insert, Delete work as in an emulator. Esc is Attn, Shift+Esc is SysReq, Ctrl alone is Reset. A click moves the cursor (a key legend like F3=Exit presses it), a double click presses Enter, and you can drag to select text. While you type, the agent waits.</p>
|
|
139
|
+
</footer>
|
|
140
|
+
</div>
|
|
141
|
+
<script>
|
|
142
|
+
(function () {
|
|
143
|
+
var params = new URLSearchParams(location.search);
|
|
144
|
+
var token = params.get('t') || '';
|
|
145
|
+
var current = params.get('s') || '';
|
|
146
|
+
var screenEl = document.getElementById('screen');
|
|
147
|
+
var cursorEl = document.getElementById('cursor');
|
|
148
|
+
var wrap = document.getElementById('wrap');
|
|
149
|
+
var stage = document.getElementById('stage');
|
|
150
|
+
var statusEl = document.getElementById('status');
|
|
151
|
+
var errorEl = document.getElementById('error');
|
|
152
|
+
var actorEl = document.getElementById('actor');
|
|
153
|
+
var connEl = document.getElementById('conn');
|
|
154
|
+
var sessionEl = document.getElementById('session');
|
|
155
|
+
var toastEl = document.getElementById('toast');
|
|
156
|
+
var stateChip = document.getElementById('state-chip');
|
|
157
|
+
var workingEl = document.getElementById('working');
|
|
158
|
+
var resetBtn = null;
|
|
159
|
+
var source = null;
|
|
160
|
+
var last = null;
|
|
161
|
+
var pending = '';
|
|
162
|
+
var flushTimer = null;
|
|
163
|
+
var queue = Promise.resolve();
|
|
164
|
+
|
|
165
|
+
setInterval(function () { if (cursorEl) cursorEl.classList.toggle('blink-off'); }, 520);
|
|
166
|
+
|
|
167
|
+
var inflight = 0, workTimer = null;
|
|
168
|
+
function workStart() {
|
|
169
|
+
inflight += 1;
|
|
170
|
+
if (!workTimer) workTimer = setTimeout(function () { workTimer = null; if (inflight > 0) workingEl.classList.add('show'); }, 300);
|
|
171
|
+
}
|
|
172
|
+
function workEnd() {
|
|
173
|
+
inflight = Math.max(0, inflight - 1);
|
|
174
|
+
if (inflight === 0) { if (workTimer) { clearTimeout(workTimer); workTimer = null; } workingEl.classList.remove('show'); }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var toastTimer = null;
|
|
178
|
+
function showToast(msg, kind) {
|
|
179
|
+
if (!msg) { toastEl.className = ''; return; }
|
|
180
|
+
toastEl.textContent = msg;
|
|
181
|
+
toastEl.className = 'show ' + (kind || 'err');
|
|
182
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
183
|
+
toastTimer = setTimeout(function () { toastEl.className = ''; }, 4500);
|
|
184
|
+
}
|
|
185
|
+
function friendlyError(e) {
|
|
186
|
+
e = e || 'Something went wrong';
|
|
187
|
+
if (/not in an input field/i.test(e)) return 'The cursor is not in an input field. Click an underlined field, then type.';
|
|
188
|
+
if (/keyboard is locked/i.test(e)) return 'The host is still working (X System). Wait a moment, then try again.';
|
|
189
|
+
if (/input is inhibited|RESET key/i.test(e)) return 'Input is blocked by a host message. Press Reset (the Ctrl key, or the Reset button).';
|
|
190
|
+
if (/read-only/i.test(e)) return 'This connection is read-only, so keys sent to the host are blocked.';
|
|
191
|
+
return e;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Configurable chrome.
|
|
195
|
+
var CHROME_KEY = 'tn5250.chrome';
|
|
196
|
+
function truthyParam(name, def) {
|
|
197
|
+
var v = params.get(name);
|
|
198
|
+
if (v === null) return def;
|
|
199
|
+
return !(v === '0' || v === 'false' || v === 'no' || v === 'off');
|
|
200
|
+
}
|
|
201
|
+
function applyChrome(c) {
|
|
202
|
+
document.body.classList.toggle('hide-header', !c.header);
|
|
203
|
+
document.body.classList.toggle('hide-keys', !c.keys);
|
|
204
|
+
document.body.classList.toggle('hide-hint', !c.hint);
|
|
205
|
+
document.body.classList.toggle('hide-status', !c.status);
|
|
206
|
+
document.body.classList.toggle('bare', !!c.bare);
|
|
207
|
+
fit();
|
|
208
|
+
}
|
|
209
|
+
function defaultChrome() {
|
|
210
|
+
var screenOnly = params.get('view') === 'screen' || truthyParam('bare', false);
|
|
211
|
+
var base = screenOnly
|
|
212
|
+
? { header: false, keys: false, hint: false, status: false, bare: true }
|
|
213
|
+
: { header: true, keys: true, hint: true, status: true, bare: false };
|
|
214
|
+
if (params.get('chrome') === 'full') base = { header: true, keys: true, hint: true, status: true, bare: false };
|
|
215
|
+
base.header = truthyParam('header', truthyParam('bar', base.header));
|
|
216
|
+
base.keys = truthyParam('keys', base.keys);
|
|
217
|
+
base.hint = truthyParam('hint', base.hint);
|
|
218
|
+
base.status = truthyParam('status', base.status);
|
|
219
|
+
return base;
|
|
220
|
+
}
|
|
221
|
+
var chrome;
|
|
222
|
+
var hasParam = ['view', 'bare', 'chrome', 'header', 'bar', 'keys', 'hint', 'status'].some(function (n) { return params.get(n) !== null; });
|
|
223
|
+
try {
|
|
224
|
+
var saved = hasParam ? null : window.localStorage.getItem(CHROME_KEY);
|
|
225
|
+
chrome = saved ? JSON.parse(saved) : defaultChrome();
|
|
226
|
+
} catch (e) { chrome = defaultChrome(); }
|
|
227
|
+
applyChrome(chrome);
|
|
228
|
+
document.getElementById('display-toggle').addEventListener('click', function () {
|
|
229
|
+
var full = chrome.header && chrome.keys && chrome.hint && chrome.status && !chrome.bare;
|
|
230
|
+
chrome = full
|
|
231
|
+
? { header: false, keys: false, hint: false, status: false, bare: true }
|
|
232
|
+
: { header: true, keys: true, hint: true, status: true, bare: false };
|
|
233
|
+
applyChrome(chrome);
|
|
234
|
+
try { window.localStorage.setItem(CHROME_KEY, JSON.stringify(chrome)); } catch (e) {}
|
|
235
|
+
wrap.focus();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Fit the 80x24 (or 27x132) grid to fill the stage: scale the font so the
|
|
239
|
+
// grid fills whichever of width or height runs out first. The embedded 3270
|
|
240
|
+
// font gives one fixed character shape on every machine, so the grid is the
|
|
241
|
+
// font's own natural advance (no letter-spacing distortion) and is identical
|
|
242
|
+
// everywhere.
|
|
243
|
+
var CELL = 0.5; // the screen font's natural character-width ratio (measured)
|
|
244
|
+
var LINE = 1.25; // keep in sync with the --lh CSS variable
|
|
245
|
+
var charRatioCache = 0;
|
|
246
|
+
function charRatio() {
|
|
247
|
+
if (charRatioCache) return charRatioCache;
|
|
248
|
+
var probe = document.createElement('span');
|
|
249
|
+
probe.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;letter-spacing:normal;font:100px "Cascadia Mono",Consolas,"DejaVu Sans Mono",Menlo,monospace;';
|
|
250
|
+
probe.textContent = '0000000000';
|
|
251
|
+
document.body.appendChild(probe);
|
|
252
|
+
var w = probe.getBoundingClientRect().width / 10 / 100;
|
|
253
|
+
document.body.removeChild(probe);
|
|
254
|
+
charRatioCache = w || 0.5;
|
|
255
|
+
CELL = charRatioCache;
|
|
256
|
+
return charRatioCache;
|
|
257
|
+
}
|
|
258
|
+
function fit() {
|
|
259
|
+
charRatio();
|
|
260
|
+
var cols = (last && last.cols) || 80, rows = (last && last.rows) || 24;
|
|
261
|
+
var availW = stage.clientWidth, availH = stage.clientHeight;
|
|
262
|
+
if (availW < 10 || availH < 10) return;
|
|
263
|
+
// Leave room for the wrap border (1px each side) and its padding.
|
|
264
|
+
availW -= 16; availH -= 12;
|
|
265
|
+
var f = Math.min(availW / (cols * CELL), availH / (rows * LINE));
|
|
266
|
+
f = Math.max(7, Math.floor(f * 100) / 100);
|
|
267
|
+
screenEl.style.fontSize = f + 'px';
|
|
268
|
+
}
|
|
269
|
+
// Re-measure and re-fit once the embedded font has actually loaded.
|
|
270
|
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(function () { charRatioCache = 0; fit(); });
|
|
271
|
+
var fitTimer = null;
|
|
272
|
+
window.addEventListener('resize', function () { if (fitTimer) clearTimeout(fitTimer); fitTimer = setTimeout(fit, 60); });
|
|
273
|
+
|
|
274
|
+
function api(path) { return path + (path.indexOf('?') >= 0 ? '&' : '?') + 't=' + encodeURIComponent(token) + '&s=' + encodeURIComponent(current); }
|
|
275
|
+
|
|
276
|
+
function send(input, quiet) {
|
|
277
|
+
queue = queue.then(function () {
|
|
278
|
+
workStart();
|
|
279
|
+
return fetch(api('/api/input'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input) })
|
|
280
|
+
.then(function (r) { return r.json(); })
|
|
281
|
+
.then(function (res) {
|
|
282
|
+
if (res.ok) { errorEl.textContent = res.note || ''; if (res.note) showToast(res.note, 'info'); }
|
|
283
|
+
else if (!quiet) { var m = friendlyError(res.error); errorEl.textContent = m; showToast(m, 'err'); }
|
|
284
|
+
})
|
|
285
|
+
.catch(function (err) { if (!quiet) { errorEl.textContent = String(err); showToast(String(err), 'err'); } })
|
|
286
|
+
.then(function () { workEnd(); });
|
|
287
|
+
});
|
|
288
|
+
return queue;
|
|
289
|
+
}
|
|
290
|
+
function flushText() {
|
|
291
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
292
|
+
if (!pending) return;
|
|
293
|
+
var text = pending; pending = '';
|
|
294
|
+
send({ text: text });
|
|
295
|
+
}
|
|
296
|
+
function key(name, quiet) { flushText(); send({ keys: name }, quiet); }
|
|
297
|
+
|
|
298
|
+
function render(view) {
|
|
299
|
+
if (!view || view.missing) {
|
|
300
|
+
screenEl.querySelectorAll('.row').forEach(function (n) { n.remove(); });
|
|
301
|
+
statusEl.textContent = current ? 'The session ' + current + ' is not open.' : 'No 5250 session is open yet.';
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
var sizeChanged = !last || last.cols !== view.cols || last.rows !== view.rows;
|
|
305
|
+
last = view;
|
|
306
|
+
var frag = document.createDocumentFragment();
|
|
307
|
+
(view.runs || []).forEach(function (runs, r) {
|
|
308
|
+
var row = document.createElement('div');
|
|
309
|
+
row.className = 'row';
|
|
310
|
+
var col = 1;
|
|
311
|
+
runs.forEach(function (run) {
|
|
312
|
+
var span = document.createElement('span');
|
|
313
|
+
span.className = 'c-' + run.color + (run.underline ? ' ul' : '') + (run.reverse ? ' rev' : '');
|
|
314
|
+
span.dataset.row = String(r + 1);
|
|
315
|
+
span.dataset.col = String(col);
|
|
316
|
+
if (run.reverse) { var i = document.createElement('i'); i.textContent = run.text; span.appendChild(i); }
|
|
317
|
+
else span.textContent = run.text;
|
|
318
|
+
col += run.text.length;
|
|
319
|
+
row.appendChild(span);
|
|
320
|
+
});
|
|
321
|
+
frag.appendChild(row);
|
|
322
|
+
});
|
|
323
|
+
screenEl.querySelectorAll('.row').forEach(function (n) { n.remove(); });
|
|
324
|
+
screenEl.appendChild(frag);
|
|
325
|
+
cursorEl.style.left = ((view.cursor.col - 1) * CELL) + 'em';
|
|
326
|
+
cursorEl.style.top = ((view.cursor.row - 1) * LINE) + 'em';
|
|
327
|
+
cursorEl.style.width = CELL + 'em';
|
|
328
|
+
cursorEl.className = (view.insertMode ? 'insert' : '') + (cursorEl.classList.contains('blink-off') ? ' blink-off' : '');
|
|
329
|
+
if (sizeChanged) fit();
|
|
330
|
+
var blocked = view.inputInhibited ? 'Input blocked — press Reset (Ctrl, or the Reset button)'
|
|
331
|
+
: (view.keyboardLocked ? 'Host busy — X System' : '');
|
|
332
|
+
stateChip.textContent = blocked;
|
|
333
|
+
stateChip.classList.toggle('show', !!blocked);
|
|
334
|
+
if (resetBtn) resetBtn.classList.toggle('pulse', !!view.inputInhibited);
|
|
335
|
+
var parts = [];
|
|
336
|
+
parts.push(view.connected ? 'Connected' : 'Disconnected' + (view.closeReason ? ': ' + view.closeReason : ''));
|
|
337
|
+
parts.push('R' + view.cursor.row + ' C' + view.cursor.col);
|
|
338
|
+
if (view.keyboardLocked && !view.inputInhibited) parts.push('<span class="on">X System</span>');
|
|
339
|
+
if (view.inputInhibited) parts.push('<span class="on">Input inhibited</span>');
|
|
340
|
+
if (view.insertMode) parts.push('<span class="on">Insert</span>');
|
|
341
|
+
if (view.messageWaiting) parts.push('<span class="on">Message waiting</span>');
|
|
342
|
+
if (view.lastKey) parts.push('Last ' + view.lastKey);
|
|
343
|
+
statusEl.innerHTML = parts.join(' · ');
|
|
344
|
+
var actor = view.lastActor === 'person' ? 'person' : view.lastActor === 'agent' ? 'agent' : '';
|
|
345
|
+
actorEl.className = 'badge ' + actor;
|
|
346
|
+
actorEl.textContent = actor === 'agent' ? 'Agent acted last' : actor === 'person' ? 'You acted last' : 'Waiting';
|
|
347
|
+
connEl.textContent = (view.connectionName || '') + ' ' + view.rows + 'x' + view.cols + (view.host ? ' ' + view.host : '');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderSessions(list) {
|
|
351
|
+
var names = list.map(function (s) { return s.connectionName; });
|
|
352
|
+
if (!current && names.length) { current = names[0]; follow(); }
|
|
353
|
+
sessionEl.innerHTML = '';
|
|
354
|
+
if (current && names.indexOf(current) < 0) names.unshift(current);
|
|
355
|
+
names.forEach(function (n) {
|
|
356
|
+
var o = document.createElement('option'); o.value = n; o.textContent = n; if (n === current) o.selected = true; sessionEl.appendChild(o);
|
|
357
|
+
});
|
|
358
|
+
sessionEl.style.display = names.length > 1 ? '' : 'none';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function follow() {
|
|
362
|
+
if (source) source.close();
|
|
363
|
+
var url = '/api/events?t=' + encodeURIComponent(token) + '&s=' + encodeURIComponent(current);
|
|
364
|
+
source = new EventSource(url);
|
|
365
|
+
source.addEventListener('screen', function (e) { render(JSON.parse(e.data)); });
|
|
366
|
+
source.addEventListener('sessions', function (e) { renderSessions(JSON.parse(e.data)); });
|
|
367
|
+
var u = new URL(location.href); u.searchParams.set('s', current); history.replaceState(null, '', u.toString());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
sessionEl.addEventListener('change', function () { current = sessionEl.value; follow(); });
|
|
371
|
+
|
|
372
|
+
var clickTimer = null;
|
|
373
|
+
function cellFrom(ev) {
|
|
374
|
+
var target = ev.target.closest ? ev.target.closest('span[data-row]') : null;
|
|
375
|
+
if (!target) return null;
|
|
376
|
+
var range = document.caretRangeFromPoint ? document.caretRangeFromPoint(ev.clientX, ev.clientY) : null;
|
|
377
|
+
var offset = range && range.startContainer && target.contains(range.startContainer) ? range.startOffset : 0;
|
|
378
|
+
if (!range && document.caretPositionFromPoint) {
|
|
379
|
+
var pos = document.caretPositionFromPoint(ev.clientX, ev.clientY);
|
|
380
|
+
if (pos && target.contains(pos.offsetNode)) offset = pos.offset;
|
|
381
|
+
}
|
|
382
|
+
return { row: Number(target.dataset.row), col: Number(target.dataset.col) + offset };
|
|
383
|
+
}
|
|
384
|
+
screenEl.addEventListener('click', function (ev) {
|
|
385
|
+
if (window.getSelection && String(window.getSelection()).replace(/\\s+/g, '').length) return;
|
|
386
|
+
wrap.focus();
|
|
387
|
+
var cell = cellFrom(ev);
|
|
388
|
+
if (!cell) return;
|
|
389
|
+
if (clickTimer) clearTimeout(clickTimer);
|
|
390
|
+
clickTimer = setTimeout(function () { clickTimer = null; flushText(); send({ click: cell }); }, 260);
|
|
391
|
+
});
|
|
392
|
+
screenEl.addEventListener('dblclick', function (ev) {
|
|
393
|
+
var cell = cellFrom(ev);
|
|
394
|
+
if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
|
|
395
|
+
if (cell) { flushText(); send({ click: { row: cell.row, col: cell.col, doubleClick: true } }); }
|
|
396
|
+
});
|
|
397
|
+
var lastWheel = 0;
|
|
398
|
+
wrap.addEventListener('wheel', function (ev) {
|
|
399
|
+
if (!last || !last.connected || last.keyboardLocked || last.inputInhibited) return;
|
|
400
|
+
ev.preventDefault();
|
|
401
|
+
var now = Date.now();
|
|
402
|
+
if (now - lastWheel < 350) return;
|
|
403
|
+
lastWheel = now;
|
|
404
|
+
key(ev.deltaY > 0 ? 'PageDown' : 'PageUp', true);
|
|
405
|
+
}, { passive: false });
|
|
406
|
+
|
|
407
|
+
var ctrlAlone = false;
|
|
408
|
+
wrap.addEventListener('keydown', function (ev) {
|
|
409
|
+
if (ev.key === 'Control') { ctrlAlone = true; return; }
|
|
410
|
+
ctrlAlone = false;
|
|
411
|
+
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
412
|
+
var k = ev.key;
|
|
413
|
+
var name = null;
|
|
414
|
+
if (/^F([1-9]|1[0-2])$/.test(k)) name = 'F' + (Number(k.substring(1)) + (ev.shiftKey ? 12 : 0));
|
|
415
|
+
else if (k === 'Enter') name = 'Enter';
|
|
416
|
+
else if (k === 'Escape') name = ev.shiftKey ? 'SysReq' : 'Attn';
|
|
417
|
+
else if (k === 'Tab') name = ev.shiftKey ? 'BackTab' : 'Tab';
|
|
418
|
+
else if (k === 'ArrowUp') name = 'Up';
|
|
419
|
+
else if (k === 'ArrowDown') name = 'Down';
|
|
420
|
+
else if (k === 'ArrowLeft') name = 'Left';
|
|
421
|
+
else if (k === 'ArrowRight') name = 'Right';
|
|
422
|
+
else if (k === 'Home') name = 'Home';
|
|
423
|
+
else if (k === 'End') name = 'End';
|
|
424
|
+
else if (k === 'Insert') name = 'Insert';
|
|
425
|
+
else if (k === 'Delete') name = 'Delete';
|
|
426
|
+
else if (k === 'Backspace') name = 'Left Delete';
|
|
427
|
+
else if (k === 'PageUp') name = 'PageUp';
|
|
428
|
+
else if (k === 'PageDown') name = 'PageDown';
|
|
429
|
+
else if (ev.code === 'NumpadAdd') name = 'Field+';
|
|
430
|
+
else if (ev.code === 'NumpadSubtract') name = 'Field-';
|
|
431
|
+
if (name) { ev.preventDefault(); key(name); return; }
|
|
432
|
+
if (k.length === 1) {
|
|
433
|
+
ev.preventDefault();
|
|
434
|
+
pending += k;
|
|
435
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
436
|
+
flushTimer = setTimeout(flushText, 40);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
wrap.addEventListener('paste', function (ev) {
|
|
440
|
+
var text = (ev.clipboardData && ev.clipboardData.getData('text')) || '';
|
|
441
|
+
text = text.split(/[\\r\\n]/)[0];
|
|
442
|
+
if (!text) return;
|
|
443
|
+
ev.preventDefault();
|
|
444
|
+
pending += text;
|
|
445
|
+
flushText();
|
|
446
|
+
});
|
|
447
|
+
wrap.addEventListener('keyup', function (ev) {
|
|
448
|
+
if (ev.key === 'Control' && ctrlAlone) { ctrlAlone = false; key('Reset'); }
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
var keysEl = document.getElementById('keys');
|
|
452
|
+
function keyRow(names, cls) {
|
|
453
|
+
var rowEl = document.createElement('div');
|
|
454
|
+
rowEl.className = 'keyrow' + (cls ? ' ' + cls : '');
|
|
455
|
+
names.forEach(function (name) {
|
|
456
|
+
var b = document.createElement('button');
|
|
457
|
+
b.textContent = name;
|
|
458
|
+
b.addEventListener('click', function () { key(name); wrap.focus(); });
|
|
459
|
+
if (name === 'Reset') resetBtn = b;
|
|
460
|
+
rowEl.appendChild(b);
|
|
461
|
+
});
|
|
462
|
+
keysEl.appendChild(rowEl);
|
|
463
|
+
}
|
|
464
|
+
keyRow(['Enter', 'Attn', 'SysReq', 'Reset', 'Field+', 'Field-', 'FieldExit', 'Dup', 'EraseEOF', 'EraseInput', 'Clear', 'Help', 'Print', 'PageUp', 'PageDown'], 'special');
|
|
465
|
+
keyRow(Array.from({ length: 12 }, function (_, i) { return 'F' + (i + 1); }));
|
|
466
|
+
keyRow(Array.from({ length: 12 }, function (_, i) { return 'F' + (i + 13); }));
|
|
467
|
+
|
|
468
|
+
fetch('/api/sessions?t=' + encodeURIComponent(token)).then(function (r) { return r.json(); }).then(function (res) {
|
|
469
|
+
var hadCurrent = Boolean(current);
|
|
470
|
+
renderSessions(res.sessions || []);
|
|
471
|
+
if (hadCurrent || !current) follow();
|
|
472
|
+
if (!current) statusEl.textContent = 'No 5250 session is open yet. Ask the agent to connect with ibmi.tn5250.connect.';
|
|
473
|
+
}).catch(function (err) { errorEl.textContent = String(err); });
|
|
474
|
+
fit();
|
|
475
|
+
wrap.focus();
|
|
476
|
+
})();
|
|
477
|
+
</script>
|
|
478
|
+
</body>
|
|
479
|
+
</html>`;
|
|
480
|
+
}
|