83 lines
3.4 KiB
HTML
83 lines
3.4 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Simbiot · Roy — Claude Code (ttyd)</title>
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
||
<style>
|
||
html,body{margin:0;height:100%;background:#161b22;color:#e6edf3;font-family:system-ui,sans-serif}
|
||
#bar{display:flex;align-items:center;gap:10px;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d}
|
||
#bar b{font-size:14px}
|
||
#st{margin-left:auto;font-size:12px;color:#8b949e}
|
||
#st.ok{color:#3fb950}#st.err{color:#f85149}
|
||
#term{padding:6px;height:calc(100% - 42px);box-sizing:border-box}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="bar"><b>Roy · 178.104.52.234 · Claude Code (ttyd)</b>
|
||
<span id="st">подключение…</span></div>
|
||
<div id="term"></div>
|
||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
||
<script>
|
||
(async () => {
|
||
const term = new Terminal({cursorBlink:true, fontSize:14, theme:{background:'#161b22'}});
|
||
const fit = new FitAddon.FitAddon();
|
||
term.loadAddon(fit);
|
||
term.open(document.getElementById('term'));
|
||
fit.fit();
|
||
addEventListener('resize', () => fit.fit());
|
||
|
||
// токен — как у дашборда: из cookie или query этой страницы
|
||
let token = '';
|
||
const q = new URLSearchParams(location.search).get('token');
|
||
if (q) { token = q; document.cookie = 'simbiot_token=' + encodeURIComponent(q); }
|
||
else { const m = document.cookie.match(/(?:^|;\s*)simbiot_token=([^;]+)/); if (m) token = decodeURIComponent(m[1]); }
|
||
const wsUrl = (location.protocol==='https:'?'wss://':'ws://') + location.host
|
||
+ '/roy/ws' + (token ? '?token=' + encodeURIComponent(token) : '');
|
||
const st = document.getElementById('st');
|
||
|
||
// ttyd-протокол: первый JSON-фрейм {AuthToken:'', Columns:, Rows:}
|
||
const ws = new WebSocket(wsUrl);
|
||
ws.binaryType = 'arraybuffer';
|
||
let initSent = false;
|
||
ws.onopen = () => {
|
||
ws.send(JSON.stringify({AuthToken:'', Columns:term.cols, Rows:term.rows}));
|
||
initSent = true;
|
||
st.textContent = 'онлайн'; st.className = 'ok';
|
||
term.focus();
|
||
};
|
||
ws.onmessage = ev => {
|
||
const d = new Uint8Array(ev.data instanceof ArrayBuffer ? ev.data : new TextEncoder().encode(ev.data));
|
||
if (!d.length) return;
|
||
if (d[0] === 0x00) { // OUTPUT: печатаем как есть
|
||
term.write(new Uint8Array(d.buffer, 1));
|
||
} else if (d[0] === 0x01) { // PING
|
||
const p = new Uint8Array(2); p[0] = 0x02; ws.send(p);
|
||
}
|
||
};
|
||
ws.onclose = () => {
|
||
st.textContent = initSent ? 'связь потеряна — обновите страницу' : 'нет доступа (токен?)';
|
||
st.className = 'err';
|
||
};
|
||
term.onData(data => {
|
||
// INPUT: 0x00 + utf-8
|
||
const p = new Uint8Array(1 + data.length);
|
||
p[0] = 0x00;
|
||
for (let i = 0; i < data.length; i++) p[1+i] = data.charCodeAt(i) & 0xFF;
|
||
ws.send(p);
|
||
});
|
||
// resize: WINDOW_RESIZE 0x02 + json {Columns,Rows}
|
||
term.onResize(({cols, rows}) => {
|
||
if (ws.readyState !== 1) return;
|
||
const j = JSON.stringify({Columns:cols, Rows:rows});
|
||
const p = new Uint8Array(1 + j.length);
|
||
p[0] = 0x02;
|
||
for (let i = 0; i < j.length; i++) p[1+i] = j.charCodeAt(i);
|
||
ws.send(p);
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|