Initial commit: Socktop WebTerm with k3s deployment

- Multi-architecture Docker image (ARM64 + AMD64)
- Kubernetes manifests for 3-replica deployment
- Traefik ingress configuration
- NGINX Proxy Manager integration
- ConfigMap-based configuration
- Automated build and deployment scripts
- Session monitoring tools
This commit is contained in:
2025-11-28 01:31:33 -08:00
parent 627073ef2d
commit 6e48c095ab
68 changed files with 12391 additions and 1007 deletions
+93
View File
@@ -0,0 +1,93 @@
# Static Assets Directory
This directory contains custom static assets for the webterm application.
## How to Add Files
Simply place your files in this directory:
```bash
# Example: Add a background image
cp my-background.png static/bg.png
# Example: Add a custom CSS file
cp my-styles.css static/custom.css
# Example: Add a logo
cp logo.svg static/logo.svg
```
## How to Access Files
Files in this directory are served at **TWO** different URL paths:
### Option 1: `/assets/*` Route (Recommended)
Files are automatically served from this directory at `/assets/*`:
```
static/bg.png → http://localhost:8082/assets/bg.png
static/logo.svg → http://localhost:8082/assets/logo.svg
static/custom.css → http://localhost:8082/assets/custom.css
```
**Use in CSS/HTML:**
```css
body {
background-image: url('/assets/bg.png');
}
```
```html
<link rel="stylesheet" href="/assets/custom.css" />
<img src="/assets/logo.svg" alt="Logo" />
```
### Option 2: `/static/*` Route (Manual Copy Required)
If you prefer to use `/static/*` URLs, copy the file to `node_modules/`:
```bash
cp static/bg.png node_modules/
```
Then access it at:
```css
background-image: url('/static/bg.png');
```
**⚠️ Warning:** Files in `node_modules/` may be removed when you run `npm install` or `npm ci`.
## Recommendation
**Use `/assets/*` for your custom assets** because:
- ✅ No need to copy files manually
- ✅ Won't be lost when running npm commands
- ✅ Clear separation between your assets and npm packages
- ✅ Better organization and maintainability
Reserve `/static/*` for npm packages (xterm.js, addons, etc.).
## Current Files
- `terminado-addon.js` - Custom xterm.js addon for Terminado protocol
- `bg.png` - Background image (1.3 MB)
## Server Configuration
The server is configured in `src/server.rs`:
```rust
App::new()
.service(actix_files::Files::new("/assets", "./static")) // This directory
.service(actix_files::Files::new("/static", "./node_modules")) // npm packages
```
## More Information
See `STATIC_ASSETS.md` in the project root for comprehensive documentation on:
- Adding different types of assets (images, fonts, CSS, JS)
- Path reference guide
- Best practices
- Troubleshooting
- Performance optimization
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+418
View File
@@ -0,0 +1,418 @@
/* Catppuccin Frappe Color Palette */
:root {
/* Base colors */
--ctp-base: #303446;
--ctp-mantle: #292c3c;
--ctp-crust: #232634;
/* Surface colors */
--ctp-surface0: #414559;
--ctp-surface1: #51576d;
--ctp-surface2: #626880;
/* Overlay colors */
--ctp-overlay0: #737994;
--ctp-overlay1: #838ba7;
--ctp-overlay2: #949cbb;
/* Text colors */
--ctp-text: #c6d0f5;
--ctp-subtext1: #b5bfe2;
--ctp-subtext0: #a5adce;
/* Accent colors */
--ctp-lavender: #babbf1;
--ctp-blue: #8caaee;
--ctp-sapphire: #85c1dc;
--ctp-sky: #99d1db;
--ctp-teal: #81c8be;
--ctp-green: #a6d189;
--ctp-yellow: #e5c890;
--ctp-peach: #ef9f76;
--ctp-maroon: #ea999c;
--ctp-red: #e78284;
--ctp-mauve: #ca9ee6;
--ctp-pink: #f4b8e4;
--ctp-flamingo: #eebebe;
--ctp-rosewater: #f2d5cf;
/* Layout */
--max-terminal-width: 1200px;
--max-terminal-height: 65vh;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
font-family:
"Inter",
"SF Pro Display",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
background-color: var(--ctp-base);
background-image: url("/static/bg.png");
background-repeat: no-repeat;
background-position: center top;
background-attachment: fixed;
background-size: cover;
color: var(--ctp-text);
line-height: 1.6;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Hero Section */
.hero-section {
text-align: center;
padding: 2rem 2rem 1.5rem 2rem;
max-width: 800px;
margin: 0 auto;
}
.hero-title {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(
135deg,
var(--ctp-mauve) 0%,
var(--ctp-blue) 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
text-shadow: 0 2px 20px rgba(202, 158, 230, 0.3);
}
.hero-tagline {
font-size: 1.125rem;
color: var(--ctp-subtext1);
margin-bottom: 1.5rem;
font-weight: 400;
letter-spacing: 0.01em;
}
/* Links Section */
.links-section {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 2rem;
}
.link-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: rgba(65, 69, 89, 0.6);
border: 1px solid rgba(186, 187, 241, 0.2);
border-radius: 12px;
color: var(--ctp-text);
text-decoration: none;
font-weight: 500;
font-size: 0.95rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.link-button:hover {
background: rgba(81, 87, 109, 0.8);
border-color: var(--ctp-mauve);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(202, 158, 230, 0.2);
}
.link-button i {
font-size: 1.2rem;
}
.link-button.github {
border-color: rgba(186, 187, 241, 0.3);
}
.link-button.github:hover {
border-color: var(--ctp-lavender);
box-shadow: 0 8px 24px rgba(186, 187, 241, 0.25);
}
.link-button.crate {
border-color: rgba(239, 159, 118, 0.3);
}
.link-button.crate:hover {
border-color: var(--ctp-peach);
box-shadow: 0 8px 24px rgba(239, 159, 118, 0.25);
}
.link-button.apt {
border-color: rgba(166, 209, 137, 0.3);
}
.link-button.apt:hover {
border-color: var(--ctp-green);
box-shadow: 0 8px 24px rgba(166, 209, 137, 0.25);
}
/* Terminal Wrapper */
.terminal-wrapper {
flex: 1 1 auto;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 0 2rem 1.5rem 2rem;
}
/* Terminal Window Frame */
.terminal-window {
width: 100%;
max-width: var(--max-terminal-width);
max-height: var(--max-terminal-height);
min-height: var(--max-terminal-height);
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
box-shadow:
0 30px 60px rgba(0, 0, 0, 0.4),
0 12px 24px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(186, 187, 241, 0.1);
background: transparent;
backdrop-filter: blur(20px);
border: 1px solid rgba(186, 187, 241, 0.15);
-webkit-mask-image: -webkit-radial-gradient(white, black);
}
/* Terminal Title Bar */
.terminal-titlebar {
height: 44px;
background: rgba(41, 44, 60, 0.8);
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
padding: 0 16px;
user-select: none;
backdrop-filter: blur(10px);
}
/* Traffic Light Buttons */
.terminal-controls {
display: flex;
gap: 8px;
margin-right: 16px;
}
.terminal-button {
width: 12px;
height: 12px;
border-radius: 50%;
border: 0.5px solid rgba(0, 0, 0, 0.4);
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.terminal-button::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6px;
height: 6px;
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s;
}
.terminal-button:hover::before {
opacity: 1;
}
.terminal-button.close {
background: var(--ctp-red);
}
.terminal-button.close:hover::before {
background: rgba(35, 38, 52, 0.8);
content: "×";
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
width: 8px;
height: 8px;
}
.terminal-button.minimize {
background: var(--ctp-yellow);
}
.terminal-button.minimize:hover::before {
background: rgba(35, 38, 52, 0.8);
}
.terminal-button.maximize {
background: var(--ctp-green);
}
.terminal-button.maximize:hover::before {
background: rgba(35, 38, 52, 0.8);
}
/* Terminal Title */
.terminal-title {
flex: 1;
text-align: center;
color: var(--ctp-subtext1);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.3px;
}
/* Terminal Content */
#terminal {
flex: 1;
overflow: hidden;
background: rgba(48, 52, 70, 0.85);
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
padding: 8px 12px 8px 12px;
}
/* Ensure xterm fills container and respects rounded corners */
.xterm {
height: 100% !important;
background: rgba(48, 52, 70, 0.85) !important;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
.xterm-viewport {
height: 100% !important;
border-bottom-left-radius: 12px !important;
border-bottom-right-radius: 12px !important;
}
.xterm-screen {
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
/* Force canvas to clip at rounded corners */
.xterm canvas {
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
/* Hide scrollbars */
#terminal,
.xterm,
.xterm-viewport {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
#terminal::-webkit-scrollbar,
.xterm::-webkit-scrollbar,
.xterm-viewport::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* Footer */
.site-footer {
padding: 2rem;
text-align: center;
color: var(--ctp-overlay1);
font-size: 0.875rem;
background: rgba(35, 38, 52, 0.3);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(186, 187, 241, 0.1);
}
.site-footer a {
color: var(--ctp-mauve);
text-decoration: none;
transition: color 0.2s;
}
.site-footer a:hover {
color: var(--ctp-lavender);
}
/* Responsive Design */
@media (max-width: 768px) {
.hero-title {
font-size: 2rem;
}
.hero-tagline {
font-size: 1rem;
}
.links-section {
flex-direction: column;
align-items: center;
}
.link-button {
width: 100%;
max-width: 300px;
justify-content: center;
}
.terminal-window {
border-radius: 8px;
}
.terminal-wrapper {
padding: 0 1rem 2rem 1rem;
}
}
@media (max-width: 480px) {
.hero-section {
padding: 2rem 1rem 1.5rem 1rem;
}
.terminal-titlebar {
height: 40px;
}
.terminal-button {
width: 10px;
height: 10px;
}
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Selection colors */
::selection {
background: var(--ctp-mauve);
color: var(--ctp-base);
}
::-moz-selection {
background: var(--ctp-mauve);
color: var(--ctp-base);
}
+182
View File
@@ -0,0 +1,182 @@
/**
* Terminado WebSocket Addon for xterm.js 5.x
*
* This addon handles the Terminado protocol for xterm.js, allowing
* bidirectional communication over WebSocket with a backend PTY process.
*
* The Terminado protocol uses JSON arrays:
* - ["stdin", data] - send input to the terminal
* - ["stdout", data] - receive output from the terminal
* - ["set_size", rows, cols] - notify backend of terminal size changes
*/
class TerminadoAddon {
constructor() {
this._disposables = [];
this._socket = null;
this._bidirectional = true;
this._buffered = true;
this._attachSocketBuffer = '';
this._flushTimeout = null;
}
activate(terminal) {
this._terminal = terminal;
}
dispose() {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
if (this._flushTimeout) {
clearTimeout(this._flushTimeout);
this._flushTimeout = null;
}
if (this._socket) {
this.detach();
}
}
attach(socket, bidirectional = true, buffered = true) {
if (this._socket) {
this.detach();
}
this._socket = socket;
this._bidirectional = bidirectional !== false;
this._buffered = buffered !== false;
// Handle incoming messages from the websocket
this._messageHandler = (ev) => {
try {
const data = JSON.parse(ev.data);
if (Array.isArray(data) && data[0] === 'stdout') {
const output = data[1];
if (this._buffered) {
this._pushToBuffer(output);
} else {
this._terminal.write(output);
}
}
} catch (err) {
console.error('Error handling terminado message:', err);
}
};
// Handle terminal input (user typing)
if (this._bidirectional) {
const dataDisposable = this._terminal.onData((data) => {
this._sendData(data);
});
this._disposables.push(dataDisposable);
}
// Handle terminal resize events
const resizeDisposable = this._terminal.onResize((size) => {
this._setSize(size);
});
this._disposables.push(resizeDisposable);
// Handle socket close/error
this._closeHandler = () => this.detach();
this._errorHandler = () => this.detach();
socket.addEventListener('message', this._messageHandler);
socket.addEventListener('close', this._closeHandler);
socket.addEventListener('error', this._errorHandler);
}
detach() {
if (!this._socket) {
return;
}
if (this._messageHandler) {
this._socket.removeEventListener('message', this._messageHandler);
this._messageHandler = null;
}
if (this._closeHandler) {
this._socket.removeEventListener('close', this._closeHandler);
this._closeHandler = null;
}
if (this._errorHandler) {
this._socket.removeEventListener('error', this._errorHandler);
this._errorHandler = null;
}
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
this._socket = null;
}
_sendData(data) {
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
try {
this._socket.send(JSON.stringify(['stdin', data]));
} catch (err) {
console.error('Error sending data to terminal:', err);
}
}
}
_setSize(size) {
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
try {
this._socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
} catch (err) {
console.error('Error sending terminal size:', err);
}
}
}
_pushToBuffer(data) {
if (this._attachSocketBuffer) {
this._attachSocketBuffer += data;
} else {
this._attachSocketBuffer = data;
if (this._flushTimeout) {
clearTimeout(this._flushTimeout);
}
this._flushTimeout = setTimeout(() => this._flushBuffer(), 10);
}
}
_flushBuffer() {
if (this._attachSocketBuffer && this._terminal) {
this._terminal.write(this._attachSocketBuffer);
this._attachSocketBuffer = '';
}
this._flushTimeout = null;
}
// Public method to manually send size
sendSize(rows, cols) {
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
try {
this._socket.send(JSON.stringify(['set_size', rows, cols]));
} catch (err) {
console.error('Error sending manual terminal size:', err);
}
}
}
// Public method to manually send command
sendCommand(command) {
if (this._socket && this._socket.readyState === WebSocket.OPEN) {
try {
this._socket.send(JSON.stringify(['stdin', command]));
} catch (err) {
console.error('Error sending command:', err);
}
}
}
}
// Export for use in browsers
if (typeof module !== 'undefined' && module.exports) {
module.exports = TerminadoAddon;
}
+177
View File
@@ -0,0 +1,177 @@
// Terminal Initialization Script for socktop webterm
// Catppuccin Frappe theme with transparency
(function() {
'use strict';
// Catppuccin Frappe theme for xterm
var term = new Terminal({
allowTransparency: true,
fontFamily:
'"JetBrains Mono", "Fira Code", "Cascadia Code", Consolas, monospace',
fontSize: 14,
lineHeight: 1.2,
cursorBlink: true,
cursorStyle: "block",
theme: {
background: "rgba(48, 52, 70, 0.75)",
foreground: "#c6d0f5",
cursor: "#f2d5cf",
cursorAccent: "#303446",
selectionBackground: "rgba(202, 158, 230, 0.3)",
// ANSI colors
black: "#51576d",
red: "#e78284",
green: "#a6d189",
yellow: "#e5c890",
blue: "#8caaee",
magenta: "#f4b8e4",
cyan: "#81c8be",
white: "#b5bfe2",
// Bright ANSI colors
brightBlack: "#626880",
brightRed: "#e78284",
brightGreen: "#a6d189",
brightYellow: "#e5c890",
brightBlue: "#8caaee",
brightMagenta: "#f4b8e4",
brightCyan: "#81c8be",
brightWhite: "#a5adce",
},
});
// Create and load the FitAddon
var fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
// Create and load the TerminadoAddon
var terminadoAddon = new TerminadoAddon();
term.loadAddon(terminadoAddon);
// Open terminal in the container
var terminalContainer = document.getElementById("terminal");
term.open(terminalContainer);
// Build websocket URL
var protocol = location.protocol === "https:" ? "wss://" : "ws://";
var socketURL =
protocol +
location.hostname +
(location.port ? ":" + location.port : "") +
window.WEBSOCKET_PATH;
var sock = new WebSocket(socketURL);
// Fit-once strategy
var fitDone = false;
function fitOnceIfReady() {
if (fitDone) return;
if (!terminalContainer) return;
var w = terminalContainer.clientWidth;
var h = terminalContainer.clientHeight;
if (!w || !h) return;
try {
fitAddon.fit();
fitDone = true;
} catch (e) {
console.error("Fit error:", e);
}
}
// Schedule fit
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
requestAnimationFrame(fitOnceIfReady);
} else {
window.addEventListener(
"DOMContentLoaded",
function () {
requestAnimationFrame(fitOnceIfReady);
},
{ once: true },
);
}
window.addEventListener(
"load",
function () {
requestAnimationFrame(fitOnceIfReady);
setTimeout(fitOnceIfReady, 150);
},
{ once: true },
);
// Send size and auto-launch command
var autoSent = false;
function sendSizeAndCommandOnce() {
if (autoSent) return;
if (!sock || sock.readyState !== WebSocket.OPEN) return;
if (!fitDone) return;
var rows = term.rows || 0;
var cols = term.cols || 0;
if (!rows || !cols) {
var approxCharW = 9;
var approxCharH = 18;
cols = Math.max(
1,
Math.floor(terminalContainer.clientWidth / approxCharW),
);
rows = Math.max(
1,
Math.floor(
terminalContainer.clientHeight / approxCharH,
),
);
}
if (rows > 0 && cols > 0) {
try {
terminadoAddon.sendSize(rows, cols);
terminadoAddon.sendCommand("socktop -P local\r");
autoSent = true;
} catch (e) {
console.error("Failed to send initial commands:", e);
}
}
}
// WebSocket event handlers
sock.addEventListener("open", function () {
terminadoAddon.attach(sock, true, true);
requestAnimationFrame(function () {
fitOnceIfReady();
setTimeout(sendSizeAndCommandOnce, 120);
});
});
function onFirstMessage() {
fitOnceIfReady();
try {
sock.removeEventListener("message", onFirstMessage);
} catch (e) {}
setTimeout(sendSizeAndCommandOnce, 40);
}
sock.addEventListener("message", onFirstMessage);
// Handle window resize
window.addEventListener("resize", function () {
try {
fitAddon.fit();
} catch (e) {
console.error("Resize fit error:", e);
}
});
sock.addEventListener("error", function (err) {
console.error("WebSocket error:", err);
});
sock.addEventListener("close", function () {
console.log("WebSocket closed");
});
})();