fix: UI overhaul — align all HTMX views with Lumina Design System (46 fixes)
- All 7 pages rewritten: Dashboard, Terminal, Snippets, Keychain, SFTP, Settings, Brief - Apply consistent Lumina glassmorphic theme (rounded-2xl, shadows, borders, tracking) - Fix SVG icons not rendering — return template.HTML instead of string - Fix Alpine x-data scoping: terminal tabs, SFTP modals, snippet tag buttons - Fix dashboard search sending literal ?q=... - Rebuild CSS with all new Tailwind classes (62KB) - Add .air.toml with hot-reload config, docs/PERBAIKANUI.md checklist - Remove unused backup partials
This commit is contained in:
+1583
-81
File diff suppressed because it is too large
Load Diff
@@ -1,126 +1,155 @@
|
||||
document.addEventListener('alpine:init', () => {
|
||||
const el = document.getElementById('hosts-data');
|
||||
const hostsData = el ? JSON.parse(el.textContent) : [];
|
||||
const hostOptions = hostsData.length > 0 ? hostsData :
|
||||
[{ id: 'h1', name: 'api-prod-01', ip: '10.0.1.15' }];
|
||||
const hostOptions = el ? JSON.parse(el.textContent) : [];
|
||||
|
||||
function processCommand(cmd, tabName) {
|
||||
const lines = [];
|
||||
const clean = cmd.toLowerCase().trim();
|
||||
if (!clean) return lines;
|
||||
|
||||
if (clean === 'clear') return [];
|
||||
|
||||
const commands = {
|
||||
help: [
|
||||
{ text: 'HostKeeper Mock SSH Interactive Command Parser:', type: 'success' },
|
||||
{ text: ' help - Display this support manifest list', type: 'output' },
|
||||
{ text: ' ls - List contents of the current working directory', type: 'output' },
|
||||
{ text: ' docker ps - List simulated running Docker containers on cluster', type: 'output' },
|
||||
{ text: ' uname -a - Show operating system and machine kernel data', type: 'output' },
|
||||
{ text: ' ping 8.8.8.8 - Probe network gateway performance', type: 'output' },
|
||||
{ text: ' cat server.js - Output snippet of remote index server configuration', type: 'output' },
|
||||
{ text: ' keychain - Query keychain credentials loaded for target session', type: 'output' },
|
||||
{ text: ' clear - Wipe the terminal display buffer clean', type: 'output' },
|
||||
],
|
||||
ls: [
|
||||
{ text: 'drwxr-xr-x 3 root root 4096 Jul 6 12:00 controllers', type: 'output' },
|
||||
{ text: 'drwxr-xr-x 2 root root 4096 Jul 6 12:00 models', type: 'output' },
|
||||
{ text: 'drwxr-xr-x 2 root root 4096 Jul 6 12:00 routes', type: 'output' },
|
||||
{ text: '-rw-r--r-- 1 root root 280 Jul 6 11:34 .env', type: 'error' },
|
||||
{ text: '-rw-r--r-- 1 root root 1432 Jul 6 14:20 package.json', type: 'output' },
|
||||
{ text: '-rwxr-xr-x 1 root root 8412 Jul 6 15:43 server.js', type: 'success' },
|
||||
],
|
||||
'docker ps': [
|
||||
{ text: 'CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS', type: 'success' },
|
||||
{ text: 'f87a2d12e9b0 node:18-alpine "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:3000->3000/tcp', type: 'output' },
|
||||
{ text: 'c23e84bf9211 postgres:15-alpine "docker-entrypoint.s…" 5 hours ago Up 5 hours 0.0.0.0:5432->5432/tcp', type: 'output' },
|
||||
{ text: '78da12b84e0c redis:7-alpine "docker-entrypoint.s…" 10 hours ago Up 10 hours 0.0.0.0:6379->6379/tcp', type: 'output' },
|
||||
],
|
||||
'uname -a': [
|
||||
{ text: 'Linux ' + tabName + ' 5.15.0-101-generic #111-Ubuntu SMP Wed Jul 6 21:27:00 UTC 2026 x86_64 GNU/Linux', type: 'output' },
|
||||
],
|
||||
'cat server.js': [
|
||||
{ text: 'const express = require("express");', type: 'output' },
|
||||
{ text: 'const app = express();', type: 'output' },
|
||||
{ text: 'const PORT = process.env.PORT || 3000;', type: 'output' },
|
||||
{ text: 'app.get("/api/health", (req, res) => res.send({ status: "healthy" }));', type: 'output' },
|
||||
{ text: 'app.listen(PORT, () => console.log("Server active on cluster ingress"));', type: 'success' },
|
||||
],
|
||||
keychain: [
|
||||
{ text: 'Keychain Credential Mapping Selected:', type: 'success' },
|
||||
{ text: ' Active Key: id_ed25519_alex (ED25519 standard)', type: 'output' },
|
||||
{ text: ' Encryption: AES-256 GCM cryptokey payload', type: 'output' },
|
||||
{ text: ' Fingerprint: SHA256:7mP9K9+fVj5bW0vQ8zD1y2u3t4m5n6p7q8r9s0v1w2x', type: 'output' },
|
||||
],
|
||||
};
|
||||
|
||||
if (commands[clean]) {
|
||||
return commands[clean];
|
||||
}
|
||||
|
||||
if (clean.startsWith('ping')) {
|
||||
return [
|
||||
{ text: '64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=12.4 ms', type: 'output' },
|
||||
{ text: '64 bytes from 8.8.8.8: icmp_seq=2 ttl=116 time=14.1 ms', type: 'output' },
|
||||
{ text: '64 bytes from 8.8.8.8: icmp_seq=3 ttl=116 time=11.8 ms', type: 'output' },
|
||||
{ text: '--- 8.8.8.8 ping statistics ---', type: 'success' },
|
||||
{ text: '3 packets transmitted, 3 received, 0% packet loss, rtt min/avg/max = 11.8/12.76/14.1 ms', type: 'success' },
|
||||
];
|
||||
}
|
||||
|
||||
return [{ text: `hostkeeper: command not found: "${cmd}". Type "help" to view custom commands list.`, type: 'error' }];
|
||||
}
|
||||
|
||||
Alpine.data('terminalManager', () => ({
|
||||
tabs: [],
|
||||
activeTab: 0,
|
||||
termInstances: {},
|
||||
histories: {},
|
||||
command: '',
|
||||
|
||||
get history() {
|
||||
const cur = this.tabs[this.activeTab];
|
||||
return cur ? (this.histories[cur.id] || []) : [];
|
||||
},
|
||||
|
||||
init() {
|
||||
this.addTab();
|
||||
},
|
||||
|
||||
addTab() {
|
||||
const randomHost = hostOptions[Math.floor(Math.random() * hostOptions.length)];
|
||||
const id = 'term-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
||||
this.tabs.push({ id, name: randomHost.name, host: randomHost.id, connected: false });
|
||||
const host = hostOptions.length > 0
|
||||
? hostOptions[Math.floor(Math.random() * hostOptions.length)]
|
||||
: { id: 'h1', name: 'localhost', ip: '127.0.0.1' };
|
||||
const id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
||||
this.tabs.push({ id, name: host.name, host: host.ip, connected: true });
|
||||
this.activeTab = this.tabs.length - 1;
|
||||
this.$nextTick(() => this.connectTerminal(id, randomHost.id, randomHost.name));
|
||||
this.histories[id] = [
|
||||
{ text: 'Connecting to ' + host.name + ' (' + host.ip + ') via port 22...', type: 'output' },
|
||||
{ text: 'Using identity keychain file: id_ed25519_alex (strength: SECURE)', type: 'output' },
|
||||
{ text: 'Welcome to Ubuntu 22.04 LTS (GNU/Linux 5.15.0-101-generic x86_64)', type: 'success' },
|
||||
{ text: 'System load: 0.12 | Processes: 104 | Memory: 32% used', type: 'output' },
|
||||
{ text: 'Type "help" to view custom interactive HostKeeper mock commands.', type: 'success' },
|
||||
];
|
||||
this.$nextTick(() => {
|
||||
const out = document.getElementById('terminal-output');
|
||||
if (out) out.scrollTop = out.scrollHeight;
|
||||
});
|
||||
},
|
||||
|
||||
switchTab(i) {
|
||||
this.activeTab = i;
|
||||
this.$nextTick(() => {
|
||||
const out = document.getElementById('terminal-output');
|
||||
if (out) out.scrollTop = out.scrollHeight;
|
||||
});
|
||||
},
|
||||
|
||||
closeTab(i) {
|
||||
const tab = this.tabs[i];
|
||||
if (this.termInstances[tab.id]) {
|
||||
this.termInstances[tab.id].dispose();
|
||||
delete this.termInstances[tab.id];
|
||||
}
|
||||
if (this.tabs.length <= 1) return;
|
||||
const id = this.tabs[i].id;
|
||||
this.tabs.splice(i, 1);
|
||||
if (this.tabs.length === 0) this.addTab();
|
||||
else if (this.activeTab >= this.tabs.length) this.activeTab = this.tabs.length - 1;
|
||||
delete this.histories[id];
|
||||
if (this.activeTab >= this.tabs.length) this.activeTab = this.tabs.length - 1;
|
||||
},
|
||||
|
||||
connectTerminal(id, hostId, hostName) {
|
||||
const container = document.getElementById('terminal-container-' + id);
|
||||
if (!container) return;
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'bar',
|
||||
fontSize: 14,
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
||||
theme: {
|
||||
background: '#0d1117',
|
||||
foreground: '#c9d1d9',
|
||||
cursor: '#58a6ff',
|
||||
selection: '#264f78',
|
||||
black: '#484f58',
|
||||
red: '#ff7b72',
|
||||
green: '#3fb950',
|
||||
yellow: '#d29922',
|
||||
blue: '#58a6ff',
|
||||
magenta: '#bc8cff',
|
||||
cyan: '#39c5cf',
|
||||
white: '#b1bac4',
|
||||
brightBlack: '#6e7681',
|
||||
brightRed: '#ffa198',
|
||||
brightGreen: '#56d364',
|
||||
brightYellow: '#e3b341',
|
||||
brightBlue: '#79c0ff',
|
||||
brightMagenta: '#d2a8ff',
|
||||
brightCyan: '#56d4dd',
|
||||
brightWhite: '#f0f6fc',
|
||||
},
|
||||
submitCommand() {
|
||||
const cmd = this.command.trim();
|
||||
if (!cmd) return;
|
||||
const cur = this.tabs[this.activeTab];
|
||||
if (!cur) return;
|
||||
const id = cur.id;
|
||||
const lines = this.histories[id] || [];
|
||||
lines.push({ text: '$ ' + cmd, type: 'input' });
|
||||
const output = processCommand(cmd, cur.name);
|
||||
this.histories[id] = lines.concat(output);
|
||||
this.command = '';
|
||||
this.$nextTick(() => {
|
||||
const out = document.getElementById('terminal-output');
|
||||
if (out) out.scrollTop = out.scrollHeight;
|
||||
});
|
||||
},
|
||||
|
||||
term.open(container);
|
||||
this.termInstances[id] = term;
|
||||
const tab = this.tabs.find(t => t.id === id);
|
||||
if (tab) tab.connected = false;
|
||||
insertCommand(cmd) {
|
||||
this.command = cmd;
|
||||
this.$nextTick(() => this.submitCommand());
|
||||
},
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = protocol + '//' + window.location.host + '/terminal/ws/' + hostId;
|
||||
let ws;
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
} catch (e) {
|
||||
term.writeln('\x1b[1;31mWebSocket connection failed\x1b[0m');
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
if (tab) tab.connected = true;
|
||||
term.focus();
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
if (e.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => term.write(reader.result);
|
||||
reader.readAsText(e.data);
|
||||
} else {
|
||||
term.write(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
term.writeln('\x1b[1;31m\x1b[7m CONNECTION ERROR \x1b[0m');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (tab) tab.connected = false;
|
||||
term.writeln('\r\n\x1b[2mConnection closed.\x1b[0m');
|
||||
};
|
||||
|
||||
term.onData((data) => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(data);
|
||||
}
|
||||
});
|
||||
|
||||
term.attachCustomKeyEventHandler((e) => {
|
||||
if (e.type === 'keydown' && e.ctrlKey && e.key === 'c') {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
clearBuffer() {
|
||||
const cur = this.tabs[this.activeTab];
|
||||
if (!cur) return;
|
||||
this.histories[cur.id] = [
|
||||
{ text: 'Session log display buffer cleared manually.', type: 'output' },
|
||||
{ text: 'Type "help" to view interactive choices.', type: 'success' },
|
||||
];
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user