diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..038975b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/certbot
diff --git a/api/Dockerfile b/api/Dockerfile
new file mode 100644
index 0000000..ecbf2df
--- /dev/null
+++ b/api/Dockerfile
@@ -0,0 +1,10 @@
+FROM python:3.9-slim
+
+WORKDIR /app
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY server.py .
+
+CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/api/requirements.txt b/api/requirements.txt
new file mode 100644
index 0000000..e5ffee2
--- /dev/null
+++ b/api/requirements.txt
@@ -0,0 +1,2 @@
+fastapi>=0.68.0
+uvicorn>=0.15.0
diff --git a/api/server.py b/api/server.py
new file mode 100644
index 0000000..edca3fe
--- /dev/null
+++ b/api/server.py
@@ -0,0 +1,38 @@
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+import socket
+import asyncio
+
+app = FastAPI()
+
+# Configure CORS
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+async def check_port(host: str, port: int, timeout: float = 2.0) -> bool:
+ try:
+ # Create async socket connection
+ reader, writer = await asyncio.wait_for(
+ asyncio.open_connection(host, port),
+ timeout=timeout
+ )
+ writer.close()
+ await writer.wait_closed()
+ return True
+ except (socket.gaierror, ConnectionRefusedError, asyncio.TimeoutError):
+ return False
+ except Exception:
+ return False
+
+@app.get("/check-port")
+async def port_check(host: str, port: int):
+ is_alive = await check_port(host, port)
+ return {"status": "online" if is_alive else "offline"}
+
+@app.get("/health")
+async def health_check():
+ return {"status": "ok"}
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..9cfff8c
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,41 @@
+services:
+ nginx:
+ image: nginx:alpine
+ container_name: nginx
+ restart: unless-stopped
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./nginx/conf.d:/etc/nginx/conf.d
+ - ./html:/usr/share/nginx/html
+ - ./certbot/www:/var/www/certbot
+ - ./certbot/conf:/etc/letsencrypt
+ networks:
+ - web-network
+ depends_on:
+ - certbot
+ - port-checker
+
+ certbot:
+ image: certbot/certbot
+ container_name: certbot
+ volumes:
+ - ./certbot/www:/var/www/certbot
+ - ./certbot/conf:/etc/letsencrypt
+ networks:
+ - web-network
+ command: certonly --webroot --webroot-path=/var/www/certbot --email p.keier@beyerstedt-it.de --agree-tos --no-eff-email --force-renewal -d garde-studios.de -d www.garde-studios.de
+
+ port-checker:
+ container_name: port-checker
+ build: ./api
+ ports:
+ - "8000:8000"
+ networks:
+ - web-network
+ restart: unless-stopped
+
+networks:
+ web-network:
+ driver: bridge
diff --git a/html/css/catppuccin.css b/html/css/catppuccin.css
new file mode 100644
index 0000000..bb68a4c
--- /dev/null
+++ b/html/css/catppuccin.css
@@ -0,0 +1,145 @@
+[data-theme="latte"] {
+ --theme-rosewater: #dc8a78;
+ --theme-flamingo: #dd7878;
+ --theme-pink: #ea76cb;
+ --theme-mauve: #8839ef;
+ --theme-red: #d20f39;
+ --theme-maroon: #e64553;
+ --theme-peach: #fe640b;
+ --theme-yellow: #df8e1d;
+ --theme-green: #40a02b;
+ --theme-teal: #179299;
+ --theme-sky: #04a5e5;
+ --theme-sapphire: #209fb5;
+ --theme-blue: #1e66f5;
+ --theme-lavender: #7287fd;
+ --theme-text: #4c4f69;
+ --theme-subtext1: #5c5f77;
+ --theme-subtext0: #6c6f85;
+ --theme-overlay2: #7c7f93;
+ --theme-overlay1: #8c8fa1;
+ --theme-overlay0: #9ca0b0;
+ --theme-surface2: #acb0be;
+ --theme-surface1: #bcc0cc;
+ --theme-surface0: #ccd0da;
+ --theme-base: #eff1f5;
+ --theme-mantle: #e6e9ef;
+ --theme-crust: #dce0e8;
+}
+
+[data-theme="frappe"] {
+ --theme-rosewater: #f2d5cf;
+ --theme-flamingo: #eebebe;
+ --theme-pink: #f4b8e4;
+ --theme-mauve: #ca9ee6;
+ --theme-red: #e78284;
+ --theme-maroon: #ea999c;
+ --theme-peach: #ef9f76;
+ --theme-yellow: #e5c890;
+ --theme-green: #a6d189;
+ --theme-teal: #81c8be;
+ --theme-sky: #99d1db;
+ --theme-sapphire: #85c1dc;
+ --theme-blue: #8caaee;
+ --theme-lavender: #babbf1;
+ --theme-text: #c6d0f5;
+ --theme-subtext1: #b5bfe2;
+ --theme-subtext0: #a5adce;
+ --theme-overlay2: #949cbb;
+ --theme-overlay1: #838ba7;
+ --theme-overlay0: #737994;
+ --theme-surface2: #626880;
+ --theme-surface1: #51576d;
+ --theme-surface0: #414559;
+ --theme-base: #303446;
+ --theme-mantle: #292c3c;
+ --theme-crust: #232634;
+}
+
+[data-theme="macchiato"] {
+ --theme-rosewater: #f4dbd6;
+ --theme-flamingo: #f0c6c6;
+ --theme-pink: #f5bde6;
+ --theme-mauve: #c6a0f6;
+ --theme-red: #ed8796;
+ --theme-maroon: #ee99a0;
+ --theme-peach: #f5a97f;
+ --theme-yellow: #eed49f;
+ --theme-green: #a6da95;
+ --theme-teal: #8bd5ca;
+ --theme-sky: #91d7e3;
+ --theme-sapphire: #7dc4e4;
+ --theme-blue: #8aadf4;
+ --theme-lavender: #b7bdf8;
+ --theme-text: #cad3f5;
+ --theme-subtext1: #b8c0e0;
+ --theme-subtext0: #a5adcb;
+ --theme-overlay2: #939ab7;
+ --theme-overlay1: #8087a2;
+ --theme-overlay0: #6e738d;
+ --theme-surface2: #5b6078;
+ --theme-surface1: #494d64;
+ --theme-surface0: #363a4f;
+ --theme-base: #24273a;
+ --theme-mantle: #1e2030;
+ --theme-crust: #181926;
+}
+
+[data-theme="mocha"] {
+ --theme-rosewater: #f5e0dc;
+ --theme-flamingo: #f2cdcd;
+ --theme-pink: #f5c2e7;
+ --theme-mauve: #cba6f7;
+ --theme-red: #f38ba8;
+ --theme-maroon: #eba0ac;
+ --theme-peach: #fab387;
+ --theme-yellow: #f9e2af;
+ --theme-green: #a6e3a1;
+ --theme-teal: #94e2d5;
+ --theme-sky: #89dceb;
+ --theme-sapphire: #74c7ec;
+ --theme-blue: #89b4fa;
+ --theme-lavender: #b4befe;
+ --theme-text: #cdd6f4;
+ --theme-subtext1: #bac2de;
+ --theme-subtext0: #a6adc8;
+ --theme-overlay2: #9399b2;
+ --theme-overlay1: #7f849c;
+ --theme-overlay0: #6c7086;
+ --theme-surface2: #585b70;
+ --theme-surface1: #45475a;
+ --theme-surface0: #313244;
+ --theme-base: #1e1e2e;
+ --theme-mantle: #181825;
+ --theme-crust: #11111b;
+}
+
+/* Color indicators for each theme */
+[data-theme-color="latte"] { background: var(--theme-red); }
+[data-theme-color="frappe"] { background: var(--theme-yellow); }
+[data-theme-color="macchiato"] { background: var(--theme-green); }
+[data-theme-color="mocha"] { background: var(--theme-blue); }
+
+/* Theme Switcher Styles */
+.theme-switcher {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+}
+
+.theme-option {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid transparent;
+ transition: transform 0.2s;
+}
+
+.theme-option:hover {
+ transform: scale(1.3);
+}
+
+.theme-option .active {
+ border-color: var(--theme-text);
+}
diff --git a/html/css/style.css b/html/css/style.css
new file mode 100644
index 0000000..d9e65fd
--- /dev/null
+++ b/html/css/style.css
@@ -0,0 +1,224 @@
+@charset "UTF-8";
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Helvetica Neue', Arial, sans-serif;
+ line-height: 1.6;
+ padding-top: 80px; /* Offset for fixed navbar */
+ background-color: var(--theme-base);
+ color: var(--theme-text);
+}
+
+a {
+ color: var(--theme-red);
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--theme-peach);
+}
+
+/* Navbar Styles */
+.navbar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ background-color: var(--theme-base);
+ box-shadow: 0 2px 10px var(--theme-blue);
+ z-index: 1000;
+}
+
+.container {
+ width: 90%;
+ max-width: 1200px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 0;
+}
+
+.logo {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--theme-text);
+ text-decoration: none;
+}
+
+.logo:hover {
+ color: var(--theme-text);
+}
+
+.nav-links ul {
+ display: flex;
+ list-style: none;
+}
+
+.nav-links li {
+ margin-left: 2rem;
+}
+
+.nav-links a {
+ color: var(--theme-sky);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.3s;
+}
+
+.nav-links a:hover {
+ color: var(--theme-lavender);
+}
+
+/* Mobile Menu */
+.hamburger {
+ display: none;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0.5rem;
+}
+
+.hamburger span {
+ display: block;
+ width: 25px;
+ height: 2px;
+ background: var(--theme-blue);
+ margin: 5px 0;
+ transition: all 0.3s ease;
+}
+
+/* Hero Section */
+.hero {
+ height: 80vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ padding: 2rem;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .nav-links {
+ position: fixed;
+ top: 80px;
+ left: -100%;
+ width: 100%;
+ height: calc(100vh - 80px);
+ background-color: var(--theme-base);
+ transition: left 0.3s ease;
+ }
+
+ .nav-links.active {
+ left: 0;
+ }
+
+ .nav-links ul {
+ flex-direction: column;
+ padding: 2rem;
+ }
+
+ .nav-links li {
+ margin: 1rem 0;
+ }
+
+ .hamburger {
+ display: block;
+ }
+
+ .hamburger.active span:nth-child(1) {
+ transform: rotate(45deg) translate(5px, 5px);
+ }
+
+ .hamburger.active span:nth-child(2) {
+ opacity: 0;
+ }
+
+ .hamburger.active span:nth-child(3) {
+ transform: rotate(-45deg) translate(5px, -5px);
+ }
+}
+
+/* Section Styles */
+.section {
+ padding: 5rem 0;
+}
+
+.container {
+ width: 90%;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+h2 {
+ font-size: 2.5rem;
+ margin-bottom: 2rem;
+ text-align: center;
+}
+
+/* Section Styling */
+#games .container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+/* Server Card Styling */
+.server-card {
+ width: 100%;
+ padding: 1.5rem;
+ background: var(--theme-base);
+ border-radius: 8px;
+ box-shadow: 0 2px 10px var(--theme-blue);
+}
+
+.server-card h3 {
+ color: var(--theme-text);
+ margin-bottom: 1rem;
+ font-size: 1.3rem;
+}
+
+/* Server Info Styling (keep your existing styles) */
+.server-info {
+ font-family: 'Courier New', monospace;
+ line-height: 1.8;
+}
+
+.server-info p {
+ margin: 0.2rem 0;
+}
+
+.arrow {
+ margin: 0.3rem;
+}
+
+.highlight {
+ font-weight: bold;
+}
+
+.card-description {
+ margin: 12px 0;
+ color: var(--theme-subtext0);
+ font-size: 0.9rem;
+ line-height: 1.4;
+}
+
+/* Status indicators (keep your existing styles) */
+.status.online {
+ color: var(--theme-green);
+}
+
+.status.offline {
+ color: var(--theme-red);
+}
+
+.status.unknown {
+ color: var(--theme-yellow);
+}
diff --git a/html/index.html b/html/index.html
new file mode 100644
index 0000000..a081efb
--- /dev/null
+++ b/html/index.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+ Garde Studios
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Games @ Garde Studios
+
+
OpenRA - Red Alert
+
+
Die Alliierten vertrauen auf fortschrittliche Technologie und schnelle Aufklärung, während die Sowjets mit roher Gewalt und überwältigenden Zahlen dominieren – in diesem zeitlosen Konflikt entscheiden Taktik und Ressourcenkontrolle über den Sieg.
+
+
+
+
>> Link: openra.net
+
>> Domain/IP: garde-studios.de
+
>> Port: 1234
+
+
>> Password: garde-studios
+
+
+
+
+
+
OpenRA - Command & Conquer: Tiberian Dawn
+
+
Die GDI setzen auf hochwertige Technologie und schwere Panzerung, während die Bruderschaft von Nod mit Guerilla-Taktik und Tiberium-Waffen kämpft – doch beide Fraktionen müssen sich gegen die tödliche Verseuchung des Tiberiums behaupten.
+
+
+
>> Link: openra.net
+
>> Domain/IP: garde-studios.de
+
>> Port: 1235
+
+
>> Password: garde-studios
+
+
+
+
+
+
OpenRA - Dune 2000
+
+
Die Atreides setzen auf Elite-Truppen und Präzision, die Harkonnen überrollen Feinde mit brutaler Feuerkraft, und die Ordos kämpfen mit hinterhältigen Söldnertaktiken – doch alle müssen um das Melange kämpfen, die Quelle der Macht auf Arrakis.
+
+
+
>> Link: openra.net
+
>> Domain/IP: garde-studios.de
+
>> Port: 1236
+
+
>> Password: garde-studios
+
+
+
+
+
+
Veloren
+
+
Die Abenteurer erkunden eine pixelige Fantasy-Welt mit komplexen Kämpfen und tiefen Handwerkssystemen, während dunkele Kreaturen und tödliche Höhlen auf unvorsichtige Helden lauern – doch der größte Feind ist oft die eigene Gier nach legendärer Beute.
+
+
+
+
>> Link: veloren.net
+
>> Domain/IP: garde-studios.de
+
>> Port: 14004
+
+
+
+
+
+
Factorio (No Space Age DLC)
+
+
Die Ingenieure optimieren Fabriken mit Präzision und Logistik, während die Biters mit schierer Zahl und evolutionärer Anpassung drohen – doch das wahre Chaos entsteht, wenn die Produktionsketten ins Stolpern geraten.
+
+
+
>> Link: factorio.com
+
>> Domain/IP: garde-studios.de
+
>> Port: 34197
+
>> Password: garde-studios
+
+
+
+
+
+
Project Zomboid
+
+
Die Überlebenden horten Vorräte und planen Fluchtwege, während die Horden durch Geräusche und Blutgeruch angelockt werden – doch der gefährlichste Feind bleibt die eigene Unvorsichtigkeit.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/html/js/main.js b/html/js/main.js
new file mode 100644
index 0000000..1d9a3a4
--- /dev/null
+++ b/html/js/main.js
@@ -0,0 +1,74 @@
+document.addEventListener('DOMContentLoaded', function() {
+ // Mobile menu toggle
+ const hamburger = document.querySelector('.hamburger');
+ const navLinks = document.querySelector('.nav-links');
+
+ hamburger.addEventListener('click', function() {
+ this.classList.toggle('active');
+ navLinks.classList.toggle('active');
+ });
+
+ // Close menu when clicking a link (mobile)
+ document.querySelectorAll('.nav-links a').forEach(link => {
+ link.addEventListener('click', () => {
+ hamburger.classList.remove('active');
+ navLinks.classList.remove('active');
+ });
+ });
+
+ // Smooth scrolling for anchor links
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function(e) {
+ e.preventDefault();
+
+ const targetId = this.getAttribute('href');
+ if (targetId === '#') return;
+
+ const targetElement = document.querySelector(targetId);
+ if (targetElement) {
+ window.scrollTo({
+ top: targetElement.offsetTop - 80, // Adjusted for fixed header
+ behavior: 'smooth'
+ });
+ }
+ });
+ });
+});
+
+document.addEventListener('DOMContentLoaded', function() {
+ const serverCards = document.querySelectorAll('.server-card');
+ const API_URL = '/api/check-port'; // Change in production
+
+ async function checkServerStatus(card) {
+ const ip = card.dataset.ip;
+ const port = card.dataset.port;
+ const statusElement = card.querySelector('.status');
+
+ if (!ip || !port || !statusElement) return;
+
+ statusElement.textContent = 'Checking...';
+ statusElement.className = 'status unknown';
+
+ try {
+ const response = await fetch(`${API_URL}?host=${encodeURIComponent(ip)}&port=${port}`);
+ const data = await response.json();
+
+ statusElement.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
+ statusElement.className = `status ${data.status}`;
+ } catch (e) {
+ statusElement.textContent = 'Error';
+ statusElement.className = 'status unknown';
+ }
+ }
+
+ // Initial check
+ serverCards.forEach(checkServerStatus);
+
+ // Click handler for manual refresh
+ serverCards.forEach(card => {
+ card.addEventListener('click', () => checkServerStatus(card));
+ });
+
+ // Periodic checks every 5 minutes
+ setInterval(() => serverCards.forEach(checkServerStatus), 5 * 60 * 1000);
+});
diff --git a/html/js/theme-switcher.js b/html/js/theme-switcher.js
new file mode 100644
index 0000000..98b83cc
--- /dev/null
+++ b/html/js/theme-switcher.js
@@ -0,0 +1,34 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const themeOptions = document.querySelectorAll('.theme-option');
+ const html = document.documentElement;
+
+ // Set initial theme from localStorage or default to latte
+ const savedTheme = localStorage.getItem('catppuccin-theme') || 'latte';
+ html.setAttribute('data-theme', savedTheme);
+ updateActiveTheme(savedTheme);
+
+ // Theme switcher functionality
+ themeOptions.forEach(option => {
+ option.addEventListener('click', () => {
+ const theme = option.dataset.theme;
+ html.setAttribute('data-theme', theme);
+ localStorage.setItem('catppuccin-theme', theme);
+ updateActiveTheme(theme);
+ });
+ });
+
+ // Update active theme indicator
+ function updateActiveTheme(theme) {
+ themeOptions.forEach(opt => {
+ const isActive = opt.dataset.theme === theme;
+ opt.classList.toggle('active', isActive);
+
+ // Update border color immediately
+ if (isActive) {
+ opt.style.borderColor = `var(--theme-mantle)`;
+ } else {
+ opt.style.borderColor = 'transparent';
+ }
+ });
+ }
+});
diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf
new file mode 100644
index 0000000..a7c5575
--- /dev/null
+++ b/nginx/conf.d/default.conf
@@ -0,0 +1,58 @@
+server {
+ listen 80;
+ server_name garde-studios.de www.garde-studios.de;
+
+ charset utf-8;
+ charset_types text/html text/css application/javascript text/plain text/xml;
+
+ location /.well-known/acme-challenge/ {
+ root /var/www/certbot;
+ }
+ location / {
+ return 301 https://$host$request_uri;
+ }
+}
+
+server {
+ listen 443 ssl http2;
+ server_name garde-studios.de www.garde-studios.de;
+
+ charset utf-8;
+ charset_types text/html text/css application/javascript text/plain text/xml;
+
+ ssl_certificate /etc/letsencrypt/live/garde-studios.de/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/garde-studios.de/privkey.pem;
+
+ # SSL configuration
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_prefer_server_ciphers on;
+ ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
+ ssl_ecdh_curve secp384r1;
+ ssl_session_timeout 10m;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_tickets off;
+ ssl_stapling on;
+ ssl_stapling_verify on;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # API reverse proxy
+ location /api/ {
+ proxy_pass http://port-checker:8000/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket support (if needed in future)
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Frontend static files
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+}