commit 90d55cad7e0d4973db187f74fd3088e5488c9f81 Author: Kendel Christoph Date: Sat Dec 27 20:13:26 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c00ed8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ + +# dependencies +node_modules/ + +# logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# runtime data +pids +*.pid +*.seed +*.pid.lock + +# coverage +coverage +*.lcov + +# build output +dist/ +build/ +.cache/ +.next/ +out/ + +# env files +.env +.env.*.local + +# misc +.DS_Store +Thumbs.db diff --git a/Dockerfile.ssh b/Dockerfile.ssh new file mode 100644 index 0000000..0a1b112 --- /dev/null +++ b/Dockerfile.ssh @@ -0,0 +1,21 @@ +# Small base with package manager; Debian is straightforward +FROM debian:bookworm-slim + +# Install OpenSSH client and basic tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + openssh-client ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# Wrapper: copy key to an SSH location with 0600 perms, then exec ssh with args +RUN printf '%s\n' \ + '#!/bin/sh' \ + 'set -eu' \ + 'mkdir -p /root/.ssh' \ + '# If a key is mounted, install it with strict permissions' \ + 'if [ -f /mnt/keys/tunnel_ed25519 ]; then' \ + ' install -m 600 /mnt/keys/tunnel_ed25519 /root/.ssh/id_ed25519' \ + 'fi' \ + 'exec ssh "$@"' \ + > /usr/local/bin/run-ssh && chmod +x /usr/local/bin/run-ssh + +ENTRYPOINT ["/usr/local/bin/run-ssh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef0bf5d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# appRobotConfig diff --git a/app/data/configRobot.yml b/app/data/configRobot.yml new file mode 100644 index 0000000..5f47e36 --- /dev/null +++ b/app/data/configRobot.yml @@ -0,0 +1,65 @@ +Mechanics: + Robot: + Name: "Arm7m" + id: "a0f57d44-1bc8-4d0e-a682-316cf538370a" # UUID of the Robot + + Access: + type: "Base, Ellbow, LowerArm" # three controller Boards + adressBase: "" + + Position: + Base: # Position of different Arucos on the Base + freedom: "x" # which degree of freedom does apply + Aruco210: "{Nr: '210', x: '+23', y: '34'}" + Aruco222: "{Nr: '222', x: '+23', y: '34'}" + + UpperArm: + freedom: "x, alpha" # which degree of freedom does apply + Aruco233: "{Nr: '233', x: '+23', y: '34'}" + + LowerArm: + freedom: "x, alpha, beta" + + Rail: + Name: "Basis 1" + id: "1d447f8a-275a-4a24-8ddd-ec7f598511af" + Markers: + + VideoSetup: + Cam1: "localhost:8080/StreamX" + Cam2: "localhost:8080/StreamY" + CSV: "localhost:8080/Video.csv" + + +Apps: + AppVideoServer: + Name: "Video Server und Steuerung" + Docker: "AppVideoServer" + localAdress: "localhost:1010" + robots: ["Robot"] + visiblePort: 8090 + + RoboticsDriver: + Name: "Robotics Driver" + Docker: "RoboticsDriver" + localAdress: "localhost: 2920" + + AppFindZero: + Name: "Set to Zero" + +Network: + Myself: + MyID: "RobotUser Max" # My ID so the Server knows, who I am + MyPublicKey: " ... ... " + MyPrivateKeyPath: "..." # To identify at Server, who saves the public Key + MyLocalIP: "192.168.2.12" # in case the WebBrowser is in the same Network + Server: + Adress: "robotics.schooltech.ch" + ServerPublicKey: "...." + LastServerInfo: "20250101" + +ThisFile: + CheckSum: "...." + LastWorkedOnBy: "...." + + diff --git a/app/programs/listDockerContainer.py b/app/programs/listDockerContainer.py new file mode 100755 index 0000000..5a58ed2 --- /dev/null +++ b/app/programs/listDockerContainer.py @@ -0,0 +1,8 @@ +# +# All other Apps on the Robot (should be) installed as Docker containers. So a simple +# List of all Dockers should reveal which Apps are installed (with each Entry-Port) +# + + + +print('{"Apps": [{"Robot": {"Container":"Robot", "port":"8808"}}]}') diff --git a/app/public/index.html b/app/public/index.html new file mode 100755 index 0000000..8948792 --- /dev/null +++ b/app/public/index.html @@ -0,0 +1,119 @@ + + + + + + + + System & Docker Status + + + +
+

System & Docker Status

+
Last update: · Auto-refresh on
+
+ +
+
+
+

CPU

+
+
0%
+
+
Load averages
+
1m:
+
5m:
+
15m:
+
+
+
+ +
+

Memory

