Compare commits
14 Commits
96034abeea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
2dcf44bc0b
|
|||
|
d95f6a1cd6
|
|||
|
136cf16cfd
|
|||
|
2c1beda59f
|
|||
|
6cc2c8db3a
|
|||
|
5f0cfbd4cb
|
|||
|
d59d08b7e1
|
|||
|
13a7306d5b
|
|||
|
f34ac3210e
|
|||
|
f2246a3386
|
|||
|
20100800a1
|
|||
|
23ed5b510b
|
|||
|
54746f4636
|
|||
|
6a30145171
|
@@ -1,3 +1,6 @@
|
|||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
*.tar
|
*.tar
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|||||||
+6
-8
@@ -1,16 +1,14 @@
|
|||||||
FROM docker.io/library/nginx:alpine
|
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 nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
COPY src/index.html /usr/share/nginx/html/
|
COPY src/ /src/
|
||||||
COPY src/style.css /usr/share/nginx/html/
|
COPY build.sh /build.sh
|
||||||
COPY src/script.js /usr/share/nginx/html/
|
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
|
||||||
COPY src/profile.jpeg /usr/share/nginx/html/
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD wget -qO- http://localhost:8080/ || exit 1
|
||||||
|
|
||||||
CMD wget -qO- http://localhost:8080/ || exit 1
|
|
||||||
@@ -13,7 +13,7 @@ podman build -t homepage:latest .
|
|||||||
### Run the container
|
### Run the container
|
||||||
|
|
||||||
```bash
|
```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.
|
Visit `http://localhost:8080` in your browser.
|
||||||
|
|||||||
@@ -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
@@ -17,6 +17,20 @@ server {
|
|||||||
gzip_min_length 256;
|
gzip_min_length 256;
|
||||||
gzip_vary on;
|
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 / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
+240
-123
@@ -1,24 +1,25 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
||||||
<meta http-equiv="Pragma" content="no-cache">
|
|
||||||
<meta http-equiv="Expires" content="0">
|
|
||||||
<title>Reese Wells - Self-Hosting & Infrastructure</title>
|
<title>Reese Wells - Self-Hosting & Infrastructure</title>
|
||||||
|
<link rel="icon" type="image/png" href="favicon.ico">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Server Rack Background -->
|
<!-- Server Rack Background -->
|
||||||
<div class="server-rack-bg"></div>
|
<div class="server-rack-bg"></div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav>
|
<nav aria-label="Main navigation">
|
||||||
<div class="nav-inner">
|
<div class="nav-inner">
|
||||||
<div class="logo"><Reese /></div>
|
<div class="logo"><Reese /></div>
|
||||||
<ul class="nav-links" id="navLinks">
|
<ul class="nav-links" id="navLinks" role="list" aria-label="Primary navigation">
|
||||||
|
<li><a href="#hero" aria-current="page">Home</a></li>
|
||||||
<li><a href="#about">About</a></li>
|
<li><a href="#about">About</a></li>
|
||||||
<li><a href="#experience">Experience</a></li>
|
<li><a href="#experience">Experience</a></li>
|
||||||
<li><a href="#skills">Skills</a></li>
|
<li><a href="#skills">Skills</a></li>
|
||||||
@@ -32,8 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<section class="hero" id="hero">
|
<section class="hero" id="hero" aria-label="Introduction">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1>
|
<h1>
|
||||||
Hi, I'm <span class="gradient-text">Reese Wells</span><br>
|
Hi, I'm <span class="gradient-text">Reese Wells</span><br>
|
||||||
@@ -47,7 +49,10 @@
|
|||||||
<div class="hero-buttons">
|
<div class="hero-buttons">
|
||||||
<a href="#projects" class="btn btn-primary">
|
<a href="#projects" class="btn btn-primary">
|
||||||
See My Projects
|
See My Projects
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="#contact" class="btn btn-secondary">Get In Touch</a>
|
<a href="#contact" class="btn btn-secondary">Get In Touch</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,12 +60,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- About -->
|
<!-- About -->
|
||||||
<section id="about">
|
<section id="about" aria-label="About">
|
||||||
<div class="about-grid">
|
<div class="about-grid">
|
||||||
<div class="about-image fade-in">
|
<div class="about-image fade-in">
|
||||||
<div class="avatar-frame">
|
<div class="avatar-frame">
|
||||||
<div class="avatar-inner">
|
<div class="avatar-inner">
|
||||||
<img src="profile.jpeg" alt="Reese Wells" style="width:100%;height:100%;object-fit:cover;border-radius:22px;">
|
<img src="profile.jpeg" alt="Reese Wells"
|
||||||
|
style="width:100%;height:100%;object-fit:cover;border-radius:22px;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,16 +85,16 @@
|
|||||||
services across a fleet of servers. Every service runs as a rootless container with
|
services across a fleet of servers. Every service runs as a rootless container with
|
||||||
dedicated systemd user sessions, backed by centralized BorgBackup.
|
dedicated systemd user sessions, backed by centralized BorgBackup.
|
||||||
</p>
|
</p>
|
||||||
<div class="about-stats">
|
<div class="about-stats" role="list" aria-label="Key statistics">
|
||||||
<div class="stat">
|
<div class="stat" role="listitem">
|
||||||
<div class="stat-number">80+</div>
|
<div class="stat-number">80+</div>
|
||||||
<div class="stat-label">Services</div>
|
<div class="stat-label">Services</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat" role="listitem">
|
||||||
<div class="stat-number">2</div>
|
<div class="stat-number">2</div>
|
||||||
<div class="stat-label">Domains</div>
|
<div class="stat-label">Domains</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat" role="listitem">
|
||||||
<div class="stat-number">100%</div>
|
<div class="stat-number">100%</div>
|
||||||
<div class="stat-label">Self-Hosted</div>
|
<div class="stat-label">Self-Hosted</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,11 +104,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Experience -->
|
<!-- Experience -->
|
||||||
<section id="experience">
|
<section id="experience" aria-label="Work Experience">
|
||||||
<p class="section-label fade-in">Experience</p>
|
<p class="section-label fade-in">Experience</p>
|
||||||
<h2 class="section-title fade-in">Where I've worked</h2>
|
<h2 class="section-title fade-in">Where I've worked</h2>
|
||||||
<div class="timeline">
|
<div class="timeline" role="list" aria-label="Work history">
|
||||||
<div class="timeline-item fade-in">
|
<div class="timeline-item fade-in" role="listitem">
|
||||||
<div class="timeline-marker"></div>
|
<div class="timeline-marker"></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div class="timeline-header">
|
<div class="timeline-header">
|
||||||
@@ -111,16 +117,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="timeline-date">Jan 2022 – Present</span>
|
<span class="timeline-date">Jan 2022 – Present</span>
|
||||||
<ul class="timeline-list">
|
<ul class="timeline-list">
|
||||||
<li>Lead a high-performing DevOps team building microelectronics design services and integrating cloud partner architectures with AWS.</li>
|
<li>Lead a high-performing DevOps team building microelectronics design services and
|
||||||
<li>Recruited and led an engineering team to deliver a machine learning platform in <1 year, utilizing Langchain, PyTorch, and BERT to identify government contract patterns.</li>
|
integrating cloud partner architectures with AWS.</li>
|
||||||
<li>Reduced new feature QA latency from one week to under 24 hours by containerizing complex infrastructure and developing a custom CLI sandbox.</li>
|
<li>Recruited and led an engineering team to deliver a machine learning platform in <1
|
||||||
<li>Led modernization of legacy Django systems using Pydantic, Vue 3, and DRF within strict Authority to Operate boundaries.</li>
|
year, utilizing Langchain, PyTorch, and BERT to identify government contract patterns.
|
||||||
<li>Resolved critical SSSD bugs in collaboration with Red Hat and AWS regarding remote authentication protocols via smart cards.</li>
|
</li>
|
||||||
<li>Developed management frameworks that enabled identification of "shadow teams," leading to three successful organizational restructures.</li>
|
<li>Reduced new feature QA latency from one week to under 24 hours by containerizing complex
|
||||||
|
infrastructure and developing a custom CLI sandbox.</li>
|
||||||
|
<li>Led modernization of legacy Django systems using Pydantic, Vue 3, and DRF within strict
|
||||||
|
Authority to Operate boundaries.</li>
|
||||||
|
<li>Resolved critical SSSD bugs in collaboration with Red Hat and AWS regarding remote
|
||||||
|
authentication protocols via smart cards.</li>
|
||||||
|
<li>Developed management frameworks that enabled identification of "shadow teams," leading
|
||||||
|
to three successful organizational restructures.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-item fade-in">
|
<div class="timeline-item fade-in" role="listitem">
|
||||||
<div class="timeline-marker"></div>
|
<div class="timeline-marker"></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div class="timeline-header">
|
<div class="timeline-header">
|
||||||
@@ -129,13 +142,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="timeline-date">Mar 2021 – Jan 2022</span>
|
<span class="timeline-date">Mar 2021 – Jan 2022</span>
|
||||||
<ul class="timeline-list">
|
<ul class="timeline-list">
|
||||||
<li>Engineered a novel version control system using Python for efficient S3 object retrieval, enforcing malware scanning (ClamAV), data signing/chain of custody, and large binary file branching.</li>
|
<li>Engineered a novel version control system using Python for efficient S3 object
|
||||||
<li>Built an integrated SPA interface in Vue 3 with TypeScript/Vuetify to interact with the proprietary version control system.</li>
|
retrieval, enforcing malware scanning (ClamAV), data signing/chain of custody, and large
|
||||||
<li>Utilized Terraform to define and deploy compliant, scalable cloud environments for DoD Impact Level 5+ high-security requirements.</li>
|
binary file branching.</li>
|
||||||
|
<li>Built an integrated SPA interface in Vue 3 with TypeScript/Vuetify to interact with the
|
||||||
|
proprietary version control system.</li>
|
||||||
|
<li>Utilized Terraform to define and deploy compliant, scalable cloud environments for DoD
|
||||||
|
Impact Level 5+ high-security requirements.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-item fade-in">
|
<div class="timeline-item fade-in" role="listitem">
|
||||||
<div class="timeline-marker"></div>
|
<div class="timeline-marker"></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div class="timeline-header">
|
<div class="timeline-header">
|
||||||
@@ -144,12 +161,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="timeline-date">Jan 2020 – Jan 2021</span>
|
<span class="timeline-date">Jan 2020 – Jan 2021</span>
|
||||||
<ul class="timeline-list">
|
<ul class="timeline-list">
|
||||||
<li>Implemented asynchronous malware scanning pipelines using AWS Lambda (containerized) to process and secure files uploaded by engineering teams in real time via ClamAV.</li>
|
<li>Implemented asynchronous malware scanning pipelines using AWS Lambda (containerized) to
|
||||||
<li>Achieved significant reduction in file management upload overhead through multi-threading, custom indexing solutions, and mtime validation.</li>
|
process and secure files uploaded by engineering teams in real time via ClamAV.</li>
|
||||||
|
<li>Achieved significant reduction in file management upload overhead through
|
||||||
|
multi-threading, custom indexing solutions, and mtime validation.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-item fade-in">
|
<div class="timeline-item fade-in" role="listitem">
|
||||||
<div class="timeline-marker"></div>
|
<div class="timeline-marker"></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div class="timeline-header">
|
<div class="timeline-header">
|
||||||
@@ -158,9 +177,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="timeline-date">May 2018 – Dec 2020</span>
|
<span class="timeline-date">May 2018 – Dec 2020</span>
|
||||||
<ul class="timeline-list">
|
<ul class="timeline-list">
|
||||||
<li>Developed a web application leveraging BFG Repo-Cleaner to scan/remediate secrets in internal Git repositories; prevented an estimated $500k+ in auditing fines.</li>
|
<li>Developed a web application leveraging BFG Repo-Cleaner to scan/remediate secrets in
|
||||||
<li>Modified "PrivacyScanner" tools to detect and alert on leaked PII data within log aggregators, preventing unauthorized exposure of sensitive user information.</li>
|
internal Git repositories; prevented an estimated $500k+ in auditing fines.</li>
|
||||||
<li>Engineered a Django web platform that automated third-party assessment processes; reduced cycle time from weeks to days through strict field validation.</li>
|
<li>Modified "PrivacyScanner" tools to detect and alert on leaked PII data within log
|
||||||
|
aggregators, preventing unauthorized exposure of sensitive user information.</li>
|
||||||
|
<li>Engineered a Django web platform that automated third-party assessment processes;
|
||||||
|
reduced cycle time from weeks to days through strict field validation.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,18 +190,19 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Skills -->
|
<!-- Skills -->
|
||||||
<div class="skills-section" id="skills">
|
<div class="skills-section" id="skills" role="region" aria-label="Skills & Expertise">
|
||||||
<div class="skills-inner">
|
<div class="skills-inner">
|
||||||
<p class="section-label fade-in">Skills & Expertise</p>
|
<p class="section-label fade-in">Skills & Expertise</p>
|
||||||
<h2 class="section-title fade-in">What I work with</h2>
|
<h2 class="section-title fade-in">What I work with</h2>
|
||||||
<p class="section-desc fade-in">
|
<p class="section-desc fade-in">
|
||||||
A broad toolkit focused on infrastructure, automation, and self-hosted services.
|
A broad toolkit focused on infrastructure, automation, and self-hosted services.
|
||||||
</p>
|
</p>
|
||||||
<div class="skills-grid">
|
<div class="skills-grid" role="list" aria-label="Skills">
|
||||||
<div class="skill-card fade-in">
|
<div class="skill-card fade-in" role="listitem">
|
||||||
<div class="skill-icon">⚙</div>
|
<div class="skill-icon">⚙</div>
|
||||||
<h3>Container Orchestration</h3>
|
<h3>Container Orchestration</h3>
|
||||||
<p>Rootless containers managed via Podman quadlets, Docker Compose, and Kubernetes clusters with Helm charts.</p>
|
<p>Rootless containers managed via Podman quadlets, Docker Compose, and Kubernetes clusters with
|
||||||
|
Helm charts.</p>
|
||||||
<div class="skill-tags">
|
<div class="skill-tags">
|
||||||
<span class="skill-tag">Podman</span>
|
<span class="skill-tag">Podman</span>
|
||||||
<span class="skill-tag">Docker</span>
|
<span class="skill-tag">Docker</span>
|
||||||
@@ -187,10 +210,11 @@
|
|||||||
<span class="skill-tag">k3s</span>
|
<span class="skill-tag">k3s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card fade-in">
|
<div class="skill-card fade-in" role="listitem">
|
||||||
<div class="skill-icon">⚛</div>
|
<div class="skill-icon">⚛</div>
|
||||||
<h3>Infrastructure Automation</h3>
|
<h3>Infrastructure Automation</h3>
|
||||||
<p>Ansible playbooks drive deployments across a multi-server fleet with strict SOP ordering and centralized configuration.</p>
|
<p>Ansible playbooks drive deployments across a multi-server fleet with strict SOP ordering and
|
||||||
|
centralized configuration.</p>
|
||||||
<div class="skill-tags">
|
<div class="skill-tags">
|
||||||
<span class="skill-tag">Ansible</span>
|
<span class="skill-tag">Ansible</span>
|
||||||
<span class="skill-tag">osbuild</span>
|
<span class="skill-tag">osbuild</span>
|
||||||
@@ -198,10 +222,11 @@
|
|||||||
<span class="skill-tag">BorgBackup</span>
|
<span class="skill-tag">BorgBackup</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card fade-in">
|
<div class="skill-card fade-in" role="listitem">
|
||||||
<div class="skill-icon">☁</div>
|
<div class="skill-icon">☁</div>
|
||||||
<h3>Networking & DNS</h3>
|
<h3>Networking & DNS</h3>
|
||||||
<p>AWS Route53 powers all DNS management with DDNS auto-updating, Caddy reverse proxy with Route53 DNS-validated TLS, and dual-domain strategy.</p>
|
<p>AWS Route53 powers all DNS management with DDNS auto-updating, Caddy reverse proxy with
|
||||||
|
Route53 DNS-validated TLS, and dual-domain strategy.</p>
|
||||||
<div class="skill-tags">
|
<div class="skill-tags">
|
||||||
<span class="skill-tag">Route53</span>
|
<span class="skill-tag">Route53</span>
|
||||||
<span class="skill-tag">Caddy</span>
|
<span class="skill-tag">Caddy</span>
|
||||||
@@ -209,10 +234,11 @@
|
|||||||
<span class="skill-tag">WireGuard</span>
|
<span class="skill-tag">WireGuard</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card fade-in">
|
<div class="skill-card fade-in" role="listitem">
|
||||||
<div class="skill-icon">🤖</div>
|
<div class="skill-icon">🤖</div>
|
||||||
<h3>Local AI & ML</h3>
|
<h3>Local AI & ML</h3>
|
||||||
<p>Full local AI stack: Ollama, LiteLLM, LocalAI for inference, Langfuse for observability, with CUDA and ROCm support.</p>
|
<p>Full local AI stack: Ollama, LiteLLM, LocalAI for inference, Langfuse for observability, with
|
||||||
|
CUDA and ROCm support.</p>
|
||||||
<div class="skill-tags">
|
<div class="skill-tags">
|
||||||
<span class="skill-tag">Ollama</span>
|
<span class="skill-tag">Ollama</span>
|
||||||
<span class="skill-tag">LocalAI</span>
|
<span class="skill-tag">LocalAI</span>
|
||||||
@@ -220,10 +246,11 @@
|
|||||||
<span class="skill-tag">ROCm</span>
|
<span class="skill-tag">ROCm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="skill-card fade-in">
|
<div class="skill-card fade-in" role="listitem">
|
||||||
<div class="skill-icon">❐</div>
|
<div class="skill-icon">❐</div>
|
||||||
<h3>Python</h3>
|
<h3>Python</h3>
|
||||||
<p>Python is the backbone of the homelab: DDNS updates, fleet-wide deployment scripts, AWS integration, and automation tooling with boto3, rich, and uv.</p>
|
<p>Python is the backbone of the homelab: DDNS updates, fleet-wide deployment scripts, AWS
|
||||||
|
integration, and automation tooling with boto3, rich, and uv.</p>
|
||||||
<div class="skill-tags">
|
<div class="skill-tags">
|
||||||
<span class="skill-tag">Python</span>
|
<span class="skill-tag">Python</span>
|
||||||
<span class="skill-tag">boto3</span>
|
<span class="skill-tag">boto3</span>
|
||||||
@@ -236,29 +263,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Projects -->
|
<!-- Projects -->
|
||||||
<section id="projects">
|
<section id="projects" aria-label="Projects">
|
||||||
<p class="section-label fade-in">Featured Projects</p>
|
<p class="section-label fade-in">Featured Projects</p>
|
||||||
<h2 class="section-title fade-in">What I've built</h2>
|
<h2 class="section-title fade-in">What I've built</h2>
|
||||||
<p class="section-desc fade-in">
|
<p class="section-desc fade-in">
|
||||||
A selection of projects from my homelab and deployment infrastructure.
|
A selection of projects from my homelab and deployment infrastructure.
|
||||||
</p>
|
</p>
|
||||||
<div class="projects-grid">
|
<div class="projects-grid" role="list" aria-label="Projects">
|
||||||
<div class="project-card fade-in">
|
<div class="project-card fade-in" role="listitem">
|
||||||
<div class="project-preview">
|
<div class="project-preview">
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-line"><span class="code-keyword">version</span>: <span class="code-string">'3.8'</span></div>
|
<div class="code-line"><span class="code-keyword">version</span>: <span
|
||||||
|
class="code-string">'3.8'</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">services</span>:</div>
|
<div class="code-line"><span class="code-keyword">services</span>:</div>
|
||||||
<div class="code-line"> <span class="code-function">caddy</span>:</div>
|
<div class="code-line"> <span class="code-function">caddy</span>:</div>
|
||||||
<div class="code-line"> <span class="code-variable">image</span>: <span class="code-string">caddy:2-alpine</span></div>
|
<div class="code-line"> <span class="code-variable">image</span>:
|
||||||
<div class="code-line"> <span class="code-variable">networks</span>:</div>
|
<span class="code-string">caddy:2-alpine</span></div>
|
||||||
<div class="code-line"> - <span class="code-string">default</span></div>
|
<div class="code-line"> <span class="code-variable">networks</span>:
|
||||||
<div class="code-line"> <span class="code-variable">labels</span>:</div>
|
</div>
|
||||||
<div class="code-line"> - <span class="code-string">"caddy.*.reeseapps.com"</span></div>
|
<div class="code-line"> - <span
|
||||||
|
class="code-string">default</span></div>
|
||||||
|
<div class="code-line"> <span class="code-variable">labels</span>:
|
||||||
|
</div>
|
||||||
|
<div class="code-line"> - <span
|
||||||
|
class="code-string">"caddy.*.reeseapps.com"</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3>Reverse Proxy Infrastructure</h3>
|
<h3>Reverse Proxy Infrastructure</h3>
|
||||||
<p>Caddy and Nginx reverse proxies serving all *.reeseapps.com domains with AWS Route53 DNS-validated TLS. DDNS auto-updates IPv4/IPv6 records across the fleet.</p>
|
<p>Caddy and Nginx reverse proxies serving all *.reeseapps.com domains with AWS Route53
|
||||||
|
DNS-validated TLS. DDNS auto-updates IPv4/IPv6 records across the fleet.</p>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span>Caddy</span>
|
<span>Caddy</span>
|
||||||
@@ -267,29 +301,45 @@
|
|||||||
<span>Podman</span>
|
<span>Podman</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-card fade-in">
|
<div class="project-card fade-in" role="listitem">
|
||||||
<div class="project-preview">
|
<div class="project-preview">
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-line"><span class="code-keyword">def</span> <span class="code-function">update_record</span>(<span class="code-variable">domain</span>):</div>
|
<div class="code-line"><span class="code-keyword">def</span> <span
|
||||||
<div class="code-line"> <span class="code-variable">ipv4</span> = <span class="code-function">get_public_ip</span>()</div>
|
class="code-function">update_record</span>(<span
|
||||||
<div class="code-line"> <span class="code-variable">record</span> = <span class="code-function">route53</span>.<span class="code-function">find</span>(<span class="code-variable">domain</span>)</div>
|
class="code-variable">domain</span>):</div>
|
||||||
<div class="code-line"> <span class="code-keyword">if</span> <span class="code-variable">record</span>.<span class="code-variable">value</span> != <span class="code-variable">ipv4</span>:</div>
|
<div class="code-line"> <span class="code-variable">ipv4</span> = <span
|
||||||
<div class="code-line"> <span class="code-function">route53</span>.<span class="code-function">update</span>(<span class="code-variable">record</span>, <span class="code-variable">ipv4</span>)</div>
|
class="code-function">get_public_ip</span>()</div>
|
||||||
<div class="code-line"> <span class="code-function">log</span>(<span class="code-string">f"Updated {domain}"</span>)</div>
|
<div class="code-line"> <span class="code-variable">record</span> = <span
|
||||||
|
class="code-function">route53</span>.<span class="code-function">find</span>(<span
|
||||||
|
class="code-variable">domain</span>)</div>
|
||||||
|
<div class="code-line"> <span class="code-keyword">if</span> <span
|
||||||
|
class="code-variable">record</span>.<span class="code-variable">value</span> !=
|
||||||
|
<span class="code-variable">ipv4</span>:</div>
|
||||||
|
<div class="code-line"> <span
|
||||||
|
class="code-function">route53</span>.<span class="code-function">update</span>(<span
|
||||||
|
class="code-variable">record</span>, <span class="code-variable">ipv4</span>)</div>
|
||||||
|
<div class="code-line"> <span class="code-function">log</span>(<span
|
||||||
|
class="code-string">f"Updated {domain}"</span>)</div>
|
||||||
<div class="code-line"> </div>
|
<div class="code-line"> </div>
|
||||||
<div class="code-line"><span class="code-comment"># Run every 5 minutes</span></div>
|
<div class="code-line"><span class="code-comment"># Run every 5 minutes</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3>Dynamic DNS Service</h3>
|
<h3>Dynamic DNS Service</h3>
|
||||||
<p>Automated DDNS keeping AWS Route53 records updated for all servers. Manages dual-domain strategy: reeseapps.com for public services and reeselink.com for internal machine-to-machine connections.</p>
|
<p>Automated DDNS keeping AWS Route53 records updated for all servers. Manages dual-domain
|
||||||
|
strategy: reeseapps.com for public services and reeselink.com for internal
|
||||||
|
machine-to-machine connections.</p>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span>Python</span>
|
<span>Python</span>
|
||||||
@@ -298,30 +348,43 @@
|
|||||||
<span>Podman</span>
|
<span>Podman</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-card fade-in">
|
<div class="project-card fade-in" role="listitem">
|
||||||
<div class="project-preview">
|
<div class="project-preview">
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-line"><span class="code-keyword">from</span> <span class="code-string">ollama</span> <span class="code-keyword">import</span> <span class="code-function">Client</span></div>
|
<div class="code-line"><span class="code-keyword">from</span> <span
|
||||||
|
class="code-string">ollama</span> <span class="code-keyword">import</span> <span
|
||||||
|
class="code-function">Client</span></div>
|
||||||
<div class="code-line"> </div>
|
<div class="code-line"> </div>
|
||||||
<div class="code-line"><span class="code-variable">client</span> = <span class="code-function">Client</span>(<span class="code-string">"http://localhost:11434"</span>)</div>
|
<div class="code-line"><span class="code-variable">client</span> = <span
|
||||||
<div class="code-line"><span class="code-variable">response</span> = <span class="code-variable">client</span>.<span class="code-function">chat</span>(</div>
|
class="code-function">Client</span>(<span
|
||||||
<div class="code-line"> <span class="code-variable">model</span>=<span class="code-string">"llama3"</span>,</div>
|
class="code-string">"http://localhost:11434"</span>)</div>
|
||||||
|
<div class="code-line"><span class="code-variable">response</span> = <span
|
||||||
|
class="code-variable">client</span>.<span class="code-function">chat</span>(</div>
|
||||||
|
<div class="code-line"> <span class="code-variable">model</span>=<span
|
||||||
|
class="code-string">"llama3"</span>,</div>
|
||||||
<div class="code-line"> <span class="code-variable">messages</span>=[...]</div>
|
<div class="code-line"> <span class="code-variable">messages</span>=[...]</div>
|
||||||
<div class="code-line">)</div>
|
<div class="code-line">)</div>
|
||||||
<div class="code-line"> </div>
|
<div class="code-line"> </div>
|
||||||
<div class="code-line"><span class="code-comment"># LiteLLM proxy for unified API</span></div>
|
<div class="code-line"><span class="code-comment"># LiteLLM proxy for unified API</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3>Local AI Stack</h3>
|
<h3>Local AI Stack</h3>
|
||||||
<p>Complete local AI infrastructure: Ollama and LocalAI for inference, LiteLLM as a unified API proxy, Bifrost for model routing, and Langfuse for observability. Supports both CUDA and ROCm.</p>
|
<p>Complete local AI infrastructure: Ollama and LocalAI for inference, LiteLLM as a unified API
|
||||||
|
proxy, Bifrost for model routing, and Langfuse for observability. Supports both CUDA and
|
||||||
|
ROCm.</p>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span>Ollama</span>
|
<span>Ollama</span>
|
||||||
@@ -330,28 +393,40 @@
|
|||||||
<span>Langfuse</span>
|
<span>Langfuse</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-card fade-in">
|
<div class="project-card fade-in" role="listitem">
|
||||||
<div class="project-preview">
|
<div class="project-preview">
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-line"><span class="code-comment"># Ansible playbook</span></div>
|
<div class="code-line"><span class="code-comment"># Ansible playbook</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- name</span>: <span class="code-function">Deploy Gitea</span></div>
|
<div class="code-line"><span class="code-keyword">- name</span>: <span
|
||||||
<div class="code-line"> <span class="code-variable">hosts</span>: <span class="code-string">gitea</span></div>
|
class="code-function">Deploy Gitea</span></div>
|
||||||
|
<div class="code-line"> <span class="code-variable">hosts</span>: <span
|
||||||
|
class="code-string">gitea</span></div>
|
||||||
<div class="code-line"> <span class="code-variable">tasks</span>:</div>
|
<div class="code-line"> <span class="code-variable">tasks</span>:</div>
|
||||||
<div class="code-line"> - <span class="code-function">docker_compose_v2</span>:</div>
|
<div class="code-line"> - <span class="code-function">docker_compose_v2</span>:
|
||||||
<div class="code-line"> <span class="code-variable">project_src</span>: <span class="code-string">/opt/gitea</span></div>
|
</div>
|
||||||
<div class="code-line"> <span class="code-variable">state</span>: <span class="code-string">present</span></div>
|
<div class="code-line"> <span
|
||||||
|
class="code-variable">project_src</span>: <span
|
||||||
|
class="code-string">/opt/gitea</span></div>
|
||||||
|
<div class="code-line"> <span class="code-variable">state</span>:
|
||||||
|
<span class="code-string">present</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3>Deployment Automation</h3>
|
<h3>Deployment Automation</h3>
|
||||||
<p>Ansible-driven deployment pipeline with strict SOP ordering (osbuild -> ddns -> caddy -> nginx -> ntfy -> gitea). Each service runs as a rootless container with dedicated systemd user sessions and centralized BorgBackup.</p>
|
<p>Ansible-driven deployment pipeline with strict SOP ordering (osbuild -> ddns -> caddy ->
|
||||||
|
nginx -> ntfy -> gitea). Each service runs as a rootless container with dedicated systemd
|
||||||
|
user sessions and centralized BorgBackup.</p>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span>Ansible</span>
|
<span>Ansible</span>
|
||||||
@@ -360,29 +435,42 @@
|
|||||||
<span>Borg</span>
|
<span>Borg</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-card fade-in">
|
<div class="project-card fade-in" role="listitem">
|
||||||
<div class="project-preview">
|
<div class="project-preview">
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-line"><span class="code-comment"># Self-hosted services</span></div>
|
<div class="code-line"><span class="code-comment"># Self-hosted services</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- Immich</span> <span class="code-comment"># Photo/video management</span></div>
|
<div class="code-line"><span class="code-keyword">- Immich</span> <span
|
||||||
<div class="code-line"><span class="code-keyword">- Jellyfin</span> <span class="code-comment"># Media streaming</span></div>
|
class="code-comment"># Photo/video management</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- Nextcloud</span> <span class="code-comment"># Cloud storage & sync</span></div>
|
<div class="code-line"><span class="code-keyword">- Jellyfin</span> <span
|
||||||
<div class="code-line"><span class="code-keyword">- Gitea</span> <span class="code-comment"># Git service</span></div>
|
class="code-comment"># Media streaming</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- Matrix</span> <span class="code-comment"># Chat protocol</span></div>
|
<div class="code-line"><span class="code-keyword">- Nextcloud</span> <span
|
||||||
<div class="code-line"><span class="code-keyword">- Home Assistant</span> <span class="code-comment"># Smart home</span></div>
|
class="code-comment"># Cloud storage & sync</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- Pi-hole</span> <span class="code-comment"># DNS ad blocking</span></div>
|
<div class="code-line"><span class="code-keyword">- Gitea</span> <span
|
||||||
|
class="code-comment"># Git service</span></div>
|
||||||
|
<div class="code-line"><span class="code-keyword">- Matrix</span> <span
|
||||||
|
class="code-comment"># Chat protocol</span></div>
|
||||||
|
<div class="code-line"><span class="code-keyword">- Home Assistant</span> <span
|
||||||
|
class="code-comment"># Smart home</span></div>
|
||||||
|
<div class="code-line"><span class="code-keyword">- Pi-hole</span> <span
|
||||||
|
class="code-comment"># DNS ad blocking</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3>Self-Hosted Services</h3>
|
<h3>Self-Hosted Services</h3>
|
||||||
<p>A diverse fleet of self-hosted services: Immich for photos, Jellyfin for media, Nextcloud for storage, Matrix for chat, Home Assistant for IoT, and more. Each running as rootless Podman containers with SELinux awareness.</p>
|
<p>A diverse fleet of self-hosted services: Immich for photos, Jellyfin for media, Nextcloud for
|
||||||
|
storage, Matrix for chat, Home Assistant for IoT, and more. Each running as rootless Podman
|
||||||
|
containers with SELinux awareness.</p>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span>Immich</span>
|
<span>Immich</span>
|
||||||
@@ -391,28 +479,40 @@
|
|||||||
<span>Matrix</span>
|
<span>Matrix</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-card fade-in">
|
<div class="project-card fade-in" role="listitem">
|
||||||
<div class="project-preview">
|
<div class="project-preview">
|
||||||
<div class="code-block">
|
<div class="code-block">
|
||||||
<div class="code-line"><span class="code-comment"># Kubernetes with k3s</span></div>
|
<div class="code-line"><span class="code-comment"># Kubernetes with k3s</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- metallb</span> <span class="code-comment"># L2 load balancer</span></div>
|
<div class="code-line"><span class="code-keyword">- metallb</span> <span
|
||||||
<div class="code-line"><span class="code-keyword">- longhorn</span> <span class="code-comment"># Distributed storage</span></div>
|
class="code-comment"># L2 load balancer</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- traefik</span> <span class="code-comment"># Ingress gateway</span></div>
|
<div class="code-line"><span class="code-keyword">- longhorn</span> <span
|
||||||
<div class="code-line"><span class="code-keyword">- external-dns</span> <span class="code-comment"># Route53 integration</span></div>
|
class="code-comment"># Distributed storage</span></div>
|
||||||
<div class="code-line"><span class="code-keyword">- grafana</span> <span class="code-comment"># Metrics dashboards</span></div>
|
<div class="code-line"><span class="code-keyword">- traefik</span> <span
|
||||||
<div class="code-line"><span class="code-keyword">- minecraft</span> <span class="code-comment"># Game server</span></div>
|
class="code-comment"># Ingress gateway</span></div>
|
||||||
|
<div class="code-line"><span class="code-keyword">- external-dns</span> <span
|
||||||
|
class="code-comment"># Route53 integration</span></div>
|
||||||
|
<div class="code-line"><span class="code-keyword">- grafana</span> <span
|
||||||
|
class="code-comment"># Metrics dashboards</span></div>
|
||||||
|
<div class="code-line"><span class="code-keyword">- minecraft</span> <span
|
||||||
|
class="code-comment"># Game server</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3>Kubernetes Cluster</h3>
|
<h3>Kubernetes Cluster</h3>
|
||||||
<p>k3s and k0s Kubernetes clusters with MetalLB for L2 failover, Longhorn for distributed storage, Traefik/Nginx ingress, cert-manager with Route53 DNS challenge, and Helm charts for service deployment.</p>
|
<p>k3s and k0s Kubernetes clusters with MetalLB for L2 failover, Longhorn for distributed
|
||||||
|
storage, Traefik/Nginx ingress, cert-manager with Route53 DNS challenge, and Helm charts for
|
||||||
|
service deployment.</p>
|
||||||
<div class="project-meta">
|
<div class="project-meta">
|
||||||
<div class="project-tech">
|
<div class="project-tech">
|
||||||
<span>k3s</span>
|
<span>k3s</span>
|
||||||
@@ -421,8 +521,12 @@
|
|||||||
<span>MetalLB</span>
|
<span>MetalLB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" title="Source" target="_blank"
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
rel="noopener">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,24 +536,29 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Contact -->
|
<!-- Contact -->
|
||||||
<section class="contact-section" id="contact">
|
<section class="contact-section" id="contact" aria-label="Contact">
|
||||||
<p class="section-label fade-in">Get In Touch</p>
|
<p class="section-label fade-in">Get In Touch</p>
|
||||||
<h2 class="section-title fade-in">Let's connect</h2>
|
<h2 class="section-title fade-in">Let's connect</h2>
|
||||||
<p class="section-desc fade-in">
|
<p class="section-desc fade-in">
|
||||||
Always open to discussing self-hosting, infrastructure, open source, or just sharing homelab stories.
|
Always open to discussing self-hosting, infrastructure, open source, or just sharing homelab stories.
|
||||||
</p>
|
</p>
|
||||||
<div class="contact-links fade-in">
|
<div class="contact-links fade-in" role="list" aria-label="Contact links">
|
||||||
<a href="mailto:reese.wells@ducoterra.net" class="contact-link">
|
<a href="mailto:reese.wells@ducoterra.net" class="contact-link" role="listitem">
|
||||||
<span class="link-icon">✉</span>
|
<span class="link-icon">✉</span>
|
||||||
<span>Email</span>
|
<span>Email</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gitea.reeseapps.com/services/homelab" class="contact-link" target="_blank" rel="noopener">
|
<a href="https://gitea.reeseapps.com/services/homelab" class="contact-link" target="_blank"
|
||||||
|
rel="noopener" role="listitem">
|
||||||
<span class="link-icon">
|
<span class="link-icon">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span>Gitea</span>
|
<span>Gitea</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/in/reesewells/" class="contact-link" target="_blank" rel="noopener">
|
<a href="https://www.linkedin.com/in/reesewells/" class="contact-link" target="_blank" rel="noopener"
|
||||||
|
role="listitem">
|
||||||
<span class="link-icon">in</span>
|
<span class="link-icon">in</span>
|
||||||
<span>LinkedIn</span>
|
<span>LinkedIn</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -457,14 +566,14 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- GPG Keys -->
|
<!-- GPG Keys -->
|
||||||
<section class="gpg-section" id="gpg">
|
<section class="gpg-section" id="gpg" aria-label="Public GPG Keys">
|
||||||
<p class="section-label fade-in">Trust</p>
|
<p class="section-label fade-in">Trust</p>
|
||||||
<h2 class="section-title fade-in">Public GPG Keys</h2>
|
<h2 class="section-title fade-in">Public GPG Keys</h2>
|
||||||
<p class="section-desc fade-in" style="margin-left:auto;margin-right:auto;text-align:center;">
|
<p class="section-desc fade-in" style="margin-left:auto;margin-right:auto;text-align:center;">
|
||||||
Use these keys to verify signed commits and communications. Both keys belong to Reese Wells.
|
Use these keys to verify signed commits and communications. Both keys belong to Reese Wells.
|
||||||
</p>
|
</p>
|
||||||
<div class="gpg-keys-grid">
|
<div class="gpg-keys-grid" role="list" aria-label="GPG keys">
|
||||||
<div class="gpg-key-card fade-in">
|
<div class="gpg-key-card fade-in" role="listitem">
|
||||||
<div class="gpg-key-header">
|
<div class="gpg-key-header">
|
||||||
<span class="gpg-key-icon">🔒</span>
|
<span class="gpg-key-icon">🔒</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -527,7 +636,7 @@ BQ==
|
|||||||
=U3eP
|
=U3eP
|
||||||
-----END PGP PUBLIC KEY BLOCK-----</code></pre>
|
-----END PGP PUBLIC KEY BLOCK-----</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="gpg-key-card fade-in">
|
<div class="gpg-key-card fade-in" role="listitem">
|
||||||
<div class="gpg-key-header">
|
<div class="gpg-key-header">
|
||||||
<span class="gpg-key-icon">🔒</span>
|
<span class="gpg-key-icon">🔒</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -563,24 +672,32 @@ XwEAnes79w4eYeMUjIytQWACEvy4QoO7X2MLTKliSqc4Ag8=
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Achievements Tracker -->
|
<!-- Achievements Tracker -->
|
||||||
<section id="achievements" class="achievements-section" style="display: none;">
|
<section id="achievements" class="achievements-section" hidden aria-label="Achievements">
|
||||||
<div class="section-label">Achievements</div>
|
<div class="section-label">Achievements</div>
|
||||||
<div class="section-title">Terminal Achievements</div>
|
<div class="section-title">Terminal Achievements</div>
|
||||||
<p class="section-desc" style="margin: 0 auto 2rem;">Explore every corner of the terminal to unlock achievements.</p>
|
<p class="section-desc" style="margin: 0 auto 2rem;">Explore every corner of the terminal to unlock
|
||||||
<div id="achievements-count" style="text-align: center; margin-bottom: 2rem; color: var(--text-muted); font-size: 0.9rem;"></div>
|
achievements.</p>
|
||||||
|
<div id="achievements-count"
|
||||||
|
style="text-align: center; margin-bottom: 2rem; color: var(--text-muted); font-size: 0.9rem;"></div>
|
||||||
<div id="achievements-grid" class="achievements-grid"></div>
|
<div id="achievements-grid" class="achievements-grid"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer>
|
</main>
|
||||||
|
<footer role="contentinfo">
|
||||||
<p style="font-size: 0.75rem; color: var(--text-muted);">
|
<p style="font-size: 0.75rem; color: var(--text-muted);">
|
||||||
Built with a 100% self-hosted LLM stack running
|
Built with a 100% self-hosted LLM stack running
|
||||||
<a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener" style="color: var(--accent-hover); text-decoration: none;">llama.cpp</a>,
|
<a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener"
|
||||||
<a href="https://huggingface.co/Qwen/Qwen3.6-35B-A3B" target="_blank" rel="noopener" style="color: var(--accent-hover); text-decoration: none;">Qwen3.6</a>, and
|
style="color: var(--accent-hover); text-decoration: none;">llama.cpp</a>,
|
||||||
<a href="https://opencode.ai/" target="_blank" rel="noopener" style="color: var(--accent-hover); text-decoration: none;">Opencode</a>
|
<a href="https://huggingface.co/Qwen/Qwen3.6-35B-A3B" target="_blank" rel="noopener"
|
||||||
|
style="color: var(--accent-hover); text-decoration: none;">Qwen3.6</a>, and
|
||||||
|
<a href="https://opencode.ai/" target="_blank" rel="noopener"
|
||||||
|
style="color: var(--accent-hover); text-decoration: none;">Opencode</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script src="terminal.js" defer></script>
|
||||||
<script src="script.js" defer></script>
|
<script src="script.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 125 KiB |
+10
-639
@@ -128,465 +128,8 @@ function createServerRack() {
|
|||||||
|
|
||||||
const serverType = Math.random();
|
const serverType = Math.random();
|
||||||
|
|
||||||
const isTerminal = i === 0;
|
if (i === 0) {
|
||||||
|
createTerminal(face);
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
} else if (serverType < 0.35) {
|
} else if (serverType < 0.35) {
|
||||||
// Vertical grill + fans
|
// Vertical grill + fans
|
||||||
const grillV = document.createElement('div');
|
const grillV = document.createElement('div');
|
||||||
@@ -724,7 +267,7 @@ function createServerRack() {
|
|||||||
face.appendChild(ledGroup);
|
face.appendChild(ledGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTerminal) {
|
if (i !== 0) {
|
||||||
const serviceLabel = document.createElement('div');
|
const serviceLabel = document.createElement('div');
|
||||||
serviceLabel.className = 'service-label';
|
serviceLabel.className = 'service-label';
|
||||||
serviceLabel.textContent = services[Math.floor(Math.random() * services.length)];
|
serviceLabel.textContent = services[Math.floor(Math.random() * services.length)];
|
||||||
@@ -745,6 +288,7 @@ function createServerRack() {
|
|||||||
|
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
|
|
||||||
|
// Update the parallax scroll position of the rack container
|
||||||
function updateParallax() {
|
function updateParallax() {
|
||||||
const scrolled = window.pageYOffset;
|
const scrolled = window.pageYOffset;
|
||||||
const parallaxSpeed = 0.3;
|
const parallaxSpeed = 0.3;
|
||||||
@@ -763,214 +307,41 @@ function createServerRack() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeTerminal = null;
|
|
||||||
createServerRack();
|
createServerRack();
|
||||||
|
|
||||||
const heroEl = document.getElementById('hero');
|
const heroEl = document.getElementById('hero');
|
||||||
let heroMouseDown = false;
|
let heroMouseDown = false;
|
||||||
|
|
||||||
|
// Track mouse state to prevent state collapse during hero click
|
||||||
heroEl.addEventListener('mousedown', () => { heroMouseDown = true; });
|
heroEl.addEventListener('mousedown', () => { heroMouseDown = true; });
|
||||||
heroEl.addEventListener('mouseup', () => { heroMouseDown = false; });
|
heroEl.addEventListener('mouseup', () => { heroMouseDown = false; });
|
||||||
|
|
||||||
|
// Toggle terminal expansion when clicking the hero section
|
||||||
heroEl.addEventListener('click', (e) => {
|
heroEl.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.btn')) return;
|
if (e.target.closest('.btn')) return;
|
||||||
if (activeTerminal && activeTerminal._mobileInput) {
|
if (activeTerminal && activeTerminal._mobileInput) {
|
||||||
if (activeTerminal.classList.contains('grown')) {
|
if (activeTerminal.classList.contains('grown')) {
|
||||||
activeTerminal.classList.remove('grown');
|
activeTerminal.classList.remove('grown');
|
||||||
|
activeTerminal.classList.remove('opaque');
|
||||||
const face = activeTerminal.closest('.server-face');
|
const face = activeTerminal.closest('.server-face');
|
||||||
const unit = face?.closest('.rack-unit');
|
const unit = face?.closest('.rack-unit');
|
||||||
face?.classList.remove('grown');
|
face?.classList.remove('grown');
|
||||||
unit?.classList.remove('grown');
|
unit?.classList.remove('grown');
|
||||||
heroEl.classList.remove('shifted');
|
heroEl.classList.remove('shifted');
|
||||||
activeTerminal._mobileInput.blur();
|
activeTerminal._mobileInput.blur();
|
||||||
|
document.querySelector('.rack-container')?.classList.remove('opaque');
|
||||||
} else {
|
} else {
|
||||||
activeTerminal._mobileInput.focus();
|
activeTerminal._mobileInput.focus();
|
||||||
activeTerminal.classList.add('grown');
|
activeTerminal.classList.add('grown');
|
||||||
|
activeTerminal.classList.add('opaque');
|
||||||
const face = activeTerminal.closest('.server-face');
|
const face = activeTerminal.closest('.server-face');
|
||||||
const unit = face?.closest('.rack-unit');
|
const unit = face?.closest('.rack-unit');
|
||||||
face?.classList.add('grown');
|
face?.classList.add('grown');
|
||||||
unit?.classList.add('grown');
|
unit?.classList.add('grown');
|
||||||
heroEl.classList.add('shifted');
|
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();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+152
-40
@@ -1,4 +1,6 @@
|
|||||||
*, *::before, *::after {
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -51,11 +53,15 @@ body {
|
|||||||
top: 100px;
|
top: 100px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 680px;
|
width: 680px;
|
||||||
transition: transform 0.1s linear;
|
transition: transform 0.1s linear, opacity 0.5s ease;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rack-container.opaque {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.rack-top-bar {
|
.rack-top-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -201,7 +207,11 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: text;
|
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 {
|
.terminal-display.grown {
|
||||||
@@ -217,13 +227,11 @@ body {
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(0deg,
|
||||||
0deg,
|
|
||||||
transparent,
|
transparent,
|
||||||
transparent 2px,
|
transparent 2px,
|
||||||
rgba(0, 0, 0, 0.15) 2px,
|
rgba(0, 0, 0, 0.15) 2px,
|
||||||
rgba(0, 0, 0, 0.15) 4px
|
rgba(0, 0, 0, 0.15) 4px);
|
||||||
);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -261,9 +269,22 @@ body {
|
|||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vim-block-cursor {
|
||||||
|
background: #22c55e;
|
||||||
|
color: #0a0a0c;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes cursorBlink {
|
@keyframes cursorBlink {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.eth-brand {
|
.eth-brand {
|
||||||
@@ -370,7 +391,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-drive-bay {
|
.server-drive-bay {
|
||||||
@@ -498,8 +519,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from {
|
||||||
to { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fan-slow {
|
.fan-slow {
|
||||||
@@ -564,25 +590,59 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink1 {
|
@keyframes blink1 {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.2; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink2 {
|
@keyframes blink2 {
|
||||||
0%, 100% { opacity: 0.3; }
|
|
||||||
50% { opacity: 1; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink3 {
|
@keyframes blink3 {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
30% { opacity: 0.1; }
|
0%,
|
||||||
60% { opacity: 0.8; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink4 {
|
@keyframes blink4 {
|
||||||
0%, 100% { opacity: 0.5; }
|
|
||||||
25% { opacity: 1; }
|
0%,
|
||||||
75% { opacity: 0.2; }
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rack-mount {
|
.rack-mount {
|
||||||
@@ -641,13 +701,11 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(180deg,
|
||||||
180deg,
|
|
||||||
transparent,
|
transparent,
|
||||||
transparent 20px,
|
transparent 20px,
|
||||||
rgba(0, 0, 0, 0.3) 20px,
|
rgba(0, 0, 0, 0.3) 20px,
|
||||||
rgba(0, 0, 0, 0.3) 21px
|
rgba(0, 0, 0, 0.3) 21px);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rack-label {
|
.rack-label {
|
||||||
@@ -800,8 +858,15 @@ nav .nav-inner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.4; }
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
@@ -1226,12 +1291,30 @@ section {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-keyword { color: #c084fc; }
|
.code-keyword {
|
||||||
.code-function { color: #60a5fa; }
|
color: #c084fc;
|
||||||
.code-string { color: #34d399; }
|
}
|
||||||
.code-comment { color: #52525b; font-style: italic; }
|
|
||||||
.code-variable { color: #f472b6; }
|
.code-function {
|
||||||
.code-operator { color: #fbbf24; }
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-string {
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-comment {
|
||||||
|
color: #52525b;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-variable {
|
||||||
|
color: #f472b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-operator {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
.project-info {
|
.project-info {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
@@ -1556,7 +1639,7 @@ footer p {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 80px;
|
top: 80px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
z-index: 1000;
|
z-index: 10001;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -1575,6 +1658,7 @@ footer p {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
animation: toastIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: toastIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
|
z-index: 10001;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.removing {
|
.toast.removing {
|
||||||
@@ -1617,6 +1701,7 @@ footer p {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100px) scale(0.9);
|
transform: translateX(100px) scale(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0) scale(1);
|
transform: translateX(0) scale(1);
|
||||||
@@ -1628,6 +1713,7 @@ footer p {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0) scale(1);
|
transform: translateX(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(100px) scale(0.9);
|
transform: translateX(100px) scale(0.9);
|
||||||
@@ -1671,11 +1757,37 @@ footer p {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links.active li:nth-child(1) { transition-delay: 0.05s; }
|
.nav-links.active li:nth-child(1) {
|
||||||
.nav-links.active li:nth-child(2) { transition-delay: 0.1s; }
|
transition-delay: 0.05s;
|
||||||
.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(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 {
|
.hamburger {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+1352
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user