thinknagent 0.1.19 → 0.1.22
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 +87 -279
- package/bin/thinknagent.js +26 -18
- package/bin/thinkncollab-mcp.js +387 -0
- package/lib/agent.js +24 -6
- package/lib/daemon.js +5 -4
- package/lib/e2ee.js +45 -0
- package/lib/logwatcher.js +46 -29
- package/lib/metrics.js +15 -4
- package/lib/shell.js +14 -0
- package/package.json +4 -3
- package/install/setup.sh +0 -90
- package/lib/app.js +0 -327
- package/thinknagent.sh +0 -560
package/lib/metrics.js
CHANGED
|
@@ -2,25 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
const si = require('systeminformation');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const DEFAULT_POLL_INTERVAL = 1000;
|
|
6
6
|
|
|
7
7
|
class MetricsPoller {
|
|
8
|
-
constructor({ connection, gpu = false, onMetricsEmit = null }) {
|
|
8
|
+
constructor({ connection, gpu = false, interval = DEFAULT_POLL_INTERVAL, onMetricsEmit = null }) {
|
|
9
9
|
this.conn = connection;
|
|
10
10
|
this.gpu = gpu;
|
|
11
|
+
this.interval = Math.max(500, interval || DEFAULT_POLL_INTERVAL);
|
|
11
12
|
this.onMetricsEmit = onMetricsEmit; // alert engine callback — no monkey-patch needed
|
|
12
13
|
this._timer = null;
|
|
14
|
+
this._isPolling = false;
|
|
13
15
|
this._history = [];
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
start() {
|
|
17
19
|
this._poll();
|
|
18
|
-
this._timer = setInterval(() =>
|
|
19
|
-
|
|
20
|
+
this._timer = setInterval(() => {
|
|
21
|
+
if (!this._isPolling) {
|
|
22
|
+
this._poll();
|
|
23
|
+
}
|
|
24
|
+
}, this.interval);
|
|
25
|
+
console.log(`[metrics] Poller started (interval: ${this.interval}ms)`);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
stop() {
|
|
23
29
|
if (this._timer) clearInterval(this._timer);
|
|
30
|
+
this._timer = null;
|
|
24
31
|
console.log('[metrics] Poller stopped');
|
|
25
32
|
}
|
|
26
33
|
|
|
@@ -29,6 +36,8 @@ class MetricsPoller {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
async _poll() {
|
|
39
|
+
if (this._isPolling) return;
|
|
40
|
+
this._isPolling = true;
|
|
32
41
|
try {
|
|
33
42
|
const [cpu, mem, disk, net, procs] = await Promise.all([
|
|
34
43
|
si.currentLoad(),
|
|
@@ -129,6 +138,8 @@ class MetricsPoller {
|
|
|
129
138
|
|
|
130
139
|
} catch (err) {
|
|
131
140
|
console.error('[metrics] Poll error:', err.message);
|
|
141
|
+
} finally {
|
|
142
|
+
this._isPolling = false;
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
}
|
package/lib/shell.js
CHANGED
|
@@ -11,6 +11,7 @@ try {
|
|
|
11
11
|
const PTY_ENV_ALLOWLIST = new Set([
|
|
12
12
|
'PATH', 'HOME', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE',
|
|
13
13
|
'USER', 'LOGNAME', 'HOSTNAME', 'TZ', 'COLORTERM', 'DISPLAY',
|
|
14
|
+
'NVM_DIR', 'NODE_PATH', 'PM2_HOME', 'EDITOR', 'VISUAL', 'CI'
|
|
14
15
|
]);
|
|
15
16
|
|
|
16
17
|
function buildSafeEnv() {
|
|
@@ -18,6 +19,19 @@ function buildSafeEnv() {
|
|
|
18
19
|
for (const key of PTY_ENV_ALLOWLIST) {
|
|
19
20
|
if (process.env[key] !== undefined) safe[key] = process.env[key];
|
|
20
21
|
}
|
|
22
|
+
const home = process.env.HOME || '/home/ubuntu';
|
|
23
|
+
const extraPaths = [
|
|
24
|
+
`${home}/.nvm/versions/node/$(ls ${home}/.nvm/versions/node 2>/dev/null | tail -n 1)/bin`,
|
|
25
|
+
`${home}/.npm-global/bin`,
|
|
26
|
+
`${home}/.local/bin`,
|
|
27
|
+
'/usr/local/bin',
|
|
28
|
+
'/usr/bin',
|
|
29
|
+
'/bin',
|
|
30
|
+
'/usr/sbin',
|
|
31
|
+
'/sbin'
|
|
32
|
+
].join(':');
|
|
33
|
+
|
|
34
|
+
safe.PATH = safe.PATH ? `${safe.PATH}:${extraPaths}` : extraPaths;
|
|
21
35
|
safe.TERM = 'xterm-256color';
|
|
22
36
|
safe.THINKNCOLLAB_AGENT = '1';
|
|
23
37
|
return safe;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinknagent",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "ThinkNCollab server agent — metrics, logs, alerts, shell bridge",
|
|
3
|
+
"version": "0.1.22",
|
|
4
|
+
"description": "ThinkNCollab server agent & MCP server — metrics, logs, alerts, shell bridge, AI planning",
|
|
5
5
|
"main": "lib/agent.js",
|
|
6
6
|
"author": "ThinkNCollab Team <raman@thinkncollab.com>",
|
|
7
7
|
"bin": {
|
|
8
|
-
"thinknagent": "bin/thinknagent.js"
|
|
8
|
+
"thinknagent": "bin/thinknagent.js",
|
|
9
|
+
"thinkncollab-mcp": "bin/thinkncollab-mcp.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"start": "node bin/thinknagent.js start",
|
package/install/setup.sh
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ThinkNCollab Agent — installer
|
|
3
|
-
# Usage: curl -fsSL https://thinkncollab.com/install-agent.sh | bash -s -- --server https://thinkncollab.com --name my-server --room <roomId>
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
SERVER=""
|
|
7
|
-
NAME=""
|
|
8
|
-
ROOM=""
|
|
9
|
-
GPU=false
|
|
10
|
-
LOGS=""
|
|
11
|
-
SYSTEMD=false
|
|
12
|
-
|
|
13
|
-
while [[ $# -gt 0 ]]; do
|
|
14
|
-
case $1 in
|
|
15
|
-
--server) SERVER="$2"; shift 2 ;;
|
|
16
|
-
--name) NAME="$2"; shift 2 ;;
|
|
17
|
-
--room) ROOM="$2"; shift 2 ;;
|
|
18
|
-
--gpu) GPU=true; shift ;;
|
|
19
|
-
--logs) LOGS="$2"; shift 2 ;;
|
|
20
|
-
--systemd) SYSTEMD=true; shift ;;
|
|
21
|
-
*) echo "Unknown option: $1"; exit 1 ;;
|
|
22
|
-
esac
|
|
23
|
-
done
|
|
24
|
-
|
|
25
|
-
[[ -z "$SERVER" ]] && echo "Error: --server required" && exit 1
|
|
26
|
-
[[ -z "$NAME" ]] && echo "Error: --name required" && exit 1
|
|
27
|
-
[[ -z "$ROOM" ]] && echo "Error: --room required" && exit 1
|
|
28
|
-
|
|
29
|
-
echo ""
|
|
30
|
-
echo " ThinkNCollab Agent Installer"
|
|
31
|
-
echo " ──────────────────────────────"
|
|
32
|
-
|
|
33
|
-
NODE_VER=$(node --version 2>/dev/null | cut -d. -f1 | tr -d 'v' || echo "0")
|
|
34
|
-
if [[ "$NODE_VER" -lt 18 ]]; then
|
|
35
|
-
echo " Error: Node.js 18+ required (found: $(node --version 2>/dev/null || echo 'not found'))"
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
echo " Node.js : $(node --version) ✓"
|
|
39
|
-
|
|
40
|
-
echo " Installing thinknagent..."
|
|
41
|
-
npm install -g thinknagent --silent
|
|
42
|
-
|
|
43
|
-
GPU_FLAG=""
|
|
44
|
-
$GPU && GPU_FLAG="--gpu"
|
|
45
|
-
|
|
46
|
-
LOGS_FLAG=""
|
|
47
|
-
[[ -n "$LOGS" ]] && LOGS_FLAG="--logs $LOGS"
|
|
48
|
-
|
|
49
|
-
thinknagent init --server "$SERVER" --name "$NAME" --room "$ROOM" $GPU_FLAG $LOGS_FLAG
|
|
50
|
-
|
|
51
|
-
if $SYSTEMD; then
|
|
52
|
-
echo ""
|
|
53
|
-
echo " Setting up systemd service..."
|
|
54
|
-
id thinknagent &>/dev/null || useradd --system --no-create-home thinknagent
|
|
55
|
-
AGENT_BIN="$(which thinknagent)"
|
|
56
|
-
cat > /etc/systemd/system/thinknagent.service << SVCEOF
|
|
57
|
-
[Unit]
|
|
58
|
-
Description=ThinkNCollab Agent
|
|
59
|
-
After=network-online.target
|
|
60
|
-
Wants=network-online.target
|
|
61
|
-
|
|
62
|
-
[Service]
|
|
63
|
-
User=thinknagent
|
|
64
|
-
Group=thinknagent
|
|
65
|
-
ExecStart=${AGENT_BIN} start
|
|
66
|
-
Restart=on-failure
|
|
67
|
-
RestartSec=10
|
|
68
|
-
Environment=NODE_ENV=production
|
|
69
|
-
StandardOutput=journal
|
|
70
|
-
StandardError=journal
|
|
71
|
-
SyslogIdentifier=thinknagent
|
|
72
|
-
|
|
73
|
-
[Install]
|
|
74
|
-
WantedBy=multi-user.target
|
|
75
|
-
SVCEOF
|
|
76
|
-
systemctl daemon-reload
|
|
77
|
-
systemctl enable thinknagent
|
|
78
|
-
systemctl start thinknagent
|
|
79
|
-
echo " systemd service: enabled + started ✓"
|
|
80
|
-
echo " Logs: journalctl -u thinknagent -f"
|
|
81
|
-
else
|
|
82
|
-
echo ""
|
|
83
|
-
echo " To start now : thinknagent start"
|
|
84
|
-
echo " To run on boot : Re-run this script with --systemd flag"
|
|
85
|
-
echo " Or with pm2 : pm2 start \$(which thinknagent) -- start && pm2 save"
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
echo ""
|
|
89
|
-
echo " ✓ Done. Open ThinkNCollab and approve this agent in your room's DevOps Wall."
|
|
90
|
-
echo ""
|
package/lib/app.js
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const { exec } = require('child_process');
|
|
7
|
-
const store = require('./store');
|
|
8
|
-
const Agent = require('./agent');
|
|
9
|
-
const si = require('systeminformation');
|
|
10
|
-
|
|
11
|
-
class AgentApp {
|
|
12
|
-
constructor(port = 4455) {
|
|
13
|
-
this.port = port;
|
|
14
|
-
this.server = null;
|
|
15
|
-
this.agentInstance = null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
start(autoOpen = true) {
|
|
19
|
-
this.server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
20
|
-
this.server.listen(this.port, '127.0.0.1', () => {
|
|
21
|
-
const url = `http://localhost:${this.port}`;
|
|
22
|
-
console.log(`\n \x1b[32m✔\x1b[0m ThinkNCollab Agent App is running at: \x1b[36m${url}\x1b[0m\n`);
|
|
23
|
-
if (autoOpen) {
|
|
24
|
-
this._openBrowser(url);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Auto-start agent if already initialized
|
|
29
|
-
const cfg = store.read();
|
|
30
|
-
if (cfg.serverUrl && cfg.roomId) {
|
|
31
|
-
try {
|
|
32
|
-
this.agentInstance = new Agent();
|
|
33
|
-
this.agentInstance.start();
|
|
34
|
-
} catch (err) {
|
|
35
|
-
console.warn('[app] Auto-start agent notice:', err.message);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_openBrowser(url) {
|
|
41
|
-
const startCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
42
|
-
exec(`${startCmd} ${url}`, () => {});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async _handleRequest(req, res) {
|
|
46
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
47
|
-
|
|
48
|
-
// API: Status & Telemetry
|
|
49
|
-
if (url.pathname === '/api/status' && req.method === 'GET') {
|
|
50
|
-
try {
|
|
51
|
-
const [cpu, mem, disk] = await Promise.all([
|
|
52
|
-
si.currentLoad(),
|
|
53
|
-
si.mem(),
|
|
54
|
-
si.fsSize()
|
|
55
|
-
]);
|
|
56
|
-
const cfg = store.read();
|
|
57
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
58
|
-
return res.end(JSON.stringify({
|
|
59
|
-
success: true,
|
|
60
|
-
config: {
|
|
61
|
-
name: cfg.name || os.hostname(),
|
|
62
|
-
serverUrl: cfg.serverUrl || '',
|
|
63
|
-
roomId: cfg.roomId || '',
|
|
64
|
-
agentId: cfg.agentId || '',
|
|
65
|
-
role: cfg.role || 'monitor',
|
|
66
|
-
status: cfg.agentToken ? 'approved' : cfg.agentId ? 'pending' : 'not_initialized'
|
|
67
|
-
},
|
|
68
|
-
telemetry: {
|
|
69
|
-
hostname: os.hostname(),
|
|
70
|
-
platform: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
71
|
-
uptime: os.uptime(),
|
|
72
|
-
cpuPercent: parseFloat(cpu.currentLoad.toFixed(1)),
|
|
73
|
-
cores: cpu.cpus?.length || os.cpus().length,
|
|
74
|
-
memoryTotalMB: Math.round(mem.total / 1048576),
|
|
75
|
-
memoryUsedMB: Math.round(mem.used / 1048576),
|
|
76
|
-
memoryUsedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
|
|
77
|
-
disk: disk.map(d => ({
|
|
78
|
-
mount: d.mount,
|
|
79
|
-
sizeGB: (d.size / 1073741824).toFixed(1),
|
|
80
|
-
usedPct: parseFloat((d.use || 0).toFixed(1))
|
|
81
|
-
}))
|
|
82
|
-
}
|
|
83
|
-
}));
|
|
84
|
-
} catch (err) {
|
|
85
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
86
|
-
return res.end(JSON.stringify({ error: err.message }));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// API: Connect / Configure
|
|
91
|
-
if (url.pathname === '/api/connect' && req.method === 'POST') {
|
|
92
|
-
let body = '';
|
|
93
|
-
req.on('data', chunk => body += chunk);
|
|
94
|
-
req.on('end', () => {
|
|
95
|
-
try {
|
|
96
|
-
const data = JSON.parse(body);
|
|
97
|
-
const { serverUrl, name, roomId } = data;
|
|
98
|
-
if (!serverUrl || !roomId) {
|
|
99
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
100
|
-
return res.end(JSON.stringify({ error: 'Server URL and Room ID are required.' }));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const { v4: uuid } = require('uuid');
|
|
104
|
-
const existing = store.read();
|
|
105
|
-
const agentId = existing.agentId || uuid();
|
|
106
|
-
|
|
107
|
-
store.write({
|
|
108
|
-
...existing,
|
|
109
|
-
serverUrl: serverUrl.replace(/\/$/, ''),
|
|
110
|
-
name: name || os.hostname(),
|
|
111
|
-
roomId,
|
|
112
|
-
agentId
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Restart Agent instance
|
|
116
|
-
if (this.agentInstance) {
|
|
117
|
-
try { this.agentInstance._shutdown('reconnect'); } catch (e) {}
|
|
118
|
-
}
|
|
119
|
-
this.agentInstance = new Agent();
|
|
120
|
-
this.agentInstance.start();
|
|
121
|
-
|
|
122
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
123
|
-
return res.end(JSON.stringify({ success: true, message: 'Agent connected! Check DevOps Wall for approval.' }));
|
|
124
|
-
} catch (err) {
|
|
125
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
126
|
-
return res.end(JSON.stringify({ error: err.message }));
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Serve Local App UI
|
|
133
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
134
|
-
res.end(this._getHtml());
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
_getHtml() {
|
|
138
|
-
return `<!DOCTYPE html>
|
|
139
|
-
<html lang="en">
|
|
140
|
-
<head>
|
|
141
|
-
<meta charset="UTF-8">
|
|
142
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
143
|
-
<title>ThinkNCollab Agent App</title>
|
|
144
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
|
145
|
-
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
|
|
146
|
-
<style>
|
|
147
|
-
* { margin:0; padding:0; box-sizing:border-box; }
|
|
148
|
-
:root {
|
|
149
|
-
--bg: #090910; --bg2: #12121e; --card: #161626; --border: rgba(255,255,255,0.08);
|
|
150
|
-
--accent: #00ff88; --accent-glow: rgba(0,255,136,0.2); --text: #f1f5f9; --text2: #94a3b8; --text3: #475569;
|
|
151
|
-
--orange: #f97316; --red: #ef4444;
|
|
152
|
-
--font: 'Syne', sans-serif; --mono: 'JetBrains Mono', monospace;
|
|
153
|
-
}
|
|
154
|
-
body { background: var(--bg); color: var(--text); font-family: var(--font); min-height: 100vh; display: flex; flex-direction: column; }
|
|
155
|
-
header { height: 60px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; background: rgba(18,18,30,0.8); backdrop-filter: blur(12px); }
|
|
156
|
-
.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 16px; letter-spacing: -0.02em; }
|
|
157
|
-
.brand-dot { color: var(--accent); }
|
|
158
|
-
.badge { font-family: var(--mono); font-size: 11px; padding: 4px 10px; border-radius: 20px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
159
|
-
.badge.online { background: rgba(0,255,136,0.12); color: var(--accent); border: 1px solid rgba(0,255,136,0.3); }
|
|
160
|
-
.badge.pending { background: rgba(249,115,22,0.12); color: var(--orange); border: 1px solid rgba(249,115,22,0.3); }
|
|
161
|
-
.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
|
|
162
|
-
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
|
|
163
|
-
.container { max-width: 960px; margin: 0 auto; width: 100%; padding: 24px; flex: 1; display: flex; flex-direction: column; gap: 20px; }
|
|
164
|
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
|
|
165
|
-
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 18px; }
|
|
166
|
-
.card-title { font-size: 11px; font-family: var(--mono); color: var(--text2); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; display: flex; align-items: center; gap: 6px; }
|
|
167
|
-
.metric-val { font-size: 26px; font-weight: 800; font-family: var(--mono); color: var(--accent); }
|
|
168
|
-
.metric-sub { font-size: 11px; color: var(--text3); font-family: var(--mono); margin-top: 4px; }
|
|
169
|
-
.form-group { margin-bottom: 12px; }
|
|
170
|
-
.form-group label { display: block; font-size: 11px; font-family: var(--mono); color: var(--text2); margin-bottom: 4px; }
|
|
171
|
-
.form-group input { width: 100%; background: var(--bg2); border: 1px solid var(--border); color: #fff; padding: 8px 12px; border-radius: 6px; font-size: 12px; font-family: var(--mono); }
|
|
172
|
-
.form-group input:focus { outline: none; border-color: var(--accent); }
|
|
173
|
-
.btn { background: var(--accent); color: #05140c; border: none; padding: 10px 18px; border-radius: 6px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: var(--font); transition: all 0.15s; }
|
|
174
|
-
.btn:hover { box-shadow: 0 0 15px var(--accent-glow); transform: translateY(-1px); }
|
|
175
|
-
.toast { position: fixed; bottom: 20px; right: 20px; background: var(--card); border: 1px solid var(--accent); padding: 12px 20px; border-radius: 8px; font-size: 12px; color: #fff; display: none; z-index: 100; }
|
|
176
|
-
</style>
|
|
177
|
-
</head>
|
|
178
|
-
<body>
|
|
179
|
-
<header>
|
|
180
|
-
<div class="brand">
|
|
181
|
-
<i class="fa-solid fa-server" style="color:var(--accent);"></i>
|
|
182
|
-
ThinkNCollab Agent App<span class="brand-dot">.</span>
|
|
183
|
-
</div>
|
|
184
|
-
<div id="status-badge" class="badge pending">
|
|
185
|
-
<div class="badge-dot"></div>
|
|
186
|
-
<span id="status-text">INITIALIZING</span>
|
|
187
|
-
</div>
|
|
188
|
-
</header>
|
|
189
|
-
|
|
190
|
-
<div class="container">
|
|
191
|
-
<div class="grid">
|
|
192
|
-
<div class="card">
|
|
193
|
-
<div class="card-title"><i class="fa-solid fa-microchip"></i> CPU Usage</div>
|
|
194
|
-
<div class="metric-val" id="val-cpu">--%</div>
|
|
195
|
-
<div class="metric-sub" id="val-cores">-- Cores</div>
|
|
196
|
-
</div>
|
|
197
|
-
<div class="card">
|
|
198
|
-
<div class="card-title"><i class="fa-solid fa-memory"></i> Memory</div>
|
|
199
|
-
<div class="metric-val" id="val-mem">--%</div>
|
|
200
|
-
<div class="metric-sub" id="val-mem-detail">-- / -- MB</div>
|
|
201
|
-
</div>
|
|
202
|
-
<div class="card">
|
|
203
|
-
<div class="card-title"><i class="fa-solid fa-hard-drive"></i> Primary Disk</div>
|
|
204
|
-
<div class="metric-val" id="val-disk">--%</div>
|
|
205
|
-
<div class="metric-sub" id="val-disk-detail">-- GB</div>
|
|
206
|
-
</div>
|
|
207
|
-
<div class="card">
|
|
208
|
-
<div class="card-title"><i class="fa-solid fa-clock"></i> Node Uptime</div>
|
|
209
|
-
<div class="metric-val" id="val-uptime" style="font-size:18px;">--</div>
|
|
210
|
-
<div class="metric-sub" id="val-platform">--</div>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<div class="card">
|
|
215
|
-
<div style="font-size:14px; font-weight:700; margin-bottom:14px; display:flex; align-items:center; gap:8px;">
|
|
216
|
-
<i class="fa-solid fa-link" style="color:var(--accent);"></i> Connect to ThinkNCollab DevOps Wall
|
|
217
|
-
</div>
|
|
218
|
-
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:12px;">
|
|
219
|
-
<div class="form-group">
|
|
220
|
-
<label>ThinkNCollab Server URL</label>
|
|
221
|
-
<input type="text" id="cfg-server" placeholder="https://thinkncollab.com" value="https://thinkncollab.com" />
|
|
222
|
-
</div>
|
|
223
|
-
<div class="form-group">
|
|
224
|
-
<label>Target Room ID (from URL)</label>
|
|
225
|
-
<input type="text" id="cfg-room" placeholder="e.g. 6a318170496c7b00a7f74260" />
|
|
226
|
-
</div>
|
|
227
|
-
<div class="form-group">
|
|
228
|
-
<label>Node Display Name</label>
|
|
229
|
-
<input type="text" id="cfg-name" placeholder="prod-web-01" />
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
<div style="display:flex; justify-content:flex-end; margin-top:8px;">
|
|
233
|
-
<button class="btn" onclick="saveConnection()"><i class="fa-solid fa-paper-plane"></i> Connect Node</button>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
|
|
238
|
-
<div id="toast" class="toast"></div>
|
|
239
|
-
|
|
240
|
-
<script>
|
|
241
|
-
async function fetchStatus() {
|
|
242
|
-
try {
|
|
243
|
-
const res = await fetch('/api/status');
|
|
244
|
-
const data = await res.json();
|
|
245
|
-
if (data.success) {
|
|
246
|
-
const t = data.telemetry;
|
|
247
|
-
const c = data.config;
|
|
248
|
-
|
|
249
|
-
document.getElementById('val-cpu').textContent = t.cpuPercent + '%';
|
|
250
|
-
document.getElementById('val-cores').textContent = t.cores + ' Cores';
|
|
251
|
-
document.getElementById('val-mem').textContent = t.memoryUsedPct + '%';
|
|
252
|
-
document.getElementById('val-mem-detail').textContent = t.memoryUsedMB + ' / ' + t.memoryTotalMB + ' MB';
|
|
253
|
-
if (t.disk && t.disk[0]) {
|
|
254
|
-
document.getElementById('val-disk').textContent = t.disk[0].usedPct + '%';
|
|
255
|
-
document.getElementById('val-disk-detail').textContent = t.disk[0].sizeGB + ' GB';
|
|
256
|
-
}
|
|
257
|
-
const upHours = Math.floor(t.uptime / 3600);
|
|
258
|
-
const upMins = Math.floor((t.uptime % 3600) / 60);
|
|
259
|
-
document.getElementById('val-uptime').textContent = upHours + 'h ' + upMins + 'm';
|
|
260
|
-
document.getElementById('val-platform').textContent = t.platform;
|
|
261
|
-
|
|
262
|
-
const badge = document.getElementById('status-badge');
|
|
263
|
-
const badgeText = document.getElementById('status-text');
|
|
264
|
-
if (c.status === 'approved') {
|
|
265
|
-
badge.className = 'badge online';
|
|
266
|
-
badgeText.textContent = 'ONLINE (APPROVED)';
|
|
267
|
-
} else if (c.status === 'pending') {
|
|
268
|
-
badge.className = 'badge pending';
|
|
269
|
-
badgeText.textContent = 'PENDING APPROVAL';
|
|
270
|
-
} else {
|
|
271
|
-
badge.className = 'badge pending';
|
|
272
|
-
badgeText.textContent = 'NOT CONFIGURED';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (c.serverUrl && !document.getElementById('cfg-server').value) document.getElementById('cfg-server').value = c.serverUrl;
|
|
276
|
-
if (c.roomId && !document.getElementById('cfg-room').value) document.getElementById('cfg-room').value = c.roomId;
|
|
277
|
-
if (c.name && !document.getElementById('cfg-name').value) document.getElementById('cfg-name').value = c.name;
|
|
278
|
-
}
|
|
279
|
-
} catch (err) {
|
|
280
|
-
console.error('Status fetch error:', err);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function saveConnection() {
|
|
285
|
-
const serverUrl = document.getElementById('cfg-server').value.trim();
|
|
286
|
-
const roomId = document.getElementById('cfg-room').value.trim();
|
|
287
|
-
const name = document.getElementById('cfg-name').value.trim();
|
|
288
|
-
|
|
289
|
-
if (!serverUrl || !roomId) {
|
|
290
|
-
showToast('Server URL and Room ID are required!');
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
const res = await fetch('/api/connect', {
|
|
296
|
-
method: 'POST',
|
|
297
|
-
headers: { 'Content-Type': 'application/json' },
|
|
298
|
-
body: JSON.stringify({ serverUrl, roomId, name })
|
|
299
|
-
});
|
|
300
|
-
const d = await res.json();
|
|
301
|
-
if (d.success) {
|
|
302
|
-
showToast(d.message || 'Connected successfully!');
|
|
303
|
-
fetchStatus();
|
|
304
|
-
} else {
|
|
305
|
-
showToast(d.error || 'Failed to connect');
|
|
306
|
-
}
|
|
307
|
-
} catch (err) {
|
|
308
|
-
showToast('Connection error: ' + err.message);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function showToast(msg) {
|
|
313
|
-
const t = document.getElementById('toast');
|
|
314
|
-
t.textContent = msg;
|
|
315
|
-
t.style.display = 'block';
|
|
316
|
-
setTimeout(() => t.style.display = 'none', 4000);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
fetchStatus();
|
|
320
|
-
setInterval(fetchStatus, 3000);
|
|
321
|
-
</script>
|
|
322
|
-
</body>
|
|
323
|
-
</html>`;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
module.exports = AgentApp;
|