+
+
+
Used
+
+
/ ()
+
+
+
Tip
+
Keep memory usage under 80% to avoid OOM kills.
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+ + + + + + +`` diff --git a/app/public/js/status.js b/app/public/js/status.js new file mode 100755 index 0000000..ebfb5e8 --- /dev/null +++ b/app/public/js/status.js @@ -0,0 +1,115 @@ + +// Simple status dashboard +const DATA_URL = 'data/status.json'; +let autoRefresh = true; +let refreshTimer = null; + +function formatMB(mb) { + return `${mb.toFixed(2)} MB`; +} +function formatPercent(p) { + return `${p.toFixed(2)}%`; +} +function setRing(el, percent) { + const deg = Math.min(100, Math.max(0, percent)) * 3.6; // 100% -> 360deg + el.style.setProperty('--deg', `${deg}deg`); +} + +function renderSystem(system) { + const cpuPercent = system?.cpu?.percent ?? 0; + const ring = document.getElementById('cpu-ring'); + setRing(ring, cpuPercent); + document.getElementById('cpu-percent').textContent = formatPercent(cpuPercent); + const la = system?.cpu?.load_avg ?? {}; + document.getElementById('load-1m').textContent = (la['1m'] ?? '–'); + document.getElementById('load-5m').textContent = (la['5m'] ?? '–'); + document.getElementById('load-15m').textContent = (la['15m'] ?? '–'); + + const mem = system?.memory ?? {}; + const pct = mem.percent ?? 0; + document.getElementById('mem-bar').style.width = `${pct}%`; + document.getElementById('mem-percent').textContent = formatPercent(pct); + document.getElementById('mem-used').textContent = formatMB(mem.used_mb ?? 0); + document.getElementById('mem-total').textContent = formatMB(mem.total_mb ?? 0); +} + +function portBadge(p) { + const host = p.host_ip ? `${p.host_ip}` : ''; + const hostPort = p.host_port ? `:${p.host_port}` : ''; + const container = p.container_port || ''; + const text = `${host}${hostPort} → ${container}`.trim(); + return `${text || '—'}`; +} + +function renderContainers(list) { + const q = document.getElementById('search').value.toLowerCase(); + const root = document.getElementById('container-list'); + root.innerHTML = ''; + + const filtered = list.filter(c => { + const s = `${c.name} ${c.image}`.toLowerCase(); + return s.includes(q); + }); + + document.getElementById('container-count').textContent = `${filtered.length} / ${list.length} shown`; + + filtered.forEach(c => { + const statusClass = (c.status === 'running') ? 'status' : 'status stopped'; + const ports = (c.openPorts && c.openPorts.length) ? c.openPorts.map(portBadge).join('') : 'No published ports'; + const cpu = c.resources?.cpu_percent ?? 0; + const memMb = c.resources?.memory_mb ?? 0; + const memPct = c.resources?.memory_percent ?? 0; + + const row = document.createElement('div'); + row.className = 'row'; + row.innerHTML = ` +
${c.name}
${c.image}
+
${c.status}
+
${ports}
+
+ CPU: ${formatPercent(cpu)} + Mem: ${formatMB(memMb)} (${formatPercent(memPct)}) +
+
${c.id.slice(0,12)}
+ `; + root.appendChild(row); + }); +} + +async function loadStatus() { + try { + const res = await fetch(`${DATA_URL}?t=${Date.now()}`, { cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + document.getElementById('last-update').textContent = new Date(data.timestamp).toLocaleString(); + renderSystem(data.system); + renderContainers(data.containers || []); + } catch (err) { + console.error('Failed to load status:', err); + document.getElementById('last-update').textContent = 'failed'; + } +} + +function startAutoRefresh() { + if (refreshTimer) clearInterval(refreshTimer); + refreshTimer = setInterval(() => { + if (autoRefresh) loadStatus(); + }, 5000); +} + +// Events +window.addEventListener('DOMContentLoaded', () => { + document.getElementById('toggle-refresh').addEventListener('click', () => { + autoRefresh = !autoRefresh; + document.getElementById('toggle-refresh').textContent = autoRefresh ? 'Pause' : 'Resume'; + document.getElementById('refresh-state').textContent = autoRefresh ? 'on' : 'paused'; + if (autoRefresh) loadStatus(); + }); + document.getElementById('search').addEventListener('input', () => { + // Re-render list using current data in memory by fetching again (cheap) + loadStatus(); + }); + + loadStatus(); + startAutoRefresh(); +}); diff --git a/app/server.js b/app/server.js new file mode 100755 index 0000000..ab5b5c9 --- /dev/null +++ b/app/server.js @@ -0,0 +1,216 @@ + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const url = require('url'); +const zlib = require('zlib'); + + +const PORT = 80; +const HOST = '0.0.0.0'; // Wichtig: IPv4 binden + +const PUBLIC_DIR = path.join(__dirname, 'public'); // ./app/public +const STATUS_FILE = path.join(PUBLIC_DIR, 'data', 'status.json'); + + + +const MIME = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.txt': 'text/plain; charset=utf-8', + '.map': 'application/json; charset=utf-8' +}; + + + +const SECURITY_HEADERS = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'SAMEORIGIN', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + // Allow same-origin scripts and styles; adjust if you load external assets + 'Content-Security-Policy': "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self';" +}; + +// Helper: safe join under PUBLIC_DIR (avoid path traversal) +function resolvePublicPath(requestPath) { + const clean = path.normalize(requestPath).replace(/^(\.\.[/\\])+/, ''); + const absolutePath = path.join(PUBLIC_DIR, clean); + if (!absolutePath.startsWith(PUBLIC_DIR)) return null; + return absolutePath; +} + +// Helper: simple logger +function log(req, res, extra = '') { + const ts = new Date().toISOString(); + console.log(`[${ts}] ${req.method} ${req.url} -> ${res.statusCode} ${extra}`); +} + +// Helper: send JSON +function sendJSON(res, statusCode, obj) { + const buf = Buffer.from(JSON.stringify(obj, null, 2), 'utf-8'); + res.writeHead(statusCode, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store', + ...SECURITY_HEADERS + }); + res.end(buf); +} + +// Helper: gzip if accepted +function maybeCompress(req, res, contentType) { + const accept = req.headers['accept-encoding'] || ''; + const supportsGzip = /\bgzip\b/.test(accept); + if (!supportsGzip) return null; + res.setHeader('Content-Encoding', 'gzip'); + res.setHeader('Vary', 'Accept-Encoding'); + return zlib.createGzip(); +} + +// Serve static files from /public +function serveStatic(req, res, pathname) { + // Default to index.html for "/" and for directory paths + let targetPath = pathname === '/' ? '/index.html' : pathname; + + // Prevent directory traversal and map to filesystem + const abs = resolvePublicPath(targetPath); + if (!abs) { + res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8', ...SECURITY_HEADERS }); + res.end('Bad request'); + return; + } + + // If the path maps to a directory, try index.html inside it + let stat; + try { + stat = fs.statSync(abs); + if (stat.isDirectory()) { + targetPath = path.join(targetPath, 'index.html'); + const absIndex = resolvePublicPath(targetPath); + if (!absIndex || !fs.existsSync(absIndex)) { + res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8', ...SECURITY_HEADERS }); + res.end('Forbidden'); + return; + } + return streamFile(req, res, absIndex); + } + } catch { + // Fallback: try adding .html if no extension and file missing + const ext = path.extname(abs); + if (!ext) { + const htmlCandidate = `${abs}.html`; + if (fs.existsSync(htmlCandidate)) { + return streamFile(req, res, htmlCandidate); + } + } + // 404 if not found + res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8', ...SECURITY_HEADERS }); + res.end('Not found'); + return; + } + + // File exists—stream it + return streamFile(req, res, abs); +} + +// Stream file with basic headers and optional gzip +function streamFile(req, res, absFile) { + const ext = path.extname(absFile).toLowerCase(); + const contentType = MIME[ext] || 'application/octet-stream'; + const stat = fs.statSync(absFile); + + // Cache policy: static assets get short cache; HTML no-cache to help dev + const isHTML = contentType.startsWith('text/html'); + const cacheControl = isHTML ? 'no-cache' : 'public, max-age=60'; + + // ETag (weak): size + mtime + const etag = `W/"${stat.size}-${stat.mtimeMs.toFixed(0)}"`; + if (req.headers['if-none-match'] === etag) { + res.writeHead(304, { 'ETag': etag, 'Cache-Control': cacheControl, ...SECURITY_HEADERS }); + res.end(); + return; + } + + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Length', stat.size); + res.setHeader('ETag', etag); + res.setHeader('Cache-Control', cacheControl); + Object.entries(SECURITY_HEADERS).forEach(([k, v]) => res.setHeader(k, v)); + + // Gzip if accepted (skip for already compressed types like images) + const maybeGzip = (/\btext\/|application\/(javascript|json|xml)/.test(contentType)) ? maybeCompress(req, res, contentType) : null; + + const stream = fs.createReadStream(absFile); + stream.on('error', (err) => { + console.error('[static] stream error:', err); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8', ...SECURITY_HEADERS }); + } + res.end('Internal server error'); + }); + + if (maybeGzip) { + stream.pipe(maybeGzip).pipe(res); + } else { + stream.pipe(res); + } +} + +const server = http.createServer((req, res) => { + const parsed = url.parse(req.url, true); + const pathname = parsed.pathname || '/'; + + // Minimal access log (only path, no query) + res.on('finish', () => log(req, res)); + + // CORS for API (read-only) + if (pathname.startsWith('/api/')) { + // Allow GET from any origin (adjust if needed) + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (pathname === '/api/status') { + fs.readFile(STATUS_FILE, 'utf-8', (err, json) => { + if (err) { + console.error('[api] status read error:', err); + sendJSON(res, 500, { error: 'status_unavailable' }); + return; + } + // Validate JSON and return + try { + const data = JSON.parse(json); + sendJSON(res, 200, data); + } catch (e) { + console.error('[api] status parse error:', e); + sendJSON(res, 500, { error: 'status_invalid_json' }); + } + }); + return; + } + + sendJSON(res, 404, { error: 'not_found' }); + return; + } + + // Serve static from /public + serveStatic(req, res, pathname); +}); + +server + .listen(PORT, HOST, () => { + console.log(`Node.js server listening on http://${HOST}:${PORT}`); + console.log(`Serving static files from: ${PUBLIC_DIR}`); + console.log(`Status file expected at: ${STATUS_FILE}`); + }) + .on('error', (err) => { + console.error('[server] listen error:', err); + }); diff --git a/client/ssh/id_ed25519 b/client/ssh/id_ed25519 new file mode 100755 index 0000000..42ee1d5 --- /dev/null +++ b/client/ssh/id_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAmxhQB5nYS00+6YwVXLYMiKQZFQMGiQrKwxJ00n5y7tQAAAJjUYyKK1GMi +igAAAAtzc2gtZWQyNTUxOQAAACAmxhQB5nYS00+6YwVXLYMiKQZFQMGiQrKwxJ00n5y7tQ +AAAECtQAl35IoNbezyw7NsF+8Pnvaa2wGW4jrB8t/ulIr4tCbGFAHmdhLTT7pjBVctgyIp +BkVAwaJCsrDEnTSfnLu1AAAAFWNsMDFca2VjaEAwMS1LVy1TMDIwNg== +-----END OPENSSH PRIVATE KEY----- diff --git a/client/ssh/id_ed25519.pub b/client/ssh/id_ed25519.pub new file mode 100755 index 0000000..613232f --- /dev/null +++ b/client/ssh/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICbGFAHmdhLTT7pjBVctgyIpBkVAwaJCsrDEnTSfnLu1 cl01\kech@01-KW-S0206 diff --git a/client/ssh/known_hosts b/client/ssh/known_hosts new file mode 100644 index 0000000..3996ffa --- /dev/null +++ b/client/ssh/known_hosts @@ -0,0 +1,32 @@ +10.98.32.183 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILDO853Xe2NzMQ67BvD5k9WSGgu38sxcFKIasuul1JVi +10.98.32.183 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCNLMjrdXqJUCjBH2i76DoBjbCxW7W9L/mIOWU+/9D8oZRBUwZZiPlzgTH5eTLvdVT7L9Qr2UCUCn8UmYf1MLPNKn6pILgbpMxK4dVp2W7sAkqTBf6Lc84OvXjLD7kg6hrwayU3NJKmbvegCMxqnT1UPFvIeOgnCw9u9/SID5jgX8htxR4MAMyR24RSKOeQoyIR2prywYsVTEKQor8PT3U4AEt7umRhVYkgGJo586NIH5hV+VEFmW6G0R8rx5+OWLGqFG7WpkUx02z6FYqrGTf4JnmjAruJIL+adhfH0TVghEc+o/UzhBr/XlgVx/W2pBZq1DzL7f5dcxptN2pQVxdEiGob0IS5bzkBXoUI9KwaFrCwemGOYlMmsDi5i5UoIMbBgD2aTsphljrJaY+RDv6Qh9K4jxFoImhPg80KBJg6Oz2vlX3U0kq4Kgb5HnAmytKFZMerJdmloSVyXf127QL/6T9y0BXmuiNLhKTk6txPxLIVQ6ZrpT0jWPSL4dQ6D98= +10.98.32.183 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNXjSomDO/196azExlsXsgth7h3kCpdzi1Cfh1Ctb92Qa0STpED7PLV96cBQFAzF2GdlYl3gh87EhjoGAuFqWEI= +raspberrypi ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPMMbpIITUewDZlzZitJu1bc+PyFu824ASOry6kps9Tb +raspberrypi ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBK6ubNzIfRSmtxFLB0kHiwDkGr26nLNk8I2jkBIvZva69VykPJyQSoxGXOoQQ1zWUv+l4r+EZ0cSX4sX+LmJZUo= +raspberrypi.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPMMbpIITUewDZlzZitJu1bc+PyFu824ASOry6kps9Tb +10.0.0.56 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxjOLjcjSIbKBDrJykrEIiiuDby6NwJLixaLx1ME5Xj +10.0.0.56 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCdpkkXZSlDdZ/LIzAzN+6nQE2YkaSeNgkfsfzvVzkC9IRepljrmMP2Y4rkiTtlTt+EQUsLXW0oscbJtCWgcCq2dAcLmHZq1tNbvDcI7uHjIPiAs5OtleaZjFV+9CjDr9VhjJNLr0ByBeHlx1MhiYAY6lU3HBfW1E5zyLS5xWPYvhMKeHIssWfVgFWTv1PJdW3sAehY0R6vEh2Tj6R5FD93AUZ0YF9d0XnFMntwp/ExDaVzo/GUE64XT2qLYugt+xQVdDtdFBtbHO0Lf6Q7V5OH+MfH4rVIu0o47Aqg4kCiaqM+cjjIhORJg/FzfNzzcKgLHSA8RckdzUAkJeIL+16q0lsCWn+Lv87HOiObMrfguj3BBeAzEG5ZcC42enfWfcJqBy+YiHZzJhjjNZUWQin34nkYW75appQH+jXQDzNJO7tA4RHfOMoM+L2NgMdlASiX7kkMxdq8N7KJIRTgCK44RlqEXn7qB88QGZ36VFcJexQi3aJj4JMoMIPASXdV6n8= +10.0.0.56 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDMAMqU35mfy8kSCWPw4ESOI0fW8y2TtkpeyVMHUbg8Top8TrpiD42fjMklRbnkkexIuKO0VDUZeJjkTXBMguro= +raspberrypi5 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII3R2w4Ijc82I5d8Wt/kcI5WqLiRn2QIZ/TexGeFUyk +raspberrypi5 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCrx9F+bihWAje9U4wxueqaB0No+8HyvvoC9Ymlf5Ijb7drVHYcgdOSVe93SJlJ23sQ7kdK8UfYbTf9W/Pr07QZ2RSkkRrRA6G5cw/RZw/4pKSRIr1FzHvfwGrJ4O/kzKzU6LdtS7FF3CD4pm9BiHy1jkSkuiZuOncIrWuwjfjE3Fgxh7J+O4ocNicy4XJl926cd4oy5P16HZtZcAZuBiPDgvBXaNU2giHYazL6NRC6pj+EWO0XllnNw8/PM4JnuvM7VtKUgZzDg81Lf+o+OSS5uirJOj5Z1c6dri0fRuT9JzOwYyf84wStf1EuRhW94ppOshQucrOkz72toNxUlNNX9xLfrckyQC3zlp1cXi2hcPlQPrDqnPTZKB8BORhVgePekOrZPITiCV5QC5UuzqecqjtVQze1dakJFMAy/GN6PuiaVIAE5EJ7Ty2soAc5/sCFanvUhd8WhMcqr52MaD9x5hQI5yxZpwnJJ1acMUiu/sgifkRppoUxZ1a4eXECjXk= +raspberrypi5 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKtmHpfOwvcqWE0HJ/uvmA8jKzccdcB0nTJSr2WBK8Bs+65V2UijhNx8rzL1RmQaBShZtVlTvHNAlCbGDttpcwM= +raspberrypi5.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII3R2w4Ijc82I5d8Wt/kcI5WqLiRn2QIZ/TexGeFUyk +ksw3dserver.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBmV41Tq7zrNdyowjRv/rvghSsSMpCwTjw9/fXObcACW +ksw3dserver.local ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDdwKVFJWcGZPSmKJZ1bobwKLvP55lUacG6GYafnP4rbH/KST7uWSOqLxpmDtCfL8g0QzsXdLO0tEDK93OFQIkDzSeXNR9lFGqlV2WGILuOMkek8FIDPA7aODd39BvYz5JeFM0rfyNbR8uVUuwPuHoaMTunjSCTu2/sAa/8S4c3yBJ7D9mEYn9AL/RJV47qnLqX9gtsTbemwWQ3Ij94N6yhdRY/WDuZ6pFnj3rpCo66fhNAisnUve/4QHWDswQ5phIIDktWtDwvJ6N1VNmwInubmBvR60n25V4gI9z1CxRLkipChvB+iX3EFoqcdCIO7L6YqglMKn1oYcgTEfi1IzM82bjWbZq7yXJCGNHlI941/09AB315MipFGuzMIAQ0cJRiDJ58AgFBfyTt6GJYfapc3J3Ja3pwmtnX9yo4JG9ukY1siIb0C5ejrj90idEjVGeyoClMpRA3JNHQljg1g6a/Kbg+DpIznh+7SlmvesJFfqvrHMfh7LTQEyNpqkm9lec= +ksw3dserver.local ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLJrDES36rr4MxeZV0up6ZN2DIJSJ1eDceom2AarwkIHjnZp67GFQE9pGz0A4ewLzogL0co7xJx4JFIPDfaZXeQ= +192.168.8.203 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII3R2w4Ijc82I5d8Wt/kcI5WqLiRn2QIZ/TexGeFUyk +10.0.0.57 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII3R2w4Ijc82I5d8Wt/kcI5WqLiRn2QIZ/TexGeFUyk +192.168.8.201 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBRr5lKH7Oz5eQMQHvhhuRn6inltN3dt03zK8lNRtIcJ +192.168.8.201 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDZ+xZmcQI9PUYnHT5CCDWxmkUuoBwqCowKueYTTyrqCxrK2dt4TrlQOvAtNiTD0RFTZGOTIUvIzzYv+D8zgrWH068hzcjw9fFYmKnwi8gc27CDTZqP2rnejhYKpXsgkSVzEIUuuiuvMU4xwXtiZ9M/yKCvKh6KJZ6mR+e8/26MzNVCb4ZaAEDg3n7dZyDdI2m4Bx26J1I9uTzKQP0XSwp/M0N/G93xr4FN00W4NjBx2zC4NRfcjcPv6sus/EDuCDIb/ICBBPF2BDt0m5EVn+1IO+KpALGd1bwR+toPzWmqkyu0e2QlhL0C5oP+LAPi8A7aXH+VZyxf3zdQ+cmS5IVQB1vnRb17dEU7sKZDIxKUCaXrzhzckp4vaBh6RrPNTJ5Y3j9quKPYiYniYDMP2d+agqwm0FliHP1XZg5ikJYGNleDMhRgnV8roPfEZ2ea1pQ9dl61LE83nfyGMyfi+X/gUSMTz1chjcWIlUM7GNvJ9UEsbjp0QbEZn43jb99Gbws= +192.168.8.201 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIz48oXT1K3fYfpUpv4wUlLKlhQu5x1kKm2SkYu136BKJ9yFCYUKn+uOpOX1SILdo+z8tumhyb388lFSZ94Rxsg= +[synologynas.local]:885 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID5nNPeTdrdu0SBm7GDoMQX6lEi/QKws3c+uJO3jMlEU +[synologynas.local]:885 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHMfMPh3FVD9YWMsukPDtPTF/fLKKiDCmkg06a/Xv9jQEPmiMsexqoxcMFCRJ2qmuSRqH9fnyjR7L+JhJ1FFFXM= +thinkcentre.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV1bzcwR2FwtByf4M82Jjqlut8dUjtlnT6PD9IK9DZN +thinkcentre.local ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2wQ331FiD/0TgP7LwfHFeDdaOjCUez2YucVpFaIbO/zvcUHF93DTwnUrUgKs2HSFVJw8D/shhnfmGNiMzFmqMCVAoCpKrt16eGtDNRvjuRTlbTMT0oaIvNkS095nbDddXBjbjh4nK4Jjiyh56UT2uFGY+TlfM30bi3G1GkWOfzoB36fyMRFgNW9Zja3RZL78j6dbL9BBpUAJGuJfrEmFck7188oGcx/CkeaX+fuButrM+wbSycv6qiRQOXeTYUcsJiIxGQhMl7t1WfiP3PYEpE+DUNFMkugXnINvovMTLn8sOmOBSkV7NYei0IEljPAQ7MWhoGIU5rl0LuLq+zwmlSC1A9LbxvNQzb4jUYJUukd19HCXwi+8bk+Du4xKY7AGvrJtdho+lg5yPLOqbpne3l2jB6bP7UsISC+Rta8JeRXmBvcjN9n/Tx4cHsQNsf4PVHLs3TzL5cqnBdvStEHt87pyvOQwByNw7RODk3QQyh4yTA2sr3UrgW5f7zvMKeNU= +thinkcentre.local ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJFp9Ba4xQBCdG4UcbALUxUAPIiJwY+byAGVlif9c6n8f2NVeDgDUs2+3YSmmfPCTNK84oxbL/hDUNEpL96wZz8= +[albulastrasse.diskstation.me]:885 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID5nNPeTdrdu0SBm7GDoMQX6lEi/QKws3c+uJO3jMlEU +[thinkcentre.local]:3222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDO2kSX8pD+VUMnZEhrOxnjyDk/4U7+h+X5bOOvTz3f5AO4s+LWIrAU5wDP66ibn2LqRrvaxrksLTOUDhKU/jLhDjlqfP5pnI6b2voZHk03pZ9Q9N0A+VJ4GOAiy/Q354CQpUER9WJq1GbhfDS325HUVCM8jHnIrtHEEBsoN4lFIsCL+bie4Gygfk6+Hvq4kljAXj043Tjyvw+sJ+N48C3RBFRBxB/LdP8CuKFurilfhQPnGk7xIuPn+8cGY9VInrX9ZdrVWnzPi/na1CoFAhatmpsqRIi+C26/U0o+lrQFEn8Nf5Dyrkv4fVYQWcJJsHLs00BKac32WBDJbThZSLCssAMUWiWydwk+SgJiYEmcbSgb+i+huKY1/6pPfGFrIVoUO469r7u02Cdb3I1/Py+0ZxXyvtd4bVfTy3hzwz3ouWcDadlmpHn8OOqHzvAVoma/hcNQ8VJv6zs6aiw9S9AdND938XMky2a1dfTM5cRY2vYyWun8DUM50804rK5bigs8w6LCKuQTPv04hkMBiH6cybFbGKBfeuyBfPiObgYMvatf2q1JVdIedPANgXP3BGMTz40I6/U1dQOxw740kwZX1QFon1KPKgRYfrxMSsmQh43NHaHHHlGdXsl6LsvwTUOMbARxpy/gwvBsVBhxOEKRtyUVMXzteSeDuy08AGwhfQ== +thinkcentre ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV1bzcwR2FwtByf4M82Jjqlut8dUjtlnT6PD9IK9DZN +[thinkcentre.local]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL7mpGrP8jGvgVHD9rUiojaRdZ3N+NgDrmnM5yDurz2O +[thinkcentre.local]:2222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJmjp9u71R3nzHn1mDcBkpv8+TFY2nJzRZ00kG2dfsKJ4/TCyIRrBLQNOZ/pwY2l3WrItmyxavSCqcxQ7gWZqxc= +[thinkcentre.local]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCNK5wArMICfo2GXJUt27MNofmAOpZd53pYu1PtlXyEnAC8MJl8XgyioLAG0z8gjhLE70+wfFJsCI6+Awhc3ahxRGCmyz45mON3mQwoDChe5OFyx/dLi5wgXzX1sFVNEA3ziDmMP/8mu/zxVy26+s10KdIKnND0ASka/qjfu3lRQ2BoYohOA3AIdpu0NlXAL+hoghqLG6S8tXJ2p3mPoOVdDHur4xgcMHcFnJXyWkKG+UafjA9Ve04aiVmid+biAREGaOsddJutjGvMnng6D+T5HtOlbqX3iZ1ew8jrzdcyTiN4uuVyNObXYaKbD5MX5HOO/xPZqMzgrcQ7tURyo2gLHJBplrB8akXE/ZpZ53nN65jEuGd5msgRSu8Wy3NbOoQPCsapQqzKjs3rZJSRkb2QLUwq2hPuIUBgyMUoBlwAqooirBcHNBwtsLGJsErKHi5nAXRnvZF7A/ApVaE5JbUeWATjABS8BbjEifsEwYhCnIZlnXP3a2YkaWOndN+YIJ8= diff --git a/client/ssh/tunnel_ed25519 b/client/ssh/tunnel_ed25519 new file mode 100644 index 0000000..ce4ffe7 --- /dev/null +++ b/client/ssh/tunnel_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCz+U/1x5we7BEIL68hS2yNuj1j11OMqIH1RcQhsHjbXAAAAJBVUf5tVVH+ +bQAAAAtzc2gtZWQyNTUxOQAAACCz+U/1x5we7BEIL68hS2yNuj1j11OMqIH1RcQhsHjbXA +AAAEBYr54zv/c7r1IWm+khOsd8luvaeHx1ZkEWn7U7fMkZMLP5T/XHnB7sEQgvryFLbI26 +PWPXU4yogfVFxCGweNtcAAAABnR1bm5lbAECAwQFBgc= +-----END OPENSSH PRIVATE KEY----- diff --git a/client/ssh/tunnel_ed25519.pub b/client/ssh/tunnel_ed25519.pub new file mode 100644 index 0000000..cbb5448 --- /dev/null +++ b/client/ssh/tunnel_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILP5T/XHnB7sEQgvryFLbI26PWPXU4yogfVFxCGweNtc tunnel diff --git a/createTunnel.sh.txt b/createTunnel.sh.txt new file mode 100644 index 0000000..32b7e21 --- /dev/null +++ b/createTunnel.sh.txt @@ -0,0 +1,2 @@ +install -m 600 ./client/ssh/tunnel_ed25519 ~/.ssh/tunnel_ed25519 +ssh -p 2222 -v -N -T -R 0.0.0.0:9000:localhost:8080 -i ~/.ssh/tunnel_ed25519 -o ExitOnForwardFailure=yes tunnel@thinkcentre.local \ No newline at end of file diff --git a/data/configRobot.yml b/data/configRobot.yml new file mode 100755 index 0000000..5f47e36 --- /dev/null +++ b/data/configRobot.yml @@ -0,0 +1,65 @@ +Mechanics: + Robot: + Name: "Arm7m" + id: "a0f57d44-1bc8-4d0e-a682-316cf538370a" # UUID of the Robot + + Access: + type: "Base, Ellbow, LowerArm" # three controller Boards + adressBase: "" + + Position: + Base: # Position of different Arucos on the Base + freedom: "x" # which degree of freedom does apply + Aruco210: "{Nr: '210', x: '+23', y: '34'}" + Aruco222: "{Nr: '222', x: '+23', y: '34'}" + + UpperArm: + freedom: "x, alpha" # which degree of freedom does apply + Aruco233: "{Nr: '233', x: '+23', y: '34'}" + + LowerArm: + freedom: "x, alpha, beta" + + Rail: + Name: "Basis 1" + id: "1d447f8a-275a-4a24-8ddd-ec7f598511af" + Markers: + + VideoSetup: + Cam1: "localhost:8080/StreamX" + Cam2: "localhost:8080/StreamY" + CSV: "localhost:8080/Video.csv" + + +Apps: + AppVideoServer: + Name: "Video Server und Steuerung" + Docker: "AppVideoServer" + localAdress: "localhost:1010" + robots: ["Robot"] + visiblePort: 8090 + + RoboticsDriver: + Name: "Robotics Driver" + Docker: "RoboticsDriver" + localAdress: "localhost: 2920" + + AppFindZero: + Name: "Set to Zero" + +Network: + Myself: + MyID: "RobotUser Max" # My ID so the Server knows, who I am + MyPublicKey: " ... ... " + MyPrivateKeyPath: "..." # To identify at Server, who saves the public Key + MyLocalIP: "192.168.2.12" # in case the WebBrowser is in the same Network + Server: + Adress: "robotics.schooltech.ch" + ServerPublicKey: "...." + LastServerInfo: "20250101" + +ThisFile: + CheckSum: "...." + LastWorkedOnBy: "...." + + diff --git a/data/status.json b/data/status.json new file mode 100644 index 0000000..8eab168 --- /dev/null +++ b/data/status.json @@ -0,0 +1,242 @@ +{ + "timestamp": "2025-12-27T13:47:09.882287+00:00", + "system": { + "cpu": { + "percent": 19.7, + "load_avg": { + "1m": 0.51220703125, + "5m": 0.27783203125, + "15m": 0.3212890625 + } + }, + "memory": { + "total_mb": 7775.16, + "used_mb": 2945.71, + "percent": 42.5 + } + }, + "containers": [ + { + "id": "d1758990b9", + "name": "reverse-tunnel", + "image": "alpine:latest", + "status": "running", + "state": "running", + "openPorts": [], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 8.95, + "memory_limit_mb": 7775.16, + "memory_percent": 0.12 + } + }, + { + "id": "b00c1a2969", + "name": "AppRobotConfig", + "image": "node:20-alpine", + "status": "running", + "state": "running", + "openPorts": [ + { + "container_port": "80/tcp", + "host_ip": "0.0.0.0", + "host_port": "8080" + }, + { + "container_port": "80/tcp", + "host_ip": "::", + "host_port": "8080" + } + ], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 11.66, + "memory_limit_mb": 7775.16, + "memory_percent": 0.15 + } + }, + { + "id": "c94cc1e834", + "name": "tunnelConnector", + "image": "ghcr.io/linuxserver/openssh-server:latest", + "status": "running", + "state": "running", + "openPorts": [], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 5.53, + "memory_limit_mb": 7775.16, + "memory_percent": 0.07 + } + }, + { + "id": "333745700e", + "name": "nginxAuth", + "image": "nginx:latest", + "status": "running", + "state": "running", + "openPorts": [ + { + "container_port": "443/tcp", + "host_ip": "0.0.0.0", + "host_port": "8082" + }, + { + "container_port": "443/tcp", + "host_ip": "::", + "host_port": "8082" + }, + { + "container_port": "444/tcp", + "host_ip": "0.0.0.0", + "host_port": "444" + }, + { + "container_port": "444/tcp", + "host_ip": "::", + "host_port": "444" + }, + { + "container_port": "80/tcp", + "host_ip": null, + "host_port": null + } + ], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 6.59, + "memory_limit_mb": 7775.16, + "memory_percent": 0.08 + } + }, + { + "id": "16675455a9", + "name": "appvideoserver", + "image": "appvideoserver:latest", + "status": "running", + "state": "running", + "openPorts": [ + { + "container_port": "8443/tcp", + "host_ip": "0.0.0.0", + "host_port": "8448" + }, + { + "container_port": "8443/tcp", + "host_ip": "::", + "host_port": "8448" + } + ], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 327.17, + "memory_limit_mb": 7775.16, + "memory_percent": 4.21 + } + }, + { + "id": "88e8a24141", + "name": "guacamoleTunneled", + "image": "abesnier/guacamole:latest", + "status": "running", + "state": "running", + "openPorts": [ + { + "container_port": "8080/tcp", + "host_ip": "0.0.0.0", + "host_port": "8081" + }, + { + "container_port": "8080/tcp", + "host_ip": "::", + "host_port": "8081" + } + ], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 402.19, + "memory_limit_mb": 7775.16, + "memory_percent": 5.17 + } + }, + { + "id": "73fd1ba6b3", + "name": "RoboticsDriver", + "image": "node:18", + "status": "running", + "state": "running", + "openPorts": [ + { + "container_port": "2095/tcp", + "host_ip": "0.0.0.0", + "host_port": "2095" + }, + { + "container_port": "2095/tcp", + "host_ip": "::", + "host_port": "2095" + } + ], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 46.09, + "memory_limit_mb": 7775.16, + "memory_percent": 0.59 + } + }, + { + "id": "9e8af64136", + "name": "gitea", + "image": "gitea/gitea:latest", + "status": "running", + "state": "running", + "openPorts": [ + { + "container_port": "22/tcp", + "host_ip": null, + "host_port": null + }, + { + "container_port": "2222/tcp", + "host_ip": "0.0.0.0", + "host_port": "3222" + }, + { + "container_port": "2222/tcp", + "host_ip": "::", + "host_port": "3222" + }, + { + "container_port": "3000/tcp", + "host_ip": "0.0.0.0", + "host_port": "3000" + }, + { + "container_port": "3000/tcp", + "host_ip": "::", + "host_port": "3000" + } + ], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 164.53, + "memory_limit_mb": 7775.16, + "memory_percent": 2.12 + } + }, + { + "id": "845c4fc6bf", + "name": "cloudflared", + "image": "cloudflare/cloudflared:latest", + "status": "running", + "state": "running", + "openPorts": [], + "resources": { + "cpu_percent": 0.0, + "memory_mb": 32.57, + "memory_limit_mb": 7775.16, + "memory_percent": 0.42 + } + } + ] +} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100755 index 0000000..00e5566 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,44 @@ + +version: "3.8" + +services: + node-app: + image: node:20-alpine + container_name: AppRobotConfig + user: "root" # allow binding privileged port 80 + working_dir: /app + volumes: + - ./app:/app # mount your Node.js app code here + - ./public:/app/public + - ./data:/app/public/data + command: ["node", "server.js"] + + expose: + - "80" # optional, nur Doku innerhalb des Netzwerks + ports: + - "8080:80" # <-- Host-Port 8080 auf Container-Port 8 + + restart: unless-stopped + + + reverse-tunnel: + image: alpine:latest + container_name: reverse-tunnel + depends_on: + - node-app + volumes: + - ./client/ssh/tunnel_ed25519:/tmp/tunnel_ed25519:ro + - ./client/ssh/known_hosts:/root/.ssh/known_hosts:ro + command: > + sh -c "apk add --no-cache openssh && + mkdir -p /root/.ssh && + install -m 600 /tmp/tunnel_ed25519 /root/.ssh/id_ed25519 && + exec ssh -p 2222 -N -T + -R 0.0.0.0:9000:node-app:80 + -i /root/.ssh/id_ed25519 + -o ExitOnForwardFailure=yes + -o ServerAliveInterval=30 + -o ServerAliveCountMax=3 + tunnel@thinkcentre.local" + + restart: unless-stopped diff --git a/generate.txt b/generate.txt new file mode 100755 index 0000000..faa111f --- /dev/null +++ b/generate.txt @@ -0,0 +1,7 @@ +i love the docker container of cloudflare that creates a tunnel. + +I want to use something simulilar. Give me two docker-compose.yaml + +1) Server where a Port is availiable (that is availiable outside of a firewall). It has the port 4080 that should be routed to the clients :80. and it has a server:4040 for ssh secured access by the client. + +2) Client that lives behind a firewall and connects to that server. That client receives the traffic and hosts not only the tunnel, but also a node.js server. \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100755 index 0000000..8948792 --- /dev/null +++ b/public/index.html @@ -0,0 +1,119 @@ + + + + + + + + System & Docker Status + + + +
+

