feat(site): add /apps and /homelab pages with shared nav
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Apps — plate-software.de</title>
|
||||
<meta name="description" content="Die Anwendungen, die aus dem plate-software-Homelab live laufen: CannaManage, InspectFlow und mehr." />
|
||||
<meta name="author" content="Patrick Plate" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta property="og:title" content="Apps — plate-software.de" />
|
||||
<meta property="og:description" content="Die Anwendungen, die aus dem plate-software-Homelab live laufen." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:locale:alternate" content="en_US" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⬡</text></svg>" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="wrap nav">
|
||||
<a class="brand" href="/"><span class="mark">⬡</span> plate-software.de</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/">
|
||||
<span data-lang-de>Start</span>
|
||||
<span data-lang-en>Home</span>
|
||||
</a>
|
||||
<a href="/apps/" class="active">Apps</a>
|
||||
<a href="/homelab/">Homelab</a>
|
||||
</nav>
|
||||
<div class="lang-toggle" role="group" aria-label="Language">
|
||||
<button id="btn-de" class="active" onclick="setLang('de')">DE</button>
|
||||
<button id="btn-en" onclick="setLang('en')">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero" style="padding: 56px 0 24px;">
|
||||
<div class="wrap">
|
||||
<a class="back-link" href="/">
|
||||
<span data-lang-de>← Zurück</span>
|
||||
<span data-lang-en>← Back</span>
|
||||
</a>
|
||||
<h1>
|
||||
<span data-lang-de>Apps aus dem <span class="grad">Homelab</span></span>
|
||||
<span data-lang-en>Apps from the <span class="grad">homelab</span></span>
|
||||
</h1>
|
||||
<p class="lead">
|
||||
<span data-lang-de>Built in the homelab, served from IONOS — same continuous-deploy story as this page itself.</span>
|
||||
<span data-lang-en>Built in the homelab, served from IONOS — same continuous-deploy story as this page itself.</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 24px 0 56px;">
|
||||
<div class="wrap">
|
||||
<div class="grid">
|
||||
<!-- CannaManage -->
|
||||
<div class="card">
|
||||
<div class="icon">🌿</div>
|
||||
<h3>CannaManage <span class="badge live">🟢 Live</span></h3>
|
||||
<div class="sub" data-lang-de>Mitgliederverwaltung für Cannabis-Anbauvereinigungen</div>
|
||||
<div class="sub" data-lang-en>Member management for German Cannabis Cultivation Associations</div>
|
||||
<p data-lang-de>Mitgliederverwaltung für Cannabis-Anbauvereinigungen nach KCanG. Mitglieder, Beitragszahlungen, Abgabemengen-Tracking, gerichtsfeste Audit-Logs.</p>
|
||||
<p data-lang-en>Member management for German Cannabis Cultivation Associations (CSCs) per KCanG. Members, dues, distribution tracking, court-proof audit logs.</p>
|
||||
<a class="link" href="https://cannamanage.plate-software.de" target="_blank" rel="noopener">cannamanage.plate-software.de ↗</a>
|
||||
</div>
|
||||
|
||||
<!-- InspectFlow -->
|
||||
<div class="card">
|
||||
<div class="icon">🛡️</div>
|
||||
<h3>InspectFlow <span class="badge live">🟢 Live</span></h3>
|
||||
<div class="sub" data-lang-de>Maschinen- und Sicherheitsinspektionen</div>
|
||||
<div class="sub" data-lang-en>Machine and safety inspections</div>
|
||||
<p data-lang-de>Maschinen- und Sicherheitsinspektionen für Produktionsbetriebe. Fragebogen-Engine, Tickets, Archiv, Audit-Trail mit Hibernate Envers.</p>
|
||||
<p data-lang-en>Machine and safety inspections for production facilities. Questionnaire engine, tickets, archive, audit trail via Hibernate Envers.</p>
|
||||
<a class="link" href="https://inspectflow.plate-software.de" target="_blank" rel="noopener">inspectflow.plate-software.de ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="wrap foot-row">
|
||||
<div>© <span id="year"></span> Patrick Plate · plate-software.de</div>
|
||||
<div class="foot-links">
|
||||
<a href="/">
|
||||
<span data-lang-de>Start</span>
|
||||
<span data-lang-en>Home</span>
|
||||
</a>
|
||||
<a href="/apps/">Apps</a>
|
||||
<a href="/homelab/">Homelab</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<p class="deploy-badge" data-de="Letzter Deploy: <!--DEPLOY_INFO-->" data-en="Last deploy: <!--DEPLOY_INFO-->"></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function setLang(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
document.getElementById('btn-de').classList.toggle('active', lang === 'de');
|
||||
document.getElementById('btn-en').classList.toggle('active', lang === 'en');
|
||||
document.querySelectorAll('[data-de][data-en]').forEach(function (el) {
|
||||
el.textContent = el.getAttribute(lang === 'en' ? 'data-en' : 'data-de');
|
||||
});
|
||||
try { localStorage.setItem('ps-lang', lang); } catch (e) {}
|
||||
}
|
||||
(function () {
|
||||
var saved;
|
||||
try { saved = localStorage.getItem('ps-lang'); } catch (e) {}
|
||||
var nav = (navigator.language || 'de').slice(0, 2);
|
||||
setLang(saved || (nav === 'en' ? 'en' : 'de'));
|
||||
var y = document.getElementById('year');
|
||||
if (y) y.textContent = new Date().getFullYear();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Homelab — plate-software.de</title>
|
||||
<meta name="description" content="Wie alles auf *.plate-software.de aus einem TrueNAS zu Hause, einem winzigen VPS und einem IONOS-Reverse-Proxy ins Netz kommt." />
|
||||
<meta name="author" content="Patrick Plate" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta property="og:title" content="Homelab — plate-software.de" />
|
||||
<meta property="og:description" content="TrueNAS + frps + IONOS-Reverse-Proxy: wie aus „push to Gitea" in unter einer Minute öffentliches HTTPS wird." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="de_DE" />
|
||||
<meta property="og:locale:alternate" content="en_US" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⬡</text></svg>" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="wrap nav">
|
||||
<a class="brand" href="/"><span class="mark">⬡</span> plate-software.de</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/">
|
||||
<span data-lang-de>Start</span>
|
||||
<span data-lang-en>Home</span>
|
||||
</a>
|
||||
<a href="/apps/">Apps</a>
|
||||
<a href="/homelab/" class="active">Homelab</a>
|
||||
</nav>
|
||||
<div class="lang-toggle" role="group" aria-label="Language">
|
||||
<button id="btn-de" class="active" onclick="setLang('de')">DE</button>
|
||||
<button id="btn-en" onclick="setLang('en')">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero" style="padding: 56px 0 24px;">
|
||||
<div class="wrap">
|
||||
<a class="back-link" href="/">
|
||||
<span data-lang-de>← Zurück</span>
|
||||
<span data-lang-en>← Back</span>
|
||||
</a>
|
||||
<h1>
|
||||
<span data-lang-de>Das <span class="grad">Homelab</span></span>
|
||||
<span data-lang-en>The <span class="grad">Homelab</span></span>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 0 0 56px;">
|
||||
<div class="wrap">
|
||||
<div class="prose">
|
||||
<p data-lang-de>Alles, was unter *.plate-software.de erreichbar ist, läuft auf einem TrueNAS bei mir zu Hause. Öffentlich wird das Ganze über einen winzigen VPS, der nichts anderes tut, als einen frps-Tunnel zu halten — davor steht ein IONOS-Reverse-Proxy, der TLS terminiert. Ein <code>git push</code> nach Gitea, der Runner baut das Docker-Image, und in unter einer Minute steht die neue Version öffentlich unter HTTPS.</p>
|
||||
<p data-lang-en>Everything you can reach under *.plate-software.de runs on a TrueNAS at home. The whole thing is exposed publicly through a tiny VPS whose only job is to hold an frps tunnel — fronted by an IONOS reverse-proxy that terminates TLS. A <code>git push</code> to Gitea, the runner builds the Docker image, and in under a minute the new version is live on public HTTPS.</p>
|
||||
|
||||
<h3 data-lang-de>Der Stack</h3>
|
||||
<h3 data-lang-en>The stack</h3>
|
||||
<p data-lang-de>TrueNAS SCALE hostet Gitea, den act_runner und pro App einen Docker-Stack. Der VPS fährt ausschließlich frps (frp-Server) — kein Anwendungs-Code, keine Datenbank, einfach nur Tunnel-Endpunkt. Eingehender Traffic landet zuerst auf IONOS-Apache-vHosts, die per Reverse-Proxy in den frps-Tunnel zeigen. Let's-Encrypt-Zertifikate kommen via acme.sh.</p>
|
||||
<p data-lang-en>TrueNAS SCALE hosts Gitea, the act_runner, and a per-app Docker stack. The VPS runs only frps (the frp server) — no application code, no database, just a tunnel endpoint. Public traffic enters IONOS Apache vhosts that reverse-proxy into the frps tunnel. Let's Encrypt certificates via acme.sh.</p>
|
||||
|
||||
<h3 data-lang-de>Warum so?</h3>
|
||||
<h3 data-lang-en>Why this setup?</h3>
|
||||
<p data-lang-de>Vor allem die Kosten: rund 10 €/Monat für VPS und Domain — bei einer vergleichbar gemanageten Hosting-Lösung wären es eher 80 €. Dazu volle Kontrolle, kein Vendor-Lock-in und ehrlich gesagt: Infrastruktur zu bauen ist die halbe Miete vom Spaß.</p>
|
||||
<p data-lang-en>Mostly cost: about €10/month for VPS plus domain, versus around €80/month for an equivalent managed hosting setup. Add full control, no vendor lock-in, and frankly — building the infrastructure is half of why this is fun.</p>
|
||||
|
||||
<h3 data-lang-de>Was ich gelernt habe</h3>
|
||||
<h3 data-lang-en>Lessons learned</h3>
|
||||
<p data-lang-de>Zwei systemische Stolpersteine sind hängen geblieben: in einer Proxy-Kette muss <code>auth()</code> von NextAuth verwendet werden, <code>getToken()</code> bricht durch die mehrfachen Hops. Und die DNS-A-Records zeigen auf IONOS, nicht auf den VPS — wer das vergisst, wundert sich lange über kaputtes Routing.</p>
|
||||
<p data-lang-en>Two systemic gotchas have stuck with me: in a proxy chain you need NextAuth's <code>auth()</code>, not <code>getToken()</code> — the latter breaks across the multiple hops. And the DNS A-records point to IONOS, not the VPS — forget that and you'll spend a long time wondering why routing is broken.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="wrap foot-row">
|
||||
<div>© <span id="year"></span> Patrick Plate · plate-software.de</div>
|
||||
<div class="foot-links">
|
||||
<a href="/">
|
||||
<span data-lang-de>Start</span>
|
||||
<span data-lang-en>Home</span>
|
||||
</a>
|
||||
<a href="/apps/">Apps</a>
|
||||
<a href="/homelab/">Homelab</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<p class="deploy-badge" data-de="Letzter Deploy: <!--DEPLOY_INFO-->" data-en="Last deploy: <!--DEPLOY_INFO-->"></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function setLang(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
document.getElementById('btn-de').classList.toggle('active', lang === 'de');
|
||||
document.getElementById('btn-en').classList.toggle('active', lang === 'en');
|
||||
document.querySelectorAll('[data-de][data-en]').forEach(function (el) {
|
||||
el.textContent = el.getAttribute(lang === 'en' ? 'data-en' : 'data-de');
|
||||
});
|
||||
try { localStorage.setItem('ps-lang', lang); } catch (e) {}
|
||||
}
|
||||
(function () {
|
||||
var saved;
|
||||
try { saved = localStorage.getItem('ps-lang'); } catch (e) {}
|
||||
var nav = (navigator.language || 'de').slice(0, 2);
|
||||
setLang(saved || (nav === 'en' ? 'en' : 'de'));
|
||||
var y = document.getElementById('year');
|
||||
if (y) y.textContent = new Date().getFullYear();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+9
-1
@@ -18,7 +18,15 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="wrap nav">
|
||||
<div class="brand"><span class="mark">⬡</span> plate-software.de</div>
|
||||
<a class="brand" href="/"><span class="mark">⬡</span> plate-software.de</a>
|
||||
<nav class="nav-links">
|
||||
<a href="/" class="active">
|
||||
<span data-lang-de>Start</span>
|
||||
<span data-lang-en>Home</span>
|
||||
</a>
|
||||
<a href="/apps/">Apps</a>
|
||||
<a href="/homelab/">Homelab</a>
|
||||
</nav>
|
||||
<div class="lang-toggle" role="group" aria-label="Language">
|
||||
<button id="btn-de" class="active" onclick="setLang('de')">DE</button>
|
||||
<button id="btn-en" onclick="setLang('en')">EN</button>
|
||||
|
||||
Reference in New Issue
Block a user