add server background with working terminal
Build and Push Container / build-and-push (push) Successful in 31s

This commit is contained in:
2026-05-28 10:29:02 -04:00
parent 2c6cd1c45b
commit b12029da81
3 changed files with 1110 additions and 57 deletions
+3 -9
View File
@@ -8,6 +8,9 @@
</head>
<body>
<!-- Server Rack Background -->
<div class="server-rack-bg"></div>
<!-- Navigation -->
<nav>
<div class="nav-inner">
@@ -27,16 +30,7 @@
<!-- Hero -->
<section class="hero" id="hero">
<div class="hero-bg">
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
</div>
<div class="hero-content">
<div class="hero-badge">
<span class="dot"></span>
Running my own infrastructure
</div>
<h1>
Hi, I'm <span class="gradient-text">Reese Wells</span><br>
I build and maintain self-hosted systems
+490
View File
@@ -44,3 +44,493 @@ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
}
});
});
// Server rack parallax background
function createServerRack() {
const bgContainer = document.querySelector('.server-rack-bg');
const rackContainer = document.createElement('div');
rackContainer.className = 'rack-container';
const topBar = document.createElement('div');
topBar.className = 'rack-top-bar';
rackContainer.appendChild(topBar);
const unitHeight = 98;
const barHeight = 72;
const totalPageHeight = document.documentElement.scrollHeight;
const totalUnits = Math.max(Math.ceil((totalPageHeight - 100 - barHeight) / unitHeight), 10);
const brands = ['Framework'];
const services = ['Borg', 'Gitea', 'Nextcloud', 'Jellyfin', 'Open WebUI', 'llama.cpp', 'Immich', 'LiteLLM', 'Caddy', 'Nginx', 'Samba', 'Slopbox'];
for (let i = 0; i < totalUnits; i++) {
const unit = document.createElement('div');
unit.className = 'rack-unit';
const leftRail = document.createElement('div');
leftRail.className = 'rail left';
unit.appendChild(leftRail);
const rightRail = document.createElement('div');
rightRail.className = 'rail right';
unit.appendChild(rightRail);
const isSwitch = i > 0 && (i % 7 === 0 || i % 11 === 0);
if (isSwitch) {
const switchFace = document.createElement('div');
switchFace.className = 'eth-switch-face';
const powerLed = document.createElement('div');
powerLed.className = 'eth-switch-led';
switchFace.appendChild(powerLed);
const brandLabel = document.createElement('div');
brandLabel.className = 'eth-brand';
brandLabel.textContent = brands[Math.floor(Math.random() * brands.length)];
switchFace.appendChild(brandLabel);
const portCount = Math.floor(Math.random() * 4) + 5;
const cappedCount = Math.min(portCount, 8);
const row = document.createElement('div');
row.className = 'eth-port-row';
for (let p = 0; p < cappedCount; p++) {
const port = document.createElement('div');
port.className = 'eth-port';
const led = document.createElement('div');
led.className = 'eth-port-led';
if (Math.random() > 0.3) {
led.classList.add('on');
if (Math.random() > 0.7) {
led.classList.add('blink');
}
}
led.style.animationDelay = `${Math.random() * 2}s`;
port.appendChild(led);
row.appendChild(port);
}
switchFace.appendChild(row);
const label = document.createElement('div');
label.className = 'eth-port-label';
label.textContent = `1-${cappedCount}`;
switchFace.appendChild(label);
unit.appendChild(switchFace);
} else {
const face = document.createElement('div');
face.className = 'server-face';
const serverType = Math.random();
const isTerminal = i === 0;
if (isTerminal) {
// Terminal display
const terminal = document.createElement('div');
terminal.className = 'terminal-display';
terminal.setAttribute('tabindex', '0');
const content = document.createElement('div');
content.className = 'terminal-content';
const line1 = document.createElement('div');
line1.innerHTML = '<span class="terminal-prompt">$</span> uptime';
content.appendChild(line1);
const line2 = document.createElement('div');
line2.textContent = 'optional';
content.appendChild(line2);
const line3 = document.createElement('div');
line3.innerHTML = '<span class="terminal-prompt">$</span> ';
content.appendChild(line3);
const cursor = document.createElement('span');
cursor.className = 'terminal-cursor';
line3.appendChild(cursor);
terminal.appendChild(content);
face.appendChild(terminal);
activeTerminal = terminal;
terminal.addEventListener('click', (e) => {
e.stopPropagation();
terminal.focus();
});
const commandHistory = [];
let historyIndex = -1;
terminal.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
const cmdLine = commandHistory[historyIndex];
const lastLine = content.lastElementChild;
lastLine.innerHTML = '<span class="terminal-prompt">$</span> ' + cmdLine + ' ';
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor';
lastLine.appendChild(newCursor);
}
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
const cmdLine = commandHistory[historyIndex];
const lastLine = content.lastElementChild;
lastLine.innerHTML = '<span class="terminal-prompt">$</span> ' + cmdLine + ' ';
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor';
lastLine.appendChild(newCursor);
} else {
historyIndex = -1;
const lastLine = content.lastElementChild;
lastLine.innerHTML = '<span class="terminal-prompt">$</span> ';
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor';
lastLine.appendChild(newCursor);
}
return;
}
if (e.key === 'Enter') {
e.preventDefault();
const lastLine = content.lastElementChild;
const oldCursor = lastLine?.querySelector('.terminal-cursor');
if (oldCursor) oldCursor.remove();
const cmdText = lastLine.textContent.replace(/^\$\s*/, '').trim();
if (cmdText) {
commandHistory.unshift(cmdText);
historyIndex = -1;
}
if (cmdText.startsWith('ls')) {
const output = [
'total 20K',
'drwxr-xr-x 5 reese reese 4.0K May 28 14:30 .',
'drwx------ 42 reese reese 4.0K May 28 10:15 ..',
'-rw-r--r-- 1 reese reese 245 May 27 09:00 .gitignore',
'-rw-r--r-- 1 reese reese 1.2K May 27 11:45 Dockerfile',
'-rw-r--r-- 1 reese reese 4.8K May 28 13:50 index.html',
'-rw-r--r-- 1 reese reese 680 May 27 09:00 nginx.conf',
'-rw-r--r-- 1 reese reese 32K May 28 14:28 profile.jpeg',
'-rw-r--r-- 1 reese reese 14K May 28 12:00 script.js',
'-rw-r--r-- 1 reese reese 42K May 28 14:25 style.css',
'drwxr-xr-x 2 reese reese 4.0K May 27 09:00 src'
].join('\n');
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
outLine.style.color = '#ccc';
outLine.textContent = output;
content.appendChild(outLine);
}
if (cmdText.startsWith('ss')) {
const output = 'Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:80 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:443 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:222 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3000 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3001 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3002 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8096 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*\ntcp LISTEN 0 128 [::]:22 [::]:*';
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
outLine.style.color = '#ccc';
outLine.textContent = output;
content.appendChild(outLine);
}
if (cmdText === 'clear') {
content.innerHTML = '';
}
const commands = {
'pwd': '/home/reese/portfolio',
'whoami': 'reese',
'hostname': 'homelab',
'date': new Date().toString(),
'uname': 'Linux homelab 7.0.0 #1 SMP x86_64 GNU/Linux',
'uname -a': 'Linux homelab 6.8.0 #1 SMP x86_64 GNU/Linux',
'uptime': ' 14:30:00 up 42 days, 3:15, 1 user, load average: 0.42, 0.38, 0.35',
'id': 'uid=1000(reese) gid=1000(reese) groups=1000(reese),985(podman)',
'cat /etc/os-release': 'PRETTY_NAME="Fedora Linux 69 (Workstation Edition)"\nNAME="Fedora Linux"\nVERSION_ID="69"\nVERSION="69 (Workstation Edition)"\nID=fedora\nVARIANT=Workstation Edition\nVARIANT_ID=workstation\nLOGO=fedora-logo-icon\nCPE_NAME="cpe:/o:fedoralinux:fedora:69"\nDEFAULT_HOSTNAME="fedora"\nHOME_URL="https://fedoraproject.org/"\nDOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f69/system-administrators-guide/"\nSUPPORT_URL="https://ask.fedoraproject.org/"\nBUG_REPORT_URL="https://bugzilla.redhat.com/"\nREDHAT_BUGZILLA_PRODUCT="Fedora"\nREDHAT_BUGZILLA_PRODUCT_VERSION=69\nREDHAT_SUPPORT_PRODUCT="Fedora"\nREDHAT_SUPPORT_PRODUCT_VERSION=69',
'cat ~/.ssh/id_ed25519.pub': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpFmKLqRKzMwRe3WkqJvQrN5mHjF2pRn8sT6yUvWxRe reese@homelab',
'docker ps': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
'docker ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
'docker container ls': 'NAMES STATUS PORTS\nborg Up 42 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp\ngitea Up 42 days 0.0.0.0:222->22/tcp, 0.0.0.0:3000->3000/tcp\nnextcloud Up 42 days 0.0.0.0:8080->80/tcp\nimmich Up 42 days 0.0.0.0:3001->3000/tcp, 0.0.0.0:3002->3001/tcp\nopen-webui Up 42 days 0.0.0.0:8081->8080/tcp\njellyfin Up 42 days 0.0.0.0:8096->8096/tcp\nslopbox Up 42 days 0.0.0.0:9090->80/tcp',
'podman ps': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
'podman ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
'podman container ls': 'NAMES STATUS\nhomepage Up 5 hours\nllama Up 42 days',
'systemctl list-units': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
'systemctl list-units --type=service --state=running --no-pager': 'UNIT LOAD ACTIVE SUB DESCRIPTION\nborg.service loaded active running Borg Backup Service\ngitea.service loaded active running Gitea\ndocker.service loaded active running Docker Application Container Engine\nnextcloud.service loaded active running Nextcloud\nimmich.service loaded active running Immich\nopen-webui.service loaded active running Open WebUI\njellyfin.service loaded active running Jellyfin Media Server\nslopbox.service loaded active running Slopbox\nhomepage.service loaded active running Homepage Container',
'ss': 'Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port\ntcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:80 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:443 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:222 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3000 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3001 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:3002 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:8096 0.0.0.0:*\ntcp LISTEN 0 128 0.0.0.0:9090 0.0.0.0:*\ntcp LISTEN 0 128 [::]:22 [::]:*',
'ip addr show': '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN\n inet 127.0.0.1/8 scope host lo\n inet6 ::1/128 scope host\n2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP\n inet 192.168.1.42/24 brd 192.168.1.255 scope global dynamic eth0\n inet6 fe80::1/64 scope link',
'df -h': 'Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 465G 182G 258G 42% /\nudev 16G 0 16G 0% /dev\ntmpfs 16G 2.1M 16G 1% /dev/shm\n/dev/sda1 511M 6.6M 505M 2% /boot/efi\n/dev/sdb1 1.8T 945G 793G 55% /mnt/data\noverlay 1.8T 945G 793G 55% /var/lib/docker/overlay2',
'free -h': ' total used free shared buff/cache available\nMem: 31Gi 8.2Gi 12Gi 512Mi 11Gi 22Gi\nSwap: 2.0Gi 0B 2.0Gi',
'ps aux': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
'ps aux --sort=-%mem | head -10': 'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nreese 1242 2.1 12.4 4285632 3932160 ? Sl May15 42:18 /opt/llama.cpp/build/bin/llama-server --model /models/Qwen3-30B-A3B.Q4_K_M.gguf --port 8082\ndocker 2341 1.2 6.8 8562348 2156032 ? Sl May15 28:45 /usr/bin/dockerd -H fd://\nreese 3456 0.8 3.2 2845632 1015808 ? Sl May15 18:22 /opt/open-webui/server\nreese 4567 0.5 2.1 1562348 665600 ? Sl May15 12:34 /usr/bin/python3 /opt/borg/borgmatic\nroot 5678 0.3 1.4 945632 448000 ? Ssl May15 8:12 /usr/bin/docker-proxy -p tcp:0.0.0.0:80:80\nreese 6789 0.2 1.1 745632 348000 ? Ssl May15 5:45 /usr/bin/podman run --name homepage\nroot 7890 0.1 0.8 545632 256000 ? Ssl May15 3:22 /usr/bin/docker-proxy -p tcp:0.0.0.0:443:443',
'neofetch': ` 'c. reese@homelab\n ,xNMM. ----------------------\n .OMMMMo OS: Fedora Linux 69 (Workstation Edition) x86_64\n OMMM0, Host: custom-build\n .;loddo:' loolloddol;. Kernel: 6.8.0\n cKMMMMMMMMMMNWMMMMMMMMMM0: Uptime: 42 days, 3 hours\n .KMMMMMMMMMMMMMMMMMMMMMMMWd. Packages: 2847 (dnf)\n XMMMMMMMMMMMMMMMMMMMMMMMX. Shell: bash 5.2.26\n;MMMMMMMMMMMMMMMMMMMMMMMM: Resolution: 2560x1440\n:MMMMMMMMMMMMMMMMMMMMMMMM: DE: GNOME 46.1\n.MMMMMMMMMMMMMMMMMMMMMMMMX. WM: Mutter\n kMMMMMMMMMMMMMMMMMMMMMMMMWd. Terminal: /dev/pts/0\n .XMMMMMMMMMMMMMMMMMMMMMMMMMMk CPU: AMD Ryzen 9 7900X (24) @ 5.6GHz\n .XMMMMMMMMMMMMMMMMMMMMMMK. GPU: NVIDIA GeForce RTX 4090\n kMMMMMMMMMMMMMMMMMMMMd GPU: AMD Ryzen Built-in\n ;KMMMMMMMWXXWMMMMMMMk. Memory: 8.2Gi / 31Gi\n .cooc,. .,coo:.\n\n<span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #22c55e">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #eab308">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #3b82f6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #8b5cf6">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span><span style="color: #ec4899">███</span>`,
'help': 'Available commands:\n ls List directory contents\n pwd Print working directory\n whoami Print current user\n hostname Print hostname\n date Print current date/time\n uname Print system information\n uptime Print system uptime\n id Print user identity\n cat Print file contents (try: cat /etc/os-release)\n docker List running containers\n podman List running pods\n systemctl List running systemd services\n ss Show listening ports\n ip Show network interfaces\n df Show disk usage\n free Show memory usage\n ps Show running processes\n neofetch System info display\n help Show this help message',
};
if (cmdText.startsWith('echo ')) {
const output = cmdText.replace(/^echo\s+/, '');
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
outLine.textContent = output;
outLine.style.color = '#ccc';
content.appendChild(outLine);
} else if (commands[cmdText] !== undefined) {
const output = commands[cmdText];
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
if (cmdText === 'neofetch') {
outLine.innerHTML = output;
} else {
outLine.textContent = output;
outLine.style.color = '#ccc';
}
content.appendChild(outLine);
}
const newLine = document.createElement('div');
newLine.innerHTML = '<span class="terminal-prompt">$</span> ';
content.appendChild(newLine);
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor';
newLine.appendChild(newCursor);
content.scrollTop = content.scrollHeight;
} else if (e.key === 'Backspace') {
const lastLine = content.lastElementChild;
if (lastLine) {
const textNodes = [];
lastLine.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '') {
textNodes.push(node);
}
});
if (textNodes.length > 0) {
e.preventDefault();
const lastChar = textNodes[textNodes.length - 1];
if (lastChar.textContent.length > 1) {
lastChar.textContent = lastChar.textContent.slice(0, -1);
} else {
lastChar.remove();
}
}
}
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
const lastLine = content.lastElementChild;
if (lastLine) {
const cursorEl = lastLine.querySelector('.terminal-cursor');
if (cursorEl) {
const textNode = document.createTextNode(e.key);
lastLine.insertBefore(textNode, cursorEl);
}
}
}
});
terminal.addEventListener('focus', () => {
terminal.style.boxShadow = '0 0 8px rgba(234, 179, 8, 0.3), inset 0 0 20px rgba(34, 197, 94, 0.1)';
terminal.style.borderColor = '#eab308';
});
terminal.addEventListener('blur', () => {
terminal.style.boxShadow = '';
terminal.style.borderColor = '#2a2a2e';
});
} else if (serverType < 0.35) {
// Vertical grill + fans
const grillV = document.createElement('div');
grillV.className = 'grill-v';
for (let g = 0; g < 10; g++) {
const bar = document.createElement('div');
bar.className = 'grill-v-bar';
grillV.appendChild(bar);
}
grillV.style.position = 'absolute';
grillV.style.left = '16px';
grillV.style.top = '50%';
grillV.style.transform = 'translateY(-50%)';
face.appendChild(grillV);
const fanContainer = document.createElement('div');
fanContainer.className = 'fan-container';
fanContainer.style.position = 'absolute';
fanContainer.style.right = '16px';
fanContainer.style.top = '50%';
fanContainer.style.transform = 'translateY(-50%)';
fanContainer.style.zIndex = '1';
const fanCount = Math.floor(Math.random() * 2) + 1;
const fanSpeeds = ['fan-slow', 'fan-medium', 'fan-fast'];
for (let f = 0; f < fanCount; f++) {
const fan = document.createElement('div');
fan.className = `fan ${fanSpeeds[Math.floor(Math.random() * fanSpeeds.length)]}`;
const blade = document.createElement('div');
blade.className = 'fan-blade';
fan.appendChild(blade);
const center = document.createElement('div');
center.className = 'fan-center';
fan.appendChild(center);
fanContainer.appendChild(fan);
}
face.appendChild(fanContainer);
const ledGroup = document.createElement('div');
ledGroup.className = 'led-group';
ledGroup.style.position = 'absolute';
ledGroup.style.top = '8px';
ledGroup.style.left = '50%';
ledGroup.style.transform = 'translateX(-50%)';
const ledCount = Math.floor(Math.random() * 3) + 1;
const blinkClasses = ['led-blink-1', 'led-blink-2', 'led-blink-3', 'led-blink-4'];
const colorClasses = ['led-green', 'led-blue', 'led-yellow', 'led-red'];
for (let l = 0; l < ledCount; l++) {
const led = document.createElement('div');
led.className = `led ${colorClasses[Math.floor(Math.random() * colorClasses.length)]} ${blinkClasses[Math.floor(Math.random() * blinkClasses.length)]}`;
led.style.animationDelay = `${Math.random() * 3}s`;
ledGroup.appendChild(led);
}
face.appendChild(ledGroup);
} else if (serverType < 0.7) {
// Horizontal grill
const grillH = document.createElement('div');
grillH.className = 'grill-h';
for (let g = 0; g < 12; g++) {
const bar = document.createElement('div');
bar.className = 'grill-h-bar';
grillH.appendChild(bar);
}
grillH.style.position = 'absolute';
grillH.style.left = '50%';
grillH.style.top = '50%';
grillH.style.transform = 'translate(-50%, -50%)';
face.appendChild(grillH);
const ledGroup = document.createElement('div');
ledGroup.className = 'led-group';
ledGroup.style.position = 'absolute';
ledGroup.style.right = '16px';
ledGroup.style.top = '50%';
ledGroup.style.transform = 'translateY(-50%)';
const ledCount = Math.floor(Math.random() * 3) + 1;
const blinkClasses = ['led-blink-1', 'led-blink-2', 'led-blink-3', 'led-blink-4'];
const colorClasses = ['led-green', 'led-blue', 'led-yellow', 'led-red'];
for (let l = 0; l < ledCount; l++) {
const led = document.createElement('div');
led.className = `led ${colorClasses[Math.floor(Math.random() * colorClasses.length)]} ${blinkClasses[Math.floor(Math.random() * blinkClasses.length)]}`;
led.style.animationDelay = `${Math.random() * 3}s`;
ledGroup.appendChild(led);
}
face.appendChild(ledGroup);
} else {
// Drive bays
const driveBayContainer = document.createElement('div');
driveBayContainer.style.display = 'flex';
driveBayContainer.style.gap = '8px';
driveBayContainer.style.alignItems = 'center';
driveBayContainer.style.position = 'absolute';
driveBayContainer.style.left = '50%';
driveBayContainer.style.top = '50%';
driveBayContainer.style.transform = 'translate(-50%, -50%)';
const driveBays = Math.floor(Math.random() * 3) + 6;
for (let b = 0; b < driveBays; b++) {
const bay = document.createElement('div');
bay.className = 'server-drive-bay';
driveBayContainer.appendChild(bay);
}
face.appendChild(driveBayContainer);
const ledGroup = document.createElement('div');
ledGroup.className = 'led-group';
ledGroup.style.position = 'absolute';
ledGroup.style.right = '10px';
ledGroup.style.top = '50%';
ledGroup.style.transform = 'translateY(-50%)';
const ledCount = Math.floor(Math.random() * 3) + 1;
const blinkClasses = ['led-blink-1', 'led-blink-2', 'led-blink-3', 'led-blink-4'];
const colorClasses = ['led-green', 'led-blue', 'led-yellow', 'led-red'];
for (let l = 0; l < ledCount; l++) {
const led = document.createElement('div');
led.className = `led ${colorClasses[Math.floor(Math.random() * colorClasses.length)]} ${blinkClasses[Math.floor(Math.random() * blinkClasses.length)]}`;
led.style.animationDelay = `${Math.random() * 3}s`;
ledGroup.appendChild(led);
}
face.appendChild(ledGroup);
}
if (!isTerminal) {
const serviceLabel = document.createElement('div');
serviceLabel.className = 'service-label';
serviceLabel.textContent = services[Math.floor(Math.random() * services.length)];
face.appendChild(serviceLabel);
}
unit.appendChild(face);
}
rackContainer.appendChild(unit);
}
const bottomBar = document.createElement('div');
bottomBar.className = 'rack-bottom-bar';
rackContainer.appendChild(bottomBar);
bgContainer.appendChild(rackContainer);
let ticking = false;
function updateParallax() {
const scrolled = window.pageYOffset;
const parallaxSpeed = 0.3;
const translateY = scrolled * parallaxSpeed;
rackContainer.style.transform = `translateX(-50%) translateY(${-translateY}px)`;
ticking = false;
}
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(updateParallax);
ticking = true;
}
});
}
let activeTerminal = null;
createServerRack();
document.getElementById('hero').addEventListener('click', () => {
if (activeTerminal) {
activeTerminal.focus();
}
});
+617 -48
View File
@@ -31,8 +31,617 @@ body {
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
position: relative;
}
/* Server Rack Background */
.server-rack-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
.rack-container {
position: absolute;
left: 50%;
top: 100px;
transform: translateX(-50%);
width: 680px;
transition: transform 0.1s linear;
opacity: 0.4;
pointer-events: auto;
}
.rack-top-bar {
width: 100%;
height: 36px;
background: linear-gradient(180deg, #2a2a30 0%, #222228 100%);
border-radius: 4px 4px 0 0;
position: relative;
margin-bottom: -1px;
border: 1px solid #3a3a40;
border-bottom: none;
}
.rack-top-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
}
.rack-bottom-bar {
width: 100%;
height: 36px;
background: linear-gradient(180deg, #222228 0%, #1e1e22 100%);
border-radius: 0 0 4px 4px;
position: relative;
margin-top: -1px;
border: 1px solid #3a3a40;
border-top: none;
}
.rack-bottom-bar::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
}
/* Ethernet Switch */
.eth-switch-face {
flex: 1;
height: calc(100% - 4px);
margin: 2px 16px;
background: linear-gradient(180deg, #1c1c20 0%, #18181c 100%);
border: 1px solid #2a2a2e;
border-radius: 3px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6px 12px;
gap: 5px;
position: relative;
}
.eth-port-row {
display: flex;
gap: 6px;
align-items: center;
overflow: hidden;
max-width: 100%;
}
.eth-port {
width: 20px;
height: 18px;
background: linear-gradient(180deg, #2a2a2e 0%, #1a1a1e 100%);
border: 1px solid #3a3a40;
border-radius: 2px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.eth-port::before {
content: '';
width: 12px;
height: 6px;
background: linear-gradient(180deg, #111114 0%, #1a1a1e 100%);
border-radius: 1px;
border-bottom: 1px solid #2a2a2e;
}
.eth-port-led {
position: absolute;
top: -2px;
right: -4px;
width: 5px;
height: 5px;
border-radius: 50%;
}
.eth-port-led.on {
background: #ffffff;
box-shadow: 0 0 3px #ffffff;
}
.eth-port-led.blink {
animation: blink1 2s ease-in-out infinite;
}
.eth-port-label {
font-size: 0.55rem;
color: #4a4a50;
font-family: monospace;
letter-spacing: 0.05em;
margin-top: 3px;
font-weight: 600;
}
.service-label {
position: absolute;
bottom: 6px;
left: 50%;
transform: translateX(-50%);
font-size: 0.6rem;
color: #4a4a50;
font-family: monospace;
font-weight: 600;
letter-spacing: 0.05em;
white-space: nowrap;
}
/* Terminal Display */
.terminal-display {
flex: 1;
height: calc(100% - 4px);
margin: 2px 16px;
background: #0a0a0c;
border: 1px solid #2a2a2e;
border-radius: 3px;
padding: 8px 12px;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
cursor: text;
z-index: 2;
}
.terminal-display::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.15) 2px,
rgba(0, 0, 0, 0.15) 4px
);
z-index: 1;
pointer-events: none;
}
.terminal-display:focus {
outline: none;
}
.terminal-content {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.7rem;
color: #22c55e;
position: relative;
z-index: 2;
line-height: 1.6;
overflow-y: auto;
flex: 1;
}
.terminal-prompt {
color: #eab308;
}
.terminal-cursor {
display: inline-block;
width: 7px;
height: 12px;
background: #22c55e;
vertical-align: text-bottom;
animation: cursorBlink 1s step-end infinite;
margin-left: 1px;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.eth-brand {
font-size: 0.65rem;
color: #3a3a40;
font-family: monospace;
font-weight: 600;
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.eth-switch-led {
position: absolute;
top: 6px;
right: 6px;
width: 5px;
height: 5px;
background: #ffffff;
border-radius: 50%;
box-shadow: 0 0 5px #ffffff;
}
.rack-unit {
width: 100%;
height: 90px;
background: linear-gradient(180deg, #1a1a1f 0%, #151518 100%);
border: 1px solid #2a2a30;
border-radius: 2px;
margin-bottom: 8px;
position: relative;
display: flex;
align-items: center;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.03),
0 2px 8px rgba(0, 0, 0, 0.4);
}
.rack-unit::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
}
.rail {
position: absolute;
top: 0;
bottom: 0;
width: 14px;
background: linear-gradient(90deg, #2a2a30 0%, #222228 100%);
z-index: 2;
}
.rail.left {
left: 0;
border-radius: 2px 0 0 2px;
}
.rail.right {
right: 0;
border-radius: 0 2px 2px 0;
}
.rail::before,
.rail::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 8px;
height: 8px;
background: radial-gradient(circle, #1a1a1e 30%, #2a2a30 100%);
border-radius: 50%;
border: 1px solid #3a3a40;
}
.rail::before {
top: 18px;
}
.rail::after {
bottom: 18px;
}
.server-face {
flex: 1;
height: calc(100% - 4px);
margin: 2px 16px;
background: linear-gradient(180deg, #1e1e22 0%, #1a1a1e 100%);
border: 1px solid #2a2a2e;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.server-drive-bay {
width: 64px;
height: 36px;
background: #151518;
border: 1px solid #2a2a2e;
border-radius: 2px;
position: relative;
}
.server-drive-bay::after {
content: '';
position: absolute;
top: 50%;
left: 10px;
width: 28px;
height: 4px;
background: #2a2a2e;
border-radius: 1px;
transform: translateY(-50%);
}
/* Server Grills */
.server-grill {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 1;
}
.grill-h {
display: flex;
flex-direction: column;
gap: 5px;
width: 100%;
padding: 0 8px;
}
.grill-h-bar {
height: 3px;
background: #2a2a2e;
border-radius: 1px;
}
.grill-v {
display: flex;
flex-direction: row;
gap: 5px;
height: 100%;
padding: 10px 0;
}
.grill-v-bar {
width: 3px;
background: #2a2a2e;
border-radius: 1px;
}
/* Fan Animation */
.fan-container {
position: absolute;
display: flex;
gap: 10px;
align-items: center;
}
.fan {
width: 44px;
height: 44px;
border-radius: 50%;
border: 2px solid #2a2a2e;
position: relative;
display: flex;
align-items: center;
justify-content: center;
animation: spin 0.8s linear infinite;
background: radial-gradient(circle, #1a1a1e 0%, transparent 70%);
}
.fan-blade {
position: absolute;
width: 100%;
height: 100%;
}
.fan-blade::before,
.fan-blade::after {
content: '';
position: absolute;
background: #1e1e22;
border: 1px solid #2a2a2e;
}
.fan-blade::before {
width: 75%;
height: 4px;
top: 50%;
left: 12.5%;
transform: translateY(-50%);
border-radius: 2px;
}
.fan-blade::after {
width: 4px;
height: 75%;
left: 50%;
top: 12.5%;
transform: translateX(-50%);
border-radius: 2px;
}
.fan-center {
width: 10px;
height: 10px;
background: #2a2a2e;
border-radius: 50%;
position: relative;
z-index: 2;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.fan-slow {
animation-duration: 1.5s;
}
.fan-medium {
animation-duration: 0.8s;
}
.fan-fast {
animation-duration: 0.4s;
}
.led-group {
display: flex;
gap: 6px;
align-items: center;
}
.led {
width: 6px;
height: 6px;
border-radius: 50%;
box-shadow: 0 0 6px currentColor;
}
.led-green {
background: #22c55e;
color: #22c55e;
}
.led-blue {
background: #3b82f6;
color: #3b82f6;
}
.led-yellow {
background: #eab308;
color: #eab308;
}
.led-red {
background: #ef4444;
color: #ef4444;
}
.led-blink-1 {
animation: blink1 2s ease-in-out infinite;
}
.led-blink-2 {
animation: blink2 3s ease-in-out infinite;
}
.led-blink-3 {
animation: blink3 1.5s ease-in-out infinite;
}
.led-blink-4 {
animation: blink4 2.5s ease-in-out infinite;
}
@keyframes blink1 {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
@keyframes blink2 {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
@keyframes blink3 {
0%, 100% { opacity: 1; }
30% { opacity: 0.1; }
60% { opacity: 0.8; }
}
@keyframes blink4 {
0%, 100% { opacity: 0.5; }
25% { opacity: 1; }
75% { opacity: 0.2; }
}
.rack-mount {
position: absolute;
left: -8px;
width: 8px;
height: 100%;
background: linear-gradient(180deg, #2a2a30 0%, #222228 100%);
border-radius: 2px 0 0 2px;
}
.rack-mount.right {
left: auto;
right: -8px;
border-radius: 0 2px 2px 0;
}
.rack-mount::before,
.rack-mount::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background: #1a1a1e;
border-radius: 50%;
}
.rack-mount::before {
top: 8px;
}
.rack-mount::after {
bottom: 8px;
}
.rack-vertical-bar {
position: absolute;
left: -8px;
width: 16px;
height: 100%;
background: linear-gradient(180deg, #2a2a30 0%, #222228 50%, #2a2a30 100%);
border-radius: 3px;
}
.rack-vertical-bar.right {
left: auto;
right: -8px;
}
.rack-vertical-bar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
180deg,
transparent,
transparent 20px,
rgba(0, 0, 0, 0.3) 20px,
rgba(0, 0, 0, 0.3) 21px
);
}
.rack-label {
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
font-size: 0.55rem;
color: #3a3a40;
font-family: monospace;
letter-spacing: 0.1em;
}
::selection {
background: var(--accent);
color: white;
@@ -134,54 +743,6 @@ nav .nav-inner {
position: relative;
}
.hero-bg {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.hero-bg .orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
animation: float 8s ease-in-out infinite;
}
.hero-bg .orb-1 {
width: 400px;
height: 400px;
background: var(--accent);
top: 10%;
left: 15%;
animation-delay: 0s;
}
.hero-bg .orb-2 {
width: 300px;
height: 300px;
background: #ca8a04;
bottom: 15%;
right: 15%;
animation-delay: -3s;
}
.hero-bg .orb-3 {
width: 200px;
height: 200px;
background: #eab308;
top: 50%;
right: 30%;
animation-delay: -5s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -20px) scale(1.05); }
66% { transform: translate(-20px, 20px) scale(0.95); }
}
.hero-content {
max-width: 800px;
text-align: center;
@@ -867,6 +1428,9 @@ footer {
border-top: 1px solid var(--border);
max-width: 1200px;
margin: 0 auto;
background: var(--bg-primary);
position: relative;
z-index: 10;
}
footer p {
@@ -1010,6 +1574,11 @@ footer p {
flex-direction: column;
gap: 0.25rem;
}
.rack-container {
width: 90vw;
max-width: 320px;
}
}
@media (max-width: 480px) {