System & Docker Status

+
Last update: · Auto-refresh on
+
+ +
+
+
+

CPU

+
+
0%
+
+
Load averages
+
1m:
+
5m:
+
15m:
+
+
+
+ +
+

Memory

+
+
+
Used
+
+
/ ()
+
+
+
Tip
+
Keep memory usage under 80% to avoid OOM kills.
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+ + + + + + +`` diff --git a/public/js/status.js b/public/js/status.js new file mode 100755 index 0000000..ebfb5e8 --- /dev/null +++ b/public/js/status.js @@ -0,0 +1,115 @@ + +// Simple status dashboard +const DATA_URL = 'data/status.json'; +let autoRefresh = true; +let refreshTimer = null; + +function formatMB(mb) { + return `${mb.toFixed(2)} MB`; +} +function formatPercent(p) { + return `${p.toFixed(2)}%`; +} +function setRing(el, percent) { + const deg = Math.min(100, Math.max(0, percent)) * 3.6; // 100% -> 360deg + el.style.setProperty('--deg', `${deg}deg`); +} + +function renderSystem(system) { + const cpuPercent = system?.cpu?.percent ?? 0; + const ring = document.getElementById('cpu-ring'); + setRing(ring, cpuPercent); + document.getElementById('cpu-percent').textContent = formatPercent(cpuPercent); + const la = system?.cpu?.load_avg ?? {}; + document.getElementById('load-1m').textContent = (la['1m'] ?? '–'); + document.getElementById('load-5m').textContent = (la['5m'] ?? '–'); + document.getElementById('load-15m').textContent = (la['15m'] ?? '–'); + + const mem = system?.memory ?? {}; + const pct = mem.percent ?? 0; + document.getElementById('mem-bar').style.width = `${pct}%`; + document.getElementById('mem-percent').textContent = formatPercent(pct); + document.getElementById('mem-used').textContent = formatMB(mem.used_mb ?? 0); + document.getElementById('mem-total').textContent = formatMB(mem.total_mb ?? 0); +} + +function portBadge(p) { + const host = p.host_ip ? `${p.host_ip}` : ''; + const hostPort = p.host_port ? `:${p.host_port}` : ''; + const container = p.container_port || ''; + const text = `${host}${hostPort} → ${container}`.trim(); + return `${text || '—'}`; +} + +function renderContainers(list) { + const q = document.getElementById('search').value.toLowerCase(); + const root = document.getElementById('container-list'); + root.innerHTML = ''; + + const filtered = list.filter(c => { + const s = `${c.name} ${c.image}`.toLowerCase(); + return s.includes(q); + }); + + document.getElementById('container-count').textContent = `${filtered.length} / ${list.length} shown`; + + filtered.forEach(c => { + const statusClass = (c.status === 'running') ? 'status' : 'status stopped'; + const ports = (c.openPorts && c.openPorts.length) ? c.openPorts.map(portBadge).join('') : 'No published ports'; + const cpu = c.resources?.cpu_percent ?? 0; + const memMb = c.resources?.memory_mb ?? 0; + const memPct = c.resources?.memory_percent ?? 0; + + const row = document.createElement('div'); + row.className = 'row'; + row.innerHTML = ` +
${c.name}
${c.image}
+
${c.status}
+
${ports}
+
+ CPU: ${formatPercent(cpu)} + Mem: ${formatMB(memMb)} (${formatPercent(memPct)}) +
+
${c.id.slice(0,12)}
+ `; + root.appendChild(row); + }); +} + +async function loadStatus() { + try { + const res = await fetch(`${DATA_URL}?t=${Date.now()}`, { cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + document.getElementById('last-update').textContent = new Date(data.timestamp).toLocaleString(); + renderSystem(data.system); + renderContainers(data.containers || []); + } catch (err) { + console.error('Failed to load status:', err); + document.getElementById('last-update').textContent = 'failed'; + } +} + +function startAutoRefresh() { + if (refreshTimer) clearInterval(refreshTimer); + refreshTimer = setInterval(() => { + if (autoRefresh) loadStatus(); + }, 5000); +} + +// Events +window.addEventListener('DOMContentLoaded', () => { + document.getElementById('toggle-refresh').addEventListener('click', () => { + autoRefresh = !autoRefresh; + document.getElementById('toggle-refresh').textContent = autoRefresh ? 'Pause' : 'Resume'; + document.getElementById('refresh-state').textContent = autoRefresh ? 'on' : 'paused'; + if (autoRefresh) loadStatus(); + }); + document.getElementById('search').addEventListener('input', () => { + // Re-render list using current data in memory by fetching again (cheap) + loadStatus(); + }); + + loadStatus(); + startAutoRefresh(); +}); diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..e824b29 --- /dev/null +++ b/run.bat @@ -0,0 +1 @@ +docker compose -f docker-compose.yaml up -d \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..e824b29 --- /dev/null +++ b/run.sh @@ -0,0 +1 @@ +docker compose -f docker-compose.yaml up -d \ No newline at end of file diff --git a/scripts/getStats.py b/scripts/getStats.py new file mode 100755 index 0000000..d9f651b --- /dev/null +++ b/scripts/getStats.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import json +import time +import psutil +import docker +import os +from datetime import datetime +from datetime import datetime, timezone + +OUTPUT_FILE_NAME = "~/Documents/AppRobotConfig/data/status.json" +OUTPUT_FILE = os.path.expanduser(OUTPUT_FILE_NAME) + +client = docker.from_env() + +def get_system_stats(): + vm = psutil.virtual_memory() + cpu = psutil.cpu_percent(interval=None) + load1, load5, load15 = psutil.getloadavg() + + return { + "cpu": { + "percent": cpu, + "load_avg": { + "1m": load1, + "5m": load5, + "15m": load15 + } + }, + "memory": { + "total_mb": round(vm.total / 1024 / 1024, 2), + "used_mb": round(vm.used / 1024 / 1024, 2), + "percent": vm.percent + } + } + +def calculate_cpu_percent(stats): + print("Calculate Percentage") + cpu_delta = ( + stats["cpu_stats"]["cpu_usage"]["total_usage"] + - stats["precpu_stats"]["cpu_usage"]["total_usage"] + ) + system_delta = ( + stats["cpu_stats"]["system_cpu_usage"] + - stats["precpu_stats"]["system_cpu_usage"] + ) + + if system_delta > 0 and cpu_delta > 0: + cpu_count = len(stats["cpu_stats"]["cpu_usage"].get("percpu_usage", [])) + return round((cpu_delta / system_delta) * cpu_count * 100, 2) + + return 0.0 + +def get_container_stats(): + containers_data = [] + + for container in client.containers.list(all=True): + info = { + "id": container.short_id, + "name": container.name, + "image": container.image.tags[0] if container.image.tags else container.image.short_id, + "status": container.status, + "state": container.attrs["State"]["Status"], + } + + # Add openPorts info + open_ports = [] + network_settings = container.attrs.get("NetworkSettings", {}) + port_bindings = network_settings.get("Ports", {}) + + for container_port, host_bindings in port_bindings.items(): + if host_bindings: + for binding in host_bindings: + open_ports.append({ + "container_port": container_port, + "host_ip": binding.get("HostIp"), + "host_port": binding.get("HostPort") + }) + else: + # Port exposed but not published + open_ports.append({ + "container_port": container_port, + "host_ip": None, + "host_port": None + }) + + info["openPorts"] = open_ports + + + if container.status == "running": + stats = container.stats(stream=False) + + cpu_percent = calculate_cpu_percent(stats) + + mem_usage = stats["memory_stats"]["usage"] + mem_limit = stats["memory_stats"]["limit"] + mem_percent = (mem_usage / mem_limit) * 100 if mem_limit > 0 else 0 + + info["resources"] = { + "cpu_percent": cpu_percent, + "memory_mb": round(mem_usage / 1024 / 1024, 2), + "memory_limit_mb": round(mem_limit / 1024 / 1024, 2), + "memory_percent": round(mem_percent, 2) + } + else: + info["resources"] = None + + containers_data.append(info) + + return containers_data + +def main(): +# while True: + data = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "system": get_system_stats(), + "containers": get_container_stats() + } + + print("output written") + with open(OUTPUT_FILE, "w+") as f: + json.dump(data, f, indent=2) + + #time.sleep(1) + +if __name__ == "__main__": + main() diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..31780c8 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,4 @@ +sudo apt update +sudo apt install -y python3 python3-full +sudo apt install python3-psutil python3-docker +sudo usermod -aG docker $USER \ No newline at end of file