Compare commits

...

14 Commits

Author SHA1 Message Date
ducoterra 2dcf44bc0b Update profile picture
Build and Push Container / build-and-push (push) Successful in 1m24s
2026-06-05 14:19:54 -04:00
ducoterra d95f6a1cd6 update favicon
Build and Push Container / build-and-push (push) Successful in 28s
2026-06-03 10:17:52 -04:00
ducoterra 136cf16cfd add favicon
Build and Push Container / build-and-push (push) Failing after 34s
2026-06-02 21:06:17 -04:00
ducoterra 2c1beda59f vim working
Build and Push Container / build-and-push (push) Successful in 1m1s
2026-05-31 14:45:49 -04:00
ducoterra 6cc2c8db3a add curl, wget, apt, and dnf
Build and Push Container / build-and-push (push) Successful in 18s
2026-05-31 00:28:21 -04:00
ducoterra 5f0cfbd4cb fix the help command 2026-05-31 00:18:39 -04:00
ducoterra d59d08b7e1 add tab autocomplete and cycling commands
Build and Push Container / build-and-push (push) Successful in 1m11s
2026-05-30 22:47:04 -04:00
ducoterra 13a7306d5b add formatting 2026-05-30 21:32:35 -04:00
ducoterra f34ac3210e I like trains
Build and Push Container / build-and-push (push) Successful in 39s
2026-05-28 21:58:56 -04:00
ducoterra f2246a3386 add actual profile picture
Build and Push Container / build-and-push (push) Successful in 19s
2026-05-28 17:02:01 -04:00
ducoterra 20100800a1 fade in and out terminal on focus
Build and Push Container / build-and-push (push) Successful in 28s
2026-05-28 16:33:56 -04:00
ducoterra 23ed5b510b fix menu fade in and server drive bay overflow
Build and Push Container / build-and-push (push) Successful in 28s
2026-05-28 16:28:46 -04:00
ducoterra 54746f4636 add cache busting
Build and Push Container / build-and-push (push) Successful in 16s
2026-05-28 16:01:42 -04:00
ducoterra 6a30145171 accessibility updates 2026-05-28 15:44:42 -04:00
11 changed files with 2216 additions and 1208 deletions
+3
View File
@@ -1,3 +1,6 @@
# Build output
dist/
# Build artifacts
*.tar
*.tar.gz
+6 -8
View File
@@ -1,16 +1,14 @@
FROM docker.io/library/nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
RUN rm /etc/nginx/conf.d/default.conf && \
apk add --no-cache coreutils
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY src/index.html /usr/share/nginx/html/
COPY src/style.css /usr/share/nginx/html/
COPY src/script.js /usr/share/nginx/html/
COPY src/profile.jpeg /usr/share/nginx/html/
COPY src/ /src/
COPY build.sh /build.sh
RUN chmod +x /build.sh && SRC=/src DIST=/dist /bin/sh /build.sh && cp -r /dist/* /usr/share/nginx/html/ && rm -rf /src /build.sh /dist
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:8080/ || exit 1
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -qO- http://localhost:8080/ || exit 1
+1 -1
View File
@@ -13,7 +13,7 @@ podman build -t homepage:latest .
### Run the container
```bash
podman run --rm -p 8080:8080 -v ./src:/usr/share/nginx/html:z homepage:latest
podman run --rm -p 0.0.0.0:8080:8080 -v ./src:/usr/share/nginx/html:z homepage:latest
```
Visit `http://localhost:8080` in your browser.
Executable
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
set -e
SRC="${SRC:-src}"
DIST="${DIST:-dist}"
# Clean and create dist
rm -rf "$DIST"
mkdir -p "$DIST"
# Copy index.html
cp "$SRC/index.html" "$DIST/index.html"
# Cache-bust assets by appending a content hash to filenames
for ext in css js jpeg png svg ico; do
for file in "$SRC"/*."$ext"; do
[ -f "$file" ] || continue
name=$(basename "$file" | sed "s/\.$ext$//")
hash=$(md5sum "$file" | cut -d' ' -f1)
newname="${name}.${hash}.${ext}"
cp "$file" "$DIST/$newname"
# Update references in HTML
case "$ext" in
css)
sed -i "s|href=\"${name}.${ext}\"|href=\"${newname}\"|g" "$DIST/index.html"
;;
js)
sed -i "s|src=\"${name}.${ext}\"|src=\"${newname}\"|g" "$DIST/index.html"
;;
jpeg|png|svg|ico)
sed -i "s|\"${name}.${ext}\"|\"${newname}\"|g" "$DIST/index.html"
;;
esac
done
done
echo "Build complete: $DIST/"
echo "Assets with cache-busting hashes:"
ls -1 "$DIST"
+14
View File
@@ -17,6 +17,20 @@ server {
gzip_min_length 256;
gzip_vary on;
# Never cache index.html so the latest HTML is always served
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}
# Cache hashed assets forever (filename contains content hash)
location ~* \.(css|js|jpeg|jpg|png|gif|ico|svg|woff2?)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+577 -460
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 125 KiB

+62 -691
View File
@@ -50,56 +50,56 @@ 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) {
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) {
@@ -110,483 +110,26 @@ function createServerRack() {
}
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 mobileInput = document.createElement('input');
mobileInput.type = 'text';
mobileInput.style.opacity = '0';
mobileInput.style.position = 'absolute';
mobileInput.style.width = '1px';
mobileInput.style.height = '1px';
mobileInput.style.padding = '0';
mobileInput.style.border = '0';
mobileInput.style.outline = 'none';
mobileInput.style.background = 'transparent';
mobileInput.setAttribute('autocorrect', 'off');
mobileInput.setAttribute('spellcheck', 'false');
mobileInput.setAttribute('autocomplete', 'off');
mobileInput.setAttribute('autocapitalize', 'off');
mobileInput.autocapitalize = 'off';
terminal.appendChild(mobileInput);
terminal._mobileInput = mobileInput;
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', () => {
mobileInput.focus();
});
const commandHistory = [];
let historyIndex = -1;
let isRoot = false;
let isLoginScreen = false;
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();
mobileInput.value = '';
lastInputValue = '';
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 = '';
}
if (cmdText === 'exit') {
if (!isRoot) {
const newAchievements = checkAchievements(cmdText, false);
if (newAchievements.length > 0) {
revealAchievements();
newAchievements.forEach(a => showToast(a));
}
isLoginScreen = true;
document.body.innerHTML = '';
document.body.style.background = '#000';
document.body.style.color = '#fff';
document.body.style.fontFamily = 'monospace';
document.body.style.padding = '20px';
document.body.style.minHeight = '100vh';
document.body.style.margin = '0';
document.body.style.lineHeight = '1.6';
const loginContainer = document.createElement('div');
loginContainer.style.marginBottom = '10px';
loginContainer.textContent = 'homelab tty1';
document.body.appendChild(loginContainer);
const loginInput = document.createElement('input');
loginInput.type = 'text';
loginInput.style.background = 'transparent';
loginInput.style.border = 'none';
loginInput.style.outline = 'none';
loginInput.style.color = '#fff';
loginInput.style.fontFamily = 'monospace';
loginInput.style.fontSize = '14px';
loginInput.style.width = '300px';
loginInput.style.caretColor = '#fff';
loginInput.autofocus = true;
const loginLine = document.createElement('div');
loginLine.style.marginBottom = '20px';
loginLine.appendChild(document.createTextNode('homelab login: '));
loginLine.appendChild(loginInput);
document.body.appendChild(loginLine);
loginInput.focus();
const handleLogin = (e) => {
if (e.key === 'Enter') {
const deniedLine = document.createElement('div');
deniedLine.textContent = 'Access denied';
deniedLine.style.marginTop = '10px';
document.body.appendChild(deniedLine);
const newLoginLine = document.createElement('div');
newLoginLine.style.marginTop = '10px';
const newInput = document.createElement('input');
newInput.type = 'text';
newInput.style.background = 'transparent';
newInput.style.border = 'none';
newInput.style.outline = 'none';
newInput.style.color = '#fff';
newInput.style.fontFamily = 'monospace';
newInput.style.fontSize = '14px';
newInput.style.width = '300px';
newInput.style.caretColor = '#fff';
newInput.autofocus = true;
newLoginLine.appendChild(document.createTextNode('homelab login: '));
newLoginLine.appendChild(newInput);
document.body.appendChild(newLoginLine);
loginInput.remove();
newInput.focus();
newInput.addEventListener('keydown', handleLogin);
}
};
loginInput.addEventListener('keydown', handleLogin);
return;
} else {
isRoot = false;
}
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;
return;
}
if (cmdText === 'rm -rf /') {
if (!isRoot) {
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
outLine.style.color = '#ef4444';
outLine.textContent = 'nice try';
content.appendChild(outLine);
} else {
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
outLine.style.color = '#ef4444';
outLine.textContent = 'System destruction initiated...';
content.appendChild(outLine);
setTimeout(() => {
document.body.innerHTML = '';
document.title = '';
}, 500);
}
const newLine = document.createElement('div');
if (isRoot) {
newLine.innerHTML = '<span class="terminal-prompt">#</span> ';
newLine.style.color = '#ef4444';
} else {
newLine.innerHTML = '<span class="terminal-prompt">$</span> ';
}
content.appendChild(newLine);
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor' + (isRoot ? ' red' : '');
newLine.appendChild(newCursor);
content.scrollTop = content.scrollHeight;
const newAchievements = checkAchievements(cmdText, isRoot);
if (newAchievements.length > 0) {
revealAchievements();
newAchievements.forEach(a => showToast(a));
}
return;
}
if (cmdText === 'sudo su -' || cmdText === 'sudo -i') {
const outLine = document.createElement('div');
outLine.style.whiteSpace = 'pre-wrap';
outLine.style.color = '#ef4444';
outLine.textContent = 'I hope you know what you\'re doing';
content.appendChild(outLine);
isRoot = true;
const newLine = document.createElement('div');
newLine.innerHTML = '<span class="terminal-prompt">#</span> ';
newLine.style.color = '#ef4444';
content.appendChild(newLine);
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor red';
newLine.appendChild(newCursor);
content.scrollTop = content.scrollHeight;
const newAchievements = checkAchievements(cmdText, isRoot);
if (newAchievements.length > 0) {
revealAchievements();
newAchievements.forEach(a => showToast(a));
}
return;
}
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 newAchievements = checkAchievements(cmdText, isRoot);
if (newAchievements.length > 0) {
revealAchievements();
newAchievements.forEach(a => showToast(a));
}
const newLine = document.createElement('div');
if (isRoot) {
newLine.innerHTML = '<span class="terminal-prompt">#</span> ';
newLine.style.color = '#ef4444';
} else {
newLine.innerHTML = '<span class="terminal-prompt">$</span> ';
}
content.appendChild(newLine);
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor' + (isRoot ? ' red' : '');
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();
}
}
}
}
});
terminal.addEventListener('focus', () => {
mobileInput.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.classList.add('grown');
const face = terminal.closest('.server-face');
const unit = face?.closest('.rack-unit');
face?.classList.add('grown');
unit?.classList.add('grown');
document.getElementById('hero').classList.add('shifted');
});
terminal.addEventListener('blur', () => {
if (heroMouseDown) return;
terminal.style.boxShadow = '';
terminal.style.borderColor = '#2a2a2e';
terminal.classList.remove('grown');
const face = terminal.closest('.server-face');
const unit = face?.closest('.rack-unit');
face?.classList.remove('grown');
unit?.classList.remove('grown');
document.getElementById('hero').classList.remove('shifted');
});
mobileInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
mobileInput.blur();
}
});
mobileInput.addEventListener('blur', () => {
if (heroMouseDown) return;
terminal.style.boxShadow = '';
terminal.style.borderColor = '#2a2a2e';
terminal.classList.remove('grown');
const face = terminal.closest('.server-face');
const unit = face?.closest('.rack-unit');
face?.classList.remove('grown');
unit?.classList.remove('grown');
document.getElementById('hero').classList.remove('shifted');
});
let lastInputValue = '';
mobileInput.addEventListener('input', () => {
const lastLine = content.lastElementChild;
if (!lastLine) return;
const cursorEl = lastLine.querySelector('.terminal-cursor');
const inputText = mobileInput.value;
const addedText = inputText.slice(lastInputValue.length);
if (addedText) {
if (cursorEl) cursorEl.remove();
lastLine.insertAdjacentText('beforeend', addedText);
const newCursor = document.createElement('span');
newCursor.className = 'terminal-cursor' + (isRoot ? ' red' : '');
lastLine.appendChild(newCursor);
}
lastInputValue = inputText;
});
if (i === 0) {
createTerminal(face);
} else if (serverType < 0.35) {
// Vertical grill + fans
const grillV = document.createElement('div');
@@ -601,7 +144,7 @@ function createServerRack() {
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';
@@ -609,46 +152,46 @@ function createServerRack() {
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');
@@ -663,27 +206,27 @@ function createServerRack() {
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');
@@ -694,7 +237,7 @@ function createServerRack() {
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');
@@ -702,59 +245,60 @@ function createServerRack() {
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) {
if (i !== 0) {
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;
// Update the parallax scroll position of the rack container
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);
@@ -763,214 +307,41 @@ function createServerRack() {
});
}
let activeTerminal = null;
createServerRack();
const heroEl = document.getElementById('hero');
let heroMouseDown = false;
// Track mouse state to prevent state collapse during hero click
heroEl.addEventListener('mousedown', () => { heroMouseDown = true; });
heroEl.addEventListener('mouseup', () => { heroMouseDown = false; });
// Toggle terminal expansion when clicking the hero section
heroEl.addEventListener('click', (e) => {
if (e.target.closest('.btn')) return;
if (activeTerminal && activeTerminal._mobileInput) {
if (activeTerminal.classList.contains('grown')) {
activeTerminal.classList.remove('grown');
activeTerminal.classList.remove('opaque');
const face = activeTerminal.closest('.server-face');
const unit = face?.closest('.rack-unit');
face?.classList.remove('grown');
unit?.classList.remove('grown');
heroEl.classList.remove('shifted');
activeTerminal._mobileInput.blur();
document.querySelector('.rack-container')?.classList.remove('opaque');
} else {
activeTerminal._mobileInput.focus();
activeTerminal.classList.add('grown');
activeTerminal.classList.add('opaque');
const face = activeTerminal.closest('.server-face');
const unit = face?.closest('.rack-unit');
face?.classList.add('grown');
unit?.classList.add('grown');
heroEl.classList.add('shifted');
document.querySelector('.rack-container')?.classList.add('opaque');
}
}
});
// Achievement System
const ACHIEVEMENTS_STORAGE_KEY = 'reese-terminal-achievements';
const ACHIEVEMENTS = {
time_flies: { name: 'Time Flies', desc: 'Check the system uptime', icon: '⏱️', cmd: 'uptime' },
curious: { name: 'Curious', desc: 'List directory contents', icon: '📁', cmd: 'ls', prefix: true },
port_scanner: { name: 'Port Scanner', desc: 'Check listening ports', icon: '🔍', cmd: 'ss', prefix: true },
password_reset: { name: 'Password Reset Request', desc: 'Submit a SNOW request', icon: '🚪', cmd: 'exit' },
power_user: { name: 'Power User', desc: 'Gain root access', icon: '👑', cmd: 'sudo su -' },
nice_try: { name: 'Nice Try', desc: 'Attempt rm -rf / as a normal user', icon: '😏', cmd: 'rm -rf /' },
restore_backup: { name: 'Restore from Backup', desc: 'Actually destroy the system as root', icon: '💥', cmd: 'rm -rf / root' },
where_am_i: { name: 'Where Am I?', desc: 'Print working directory', icon: '📍', cmd: 'pwd' },
self_aware: { name: 'Self-Aware', desc: 'Print current user', icon: '🪞', cmd: 'whoami' },
identity: { name: 'Identity Crisis', desc: 'Print hostname', icon: '🖥️', cmd: 'hostname' },
time_keeper: { name: 'Time Keeper', desc: 'Print current date', icon: '📅', cmd: 'date' },
system_explorer: { name: 'System Explorer', desc: 'Print system information', icon: '🔧', cmd: 'uname' },
access_granted: { name: 'Access Granted', desc: 'Check user identity', icon: '🆔', cmd: 'id' },
os_detective: { name: 'OS Detective', desc: 'Read the OS release file', icon: '🐧', cmd: 'cat /etc/os-release' },
ssh_explorer: { name: 'SSH Explorer', desc: 'View SSH public key', icon: '🔑', cmd: 'cat ~/.ssh/id_ed25519.pub' },
container_watcher: { name: 'Container Watcher', desc: 'List Docker containers', icon: '🐳', cmd: 'docker ps', prefix: true },
docker_confused: { name: 'Docker Confused', desc: 'Use wrong Docker command', icon: '🤔', cmd: 'docker ls', prefix: true },
container_pro: { name: 'Container Pro', desc: 'Use correct container command', icon: '✅', cmd: 'docker container ls', prefix: true },
podman_fan: { name: 'Podman Fan', desc: 'List Podman containers', icon: '📦', cmd: 'podman ps', prefix: true },
podman_confused: { name: 'Podman Confused', desc: 'Use wrong Podman command', icon: '🤔', cmd: 'podman ls', prefix: true },
podman_pro: { name: 'Podman Pro', desc: 'Use correct Podman command', icon: '✅', cmd: 'podman container ls', prefix: true },
service_hunter: { name: 'Service Hunter', desc: 'List systemd units', icon: '🔎', cmd: 'systemctl list-units', prefix: true },
service_filter: { name: 'Service Filter', desc: 'Filter running services', icon: '🎯', cmd: 'systemctl list-units --type=service --state=running --no-pager' },
network_ninja: { name: 'Network Ninja', desc: 'Show network interfaces', icon: '🌐', cmd: 'ip addr show', prefix: true },
disk_detective: { name: 'Disk Detective', desc: 'Check disk usage', icon: '💾', cmd: 'df -h' },
memory_minded: { name: 'Memory Minded', desc: 'Check memory usage', icon: '🧠', cmd: 'free -h' },
process_watcher: { name: 'Process Watcher', desc: 'List running processes', icon: '👁️', cmd: 'ps aux', prefix: true },
memory_hog: { name: 'Memory Hog', desc: 'Find top memory consumers', icon: '🐗', cmd: 'ps aux --sort=-%mem | head -10' },
aesthetic_mode: { name: 'Aesthetic Mode', desc: 'Display system info with style', icon: '✨', cmd: 'neofetch' },
help_seeker: { name: 'Help Seeker', desc: 'Look up available commands', icon: '📖', cmd: 'help' },
clean_slate: { name: 'Clean Slate', desc: 'Clear the terminal', icon: '🧹', cmd: 'clear' },
};
function loadAchievements() {
try {
const saved = localStorage.getItem(ACHIEVEMENTS_STORAGE_KEY);
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
}
function saveAchievements(achieved) {
localStorage.setItem(ACHIEVEMENTS_STORAGE_KEY, JSON.stringify(achieved));
}
function checkAchievements(cmdText, isRoot) {
const achieved = loadAchievements();
const newAchievements = [];
for (const [key, achievement] of Object.entries(ACHIEVEMENTS)) {
if (achieved.includes(key)) continue;
if (key === 'restore_backup' && cmdText === 'rm -rf /' && isRoot) {
achieved.push(key);
newAchievements.push(achievement);
} else if (key === 'power_user' && (cmdText === 'sudo su -' || cmdText === 'sudo -i')) {
achieved.push(key);
newAchievements.push(achievement);
} else if (key === 'password_reset' && cmdText === 'exit' && !isRoot) {
achieved.push(key);
newAchievements.push(achievement);
} else if (key === 'nice_try' && cmdText === 'rm -rf /' && !isRoot) {
achieved.push(key);
newAchievements.push(achievement);
} else if (key === 'system_explorer' && (cmdText === 'uname' || cmdText === 'uname -a')) {
achieved.push(key);
newAchievements.push(achievement);
} else if (key === 'docker_confused' && cmdText === 'docker ls') {
achieved.push(key);
newAchievements.push(achievement);
} else if (key === 'podman_confused' && cmdText === 'podman ls') {
achieved.push(key);
newAchievements.push(achievement);
} else if (achievement.prefix ? cmdText.startsWith(achievement.cmd) : achievement.cmd === cmdText) {
if ((key === 'nice_try' && isRoot) || (key === 'restore_backup' && !isRoot)) {
continue;
}
achieved.push(key);
newAchievements.push(achievement);
}
}
if (newAchievements.length > 0) {
saveAchievements(achieved);
}
return newAchievements;
}
function showToast(achievement) {
const container = document.getElementById('toast-container') || createToastContainer();
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<div class="toast-icon">${achievement.icon}</div>
<div class="toast-content">
<div class="toast-label">Achievement Unlocked</div>
<div class="toast-title">${achievement.name}</div>
</div>
`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('removing');
setTimeout(() => toast.remove(), 300);
}, 4000);
}
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toast-container';
container.className = 'toast-container';
document.body.appendChild(container);
return container;
}
function revealAchievements() {
const section = document.getElementById('achievements');
if (section.style.display === 'none' || !section.style.display) {
section.style.display = 'block';
section.classList.add('fade-in');
setTimeout(() => section.classList.add('visible'), 50);
addAchievementsNav();
}
renderAchievements();
}
function renderAchievements() {
const achieved = loadAchievements();
const total = Object.keys(ACHIEVEMENTS).length;
const count = achieved.length;
const countEl = document.getElementById('achievements-count');
countEl.textContent = `${count} / ${total} achievements unlocked`;
const grid = document.getElementById('achievements-grid');
grid.innerHTML = '';
for (const [key, achievement] of Object.entries(ACHIEVEMENTS)) {
const isUnlocked = achieved.includes(key);
const card = document.createElement('div');
card.className = `achievement-card ${isUnlocked ? 'unlocked' : 'locked'}`;
card.innerHTML = `
<div class="achievement-icon">${achievement.icon}</div>
<div class="achievement-info">
<h3>${isUnlocked ? achievement.name : '???'}</h3>
<p>${isUnlocked ? achievement.desc : 'Keep exploring the terminal...'}</p>
</div>
`;
grid.appendChild(card);
}
}
function addAchievementsNav() {
const navLinks = document.getElementById('navLinks');
const existing = document.getElementById('nav-achievements');
if (!existing && loadAchievements().length > 0) {
const li = document.createElement('li');
li.id = 'nav-achievements';
li.innerHTML = '<a href="#achievements">Achievements</a>';
navLinks.appendChild(li);
}
}
// Initialize
(function initAchievements() {
const achieved = loadAchievements();
if (achieved.length > 0) {
revealAchievements();
addAchievementsNav();
}
})();
+160 -48
View File
@@ -1,4 +1,6 @@
*, *::before, *::after {
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
@@ -51,11 +53,15 @@ body {
top: 100px;
transform: translateX(-50%);
width: 680px;
transition: transform 0.1s linear;
transition: transform 0.1s linear, opacity 0.5s ease;
opacity: 0.4;
pointer-events: auto;
}
.rack-container.opaque {
opacity: 1;
}
.rack-top-bar {
width: 100%;
height: 36px;
@@ -201,7 +207,11 @@ body {
position: relative;
overflow: hidden;
cursor: text;
transition: height 0.5s cubic-bezier(0.16, 1, 0.3, 1);
transition: height 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.5s ease;
}
.terminal-display.opaque {
opacity: 1;
}
.terminal-display.grown {
@@ -217,13 +227,11 @@ body {
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
);
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;
}
@@ -261,9 +269,22 @@ body {
background: #ef4444;
}
.vim-block-cursor {
background: #22c55e;
color: #0a0a0c;
border-radius: 1px;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.eth-brand {
@@ -297,7 +318,7 @@ body {
position: relative;
display: flex;
align-items: center;
box-shadow:
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.03),
0 2px 8px rgba(0, 0, 0, 0.4);
overflow: visible;
@@ -370,7 +391,7 @@ body {
align-items: center;
justify-content: center;
position: relative;
overflow: visible;
overflow: hidden;
}
.server-drive-bay {
@@ -498,8 +519,13 @@ body {
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.fan-slow {
@@ -564,25 +590,59 @@ body {
}
@keyframes blink1 {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.2;
}
}
@keyframes blink2 {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 1;
}
}
@keyframes blink3 {
0%, 100% { opacity: 1; }
30% { opacity: 0.1; }
60% { opacity: 0.8; }
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; }
0%,
100% {
opacity: 0.5;
}
25% {
opacity: 1;
}
75% {
opacity: 0.2;
}
}
.rack-mount {
@@ -641,13 +701,11 @@ body {
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
);
background: repeating-linear-gradient(180deg,
transparent,
transparent 20px,
rgba(0, 0, 0, 0.3) 20px,
rgba(0, 0, 0, 0.3) 21px);
}
.rack-label {
@@ -800,8 +858,15 @@ nav .nav-inner {
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.hero h1 {
@@ -1226,12 +1291,30 @@ section {
line-height: 1.8;
}
.code-keyword { color: #c084fc; }
.code-function { color: #60a5fa; }
.code-string { color: #34d399; }
.code-comment { color: #52525b; font-style: italic; }
.code-variable { color: #f472b6; }
.code-operator { color: #fbbf24; }
.code-keyword {
color: #c084fc;
}
.code-function {
color: #60a5fa;
}
.code-string {
color: #34d399;
}
.code-comment {
color: #52525b;
font-style: italic;
}
.code-variable {
color: #f472b6;
}
.code-operator {
color: #fbbf24;
}
.project-info {
padding: 1.5rem;
@@ -1556,7 +1639,7 @@ footer p {
position: fixed;
top: 80px;
right: 20px;
z-index: 1000;
z-index: 10001;
display: flex;
flex-direction: column;
gap: 0.75rem;
@@ -1575,6 +1658,7 @@ footer p {
pointer-events: auto;
animation: toastIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
max-width: 340px;
z-index: 10001;
}
.toast.removing {
@@ -1617,6 +1701,7 @@ footer p {
opacity: 0;
transform: translateX(100px) scale(0.9);
}
to {
opacity: 1;
transform: translateX(0) scale(1);
@@ -1628,6 +1713,7 @@ footer p {
opacity: 1;
transform: translateX(0) scale(1);
}
to {
opacity: 0;
transform: translateX(100px) scale(0.9);
@@ -1671,11 +1757,37 @@ footer p {
transform: translateY(0);
}
.nav-links.active li:nth-child(1) { transition-delay: 0.05s; }
.nav-links.active li:nth-child(2) { transition-delay: 0.1s; }
.nav-links.active li:nth-child(3) { transition-delay: 0.15s; }
.nav-links.active li:nth-child(4) { transition-delay: 0.2s; }
.nav-links.active li:nth-child(5) { transition-delay: 0.25s; }
.nav-links.active li:nth-child(1) {
transition-delay: 0.05s;
}
.nav-links.active li:nth-child(2) {
transition-delay: 0.1s;
}
.nav-links.active li:nth-child(3) {
transition-delay: 0.15s;
}
.nav-links.active li:nth-child(4) {
transition-delay: 0.2s;
}
.nav-links.active li:nth-child(5) {
transition-delay: 0.25s;
}
.nav-links.active li:nth-child(6) {
transition-delay: 0.3s;
}
.nav-links.active li:nth-child(7) {
transition-delay: 0.35s;
}
.nav-links.active li:nth-child(8) {
transition-delay: 0.4s;
}
.hamburger {
display: flex;
@@ -1769,4 +1881,4 @@ footer p {
.about-stats {
grid-template-columns: 1fr;
}
}
}
+1352
View File
File diff suppressed because it is too large Load Diff