feat(infra): add Cloudflare fallback plan + self-hosted landing pages

Two infrastructure improvements for tech independence:

1. Cloudflare Fallback Documentation (docs/CLOUDFLARE_FALLBACK.md):
   - Plan B: WireGuard + Caddy on Hetzner VPS (€3.79/mo)
   - Complete Caddyfile with all 30+ subdomains
   - Step-by-step failover checklist (~15 min to switch)
   - Plan C: Direct IP with ISP

2. Self-Hosted Landing Pages (eliminates Cloudflare Pages dependency):
   - Nginx container (mana-infra-landings) on port 4400
   - Multi-site config: each subdomain → separate dist/ folder
   - Build script: scripts/mac-mini/build-landings.sh
   - Cloudflare Tunnel ingress rules for 10 landing page domains
   - Storage: /Volumes/ManaData/landings/ on external SSD
   - Domains: it, chats, pics, zitares, presis, clocks,
     manadeck, nutriphi, citycorners, docs

Migration path: Build landings locally, set Cloudflare DNS to
tunnel instead of Pages, then decommission CF Pages projects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-24 12:07:40 +01:00
parent 954b204bac
commit e3115b302d
11 changed files with 733 additions and 27 deletions

View file

@ -212,6 +212,22 @@ const manascoreCollection = defineCollection({
seo: z.number().min(0).max(100), seo: z.number().min(0).max(100),
}) })
.optional(), .optional(),
// Dependency health metrics
dependencies: z
.object({
total: z.number(), // Total dependency count
outdated: z.number(), // Packages with available updates
vulnerabilities: z
.object({
critical: z.number().default(0),
high: z.number().default(0),
moderate: z.number().default(0),
low: z.number().default(0),
})
.optional(),
lastChecked: z.string().optional(), // ISO date
})
.optional(),
// Score history for trend visualization // Score history for trend visualization
history: z history: z
.array( .array(

View file

@ -18,6 +18,15 @@ scores:
documentation: 98 documentation: 98
security: 92 security: 92
ux: 95 ux: 95
dependencies:
total: 42
outdated: 9
vulnerabilities:
critical: 0
high: 0
moderate: 0
low: 0
lastChecked: '2026-03-24'
lighthouse: lighthouse:
performance: 92 performance: 92
accessibility: 95 accessibility: 95

View file

@ -18,6 +18,15 @@ scores:
documentation: 95 documentation: 95
security: 90 security: 90
ux: 94 ux: 94
dependencies:
total: 38
outdated: 9
vulnerabilities:
critical: 0
high: 0
moderate: 0
low: 0
lastChecked: '2026-03-24'
lighthouse: lighthouse:
performance: 90 performance: 90
accessibility: 93 accessibility: 93

View file

@ -274,6 +274,113 @@ function getBarColor(score: number): string {
})() })()
} }
{/* Dependency Health */}
{
audit.data.dependencies &&
(() => {
const deps = audit.data.dependencies;
const vulnCount = deps.vulnerabilities
? deps.vulnerabilities.critical +
deps.vulnerabilities.high +
deps.vulnerabilities.moderate +
deps.vulnerabilities.low
: 0;
const healthPct = Math.round(((deps.total - deps.outdated) / deps.total) * 100);
return (
<div class="border-border/50 mb-8 rounded-xl border bg-gradient-to-br from-white/5 to-white/[0.02] p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-foreground text-sm font-semibold">Dependency Health</h2>
<p class="text-muted-foreground text-xs">
Paketstand und Sicherheit
{deps.lastChecked ? ` (geprüft ${deps.lastChecked})` : ''}
</p>
</div>
<span
class={`text-lg font-bold ${healthPct >= 80 ? 'text-emerald-500' : healthPct >= 60 ? 'text-yellow-500' : 'text-red-500'}`}
>
{healthPct}% aktuell
</span>
</div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
<div>
<span class="text-foreground text-2xl font-bold">{deps.total}</span>
<span class="text-muted-foreground block text-xs">Pakete gesamt</span>
</div>
<div>
<span
class={`text-2xl font-bold ${deps.outdated > 10 ? 'text-yellow-500' : 'text-foreground'}`}
>
{deps.outdated}
</span>
<span class="text-muted-foreground block text-xs">Veraltet</span>
</div>
<div>
<span
class={`text-2xl font-bold ${vulnCount > 0 ? 'text-red-500' : 'text-emerald-500'}`}
>
{vulnCount}
</span>
<span class="text-muted-foreground block text-xs">Vulnerabilities</span>
</div>
<div>
{deps.vulnerabilities ? (
<div class="flex gap-1.5 mt-1">
{deps.vulnerabilities.critical > 0 && (
<span class="bg-red-500/10 text-red-500 rounded px-1.5 py-0.5 text-[10px] font-medium">
{deps.vulnerabilities.critical} Critical
</span>
)}
{deps.vulnerabilities.high > 0 && (
<span class="bg-orange-500/10 text-orange-500 rounded px-1.5 py-0.5 text-[10px] font-medium">
{deps.vulnerabilities.high} High
</span>
)}
{deps.vulnerabilities.moderate > 0 && (
<span class="bg-yellow-500/10 text-yellow-500 rounded px-1.5 py-0.5 text-[10px] font-medium">
{deps.vulnerabilities.moderate} Mod
</span>
)}
{deps.vulnerabilities.low > 0 && (
<span class="bg-blue-500/10 text-blue-500 rounded px-1.5 py-0.5 text-[10px] font-medium">
{deps.vulnerabilities.low} Low
</span>
)}
{vulnCount === 0 && (
<span class="bg-emerald-500/10 text-emerald-500 rounded px-1.5 py-0.5 text-[10px] font-medium">
✓ Sicher
</span>
)}
</div>
) : (
<span class="text-muted-foreground text-xs">Keine Daten</span>
)}
<span class="text-muted-foreground block text-xs mt-1">Schweregrad</span>
</div>
</div>
{/* Health bar */}
<div class="mt-4">
<div class="bg-muted h-2 overflow-hidden rounded-full">
<div
class={`h-full rounded-full transition-all ${healthPct >= 80 ? 'bg-emerald-500' : healthPct >= 60 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={`width: ${healthPct}%`}
/>
</div>
<div class="flex justify-between mt-1">
<span class="text-muted-foreground/50 text-[10px]">
{deps.outdated} veraltet
</span>
<span class="text-muted-foreground/50 text-[10px]">
{deps.total - deps.outdated} aktuell
</span>
</div>
</div>
</div>
);
})()
}
{/* Stats */} {/* Stats */}
{ {
audit.data.stats && ( audit.data.stats && (

View file

@ -172,6 +172,46 @@ const statuses = [...new Set(sortedAudits.map((a) => a.data.status))];
))} ))}
</div> </div>
{/* Dependency health */}
{data.dependencies &&
(() => {
const deps = data.dependencies;
const vulnCount = deps.vulnerabilities
? deps.vulnerabilities.critical +
deps.vulnerabilities.high +
deps.vulnerabilities.moderate +
deps.vulnerabilities.low
: 0;
const healthPct = Math.round(
((deps.total - deps.outdated) / deps.total) * 100
);
const healthColor =
vulnCount > 0
? deps.vulnerabilities?.critical || deps.vulnerabilities?.high
? 'text-red-500'
: 'text-yellow-500'
: healthPct >= 80
? 'text-emerald-500'
: 'text-yellow-500';
return (
<div class="mt-2 flex items-center gap-3">
<span class="text-muted-foreground/50 text-[9px]">Deps:</span>
<span class={`text-[10px] font-medium ${healthColor}`}>
{deps.total} total, {deps.outdated} outdated
</span>
{vulnCount > 0 ? (
<span class="text-[10px] font-medium text-red-500">
⚠ {vulnCount} vulnerabilities
</span>
) : (
<span class="text-[10px] font-medium text-emerald-500">
✓ no vulnerabilities
</span>
)}
</div>
);
})()}
{/* Lighthouse scores */} {/* Lighthouse scores */}
{data.lighthouse && ( {data.lighthouse && (
<div class="mt-2 flex items-center gap-3"> <div class="mt-2 flex items-center gap-3">

View file

@ -114,5 +114,27 @@ ingress:
- hostname: glitchtip.mana.how - hostname: glitchtip.mana.how
service: http://localhost:8020 service: http://localhost:8020
# Self-Hosted Landing Pages (via Nginx on port 4400)
- hostname: it.mana.how
service: http://localhost:4400
- hostname: chats.mana.how
service: http://localhost:4400
- hostname: pics.mana.how
service: http://localhost:4400
- hostname: zitares.mana.how
service: http://localhost:4400
- hostname: presis.mana.how
service: http://localhost:4400
- hostname: clocks.mana.how
service: http://localhost:4400
- hostname: manadeck.mana.how
service: http://localhost:4400
- hostname: nutriphi.mana.how
service: http://localhost:4400
- hostname: citycorners.mana.how
service: http://localhost:4400
- hostname: docs.mana.how
service: http://localhost:4400
# Catch-all # Catch-all
- service: http_status:404 - service: http_status:404

View file

@ -87,6 +87,25 @@ services:
done done
" "
# Self-hosted Landing Pages (replaces Cloudflare Pages)
# Serves all Astro landing page dist/ folders via Nginx
# Build with: ./scripts/mac-mini/build-landings.sh
landings:
image: nginx:alpine
container_name: mana-infra-landings
restart: always
volumes:
- ./docker/nginx/landings.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/nginx/snippets:/etc/nginx/snippets:ro
- /Volumes/ManaData/landings:/srv/landings:ro
ports:
- "4400:80"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: mana-infra-redis container_name: mana-infra-redis
@ -871,9 +890,10 @@ services:
- "4080:80" - "4080:80"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
interval: 120s interval: 180s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s
matrix-web: matrix-web:
build: build:
@ -893,9 +913,10 @@ services:
- "4090:5180" - "4090:5180"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5180/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5180/health"]
interval: 120s interval: 180s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s
# Matrix Bots (Ports 4010-4029) # Matrix Bots (Ports 4010-4029)
matrix-mana-bot: matrix-mana-bot:
@ -932,7 +953,7 @@ services:
- "4010:4010" - "4010:4010"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4010/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4010/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -961,7 +982,7 @@ services:
- "4011:4011" - "4011:4011"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4011/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4011/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1002,7 +1023,7 @@ services:
- "4012:4012" - "4012:4012"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4012/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4012/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1035,7 +1056,7 @@ services:
- "4013:4013" - "4013:4013"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4013/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4013/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1071,7 +1092,7 @@ services:
- "4014:4014" - "4014:4014"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4014/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4014/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1105,7 +1126,7 @@ services:
- "4015:4015" - "4015:4015"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4015/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4015/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1139,7 +1160,7 @@ services:
- "4016:4016" - "4016:4016"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4016/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4016/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1169,7 +1190,7 @@ services:
- "4017:4017" - "4017:4017"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4017/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4017/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1210,7 +1231,7 @@ services:
- "4018:4018" - "4018:4018"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4018/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4018/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1244,7 +1265,7 @@ services:
- "4019:4019" - "4019:4019"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4019/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4019/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1277,7 +1298,7 @@ services:
- "4021:4021" - "4021:4021"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4021/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4021/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1314,7 +1335,7 @@ services:
- "4020:4020" - "4020:4020"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4020/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4020/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1354,7 +1375,7 @@ services:
- "4022:4022" - "4022:4022"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4022/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4022/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1907,9 +1928,10 @@ services:
- "8000:8000" - "8000:8000"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8000/api/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8000/api/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 30s
umami: umami:
image: ghcr.io/umami-software/umami:postgresql-latest image: ghcr.io/umami-software/umami:postgresql-latest
@ -1927,7 +1949,7 @@ services:
- "8010:3000" - "8010:3000"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/heartbeat"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/heartbeat"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
@ -1956,9 +1978,10 @@ services:
- "9090:9090" - "9090:9090"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9090/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9090/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s
pushgateway: pushgateway:
image: prom/pushgateway:v1.7.0 image: prom/pushgateway:v1.7.0
@ -1968,9 +1991,10 @@ services:
- "9091:9091" - "9091:9091"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9091/-/healthy"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9091/-/healthy"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 10s
cadvisor: cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1 image: gcr.io/cadvisor/cadvisor:v0.49.1
@ -1987,9 +2011,10 @@ services:
- "9110:8080" - "9110:8080"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/healthz"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/healthz"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s
postgres-exporter: postgres-exporter:
image: prometheuscommunity/postgres-exporter:v0.15.0 image: prometheuscommunity/postgres-exporter:v0.15.0
@ -2036,9 +2061,10 @@ services:
- "9100:9100" - "9100:9100"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9100/metrics"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9100/metrics"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 10s
# ============================================ # ============================================
# Alerting Stack (Ports 9093-9095) # Alerting Stack (Ports 9093-9095)
@ -2067,9 +2093,10 @@ services:
- "8880:8880" - "8880:8880"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8880/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8880/health"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s
alertmanager: alertmanager:
image: prom/alertmanager:v0.27.0 image: prom/alertmanager:v0.27.0
@ -2089,9 +2116,10 @@ services:
- "9093:9093" - "9093:9093"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9093/-/healthy"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:9093/-/healthy"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 10s
alert-notifier: alert-notifier:
build: build:
@ -2109,10 +2137,10 @@ services:
- "9095:8080" - "9095:8080"
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8080/health"]
interval: 120s interval: 300s
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 5s start_period: 10s
# ============================================ # ============================================
# Auto-Update (Watchtower) # Auto-Update (Watchtower)
@ -2162,9 +2190,10 @@ services:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8020/_health/')"] test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8020/_health/')"]
interval: 120s interval: 300s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s
glitchtip-worker: glitchtip-worker:
image: glitchtip/glitchtip:latest image: glitchtip/glitchtip:latest
@ -2203,9 +2232,10 @@ services:
- "5100:5100" - "5100:5100"
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5100/"] test: ["CMD", "wget", "-q", "--spider", "http://localhost:5100/"]
interval: 120s interval: 180s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 20s
volumes: volumes:
redis_data: redis_data:

108
docker/nginx/landings.conf Normal file
View file

@ -0,0 +1,108 @@
# Nginx Configuration for Self-Hosted Landing Pages
# Each server block serves a different landing page from its dist/ directory
# All traffic comes through Cloudflare Tunnel → localhost:4400
# Shared settings
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json image/svg+xml;
# Default server (catch-all → it.mana.how as homepage)
server {
listen 80 default_server;
server_name _;
root /srv/landings/it;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# it.mana.how — European Tech Independence
server {
listen 80;
server_name it.mana.how;
root /srv/landings/it;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# chats.mana.how — Chat Landing
server {
listen 80;
server_name chats.mana.how;
root /srv/landings/chat;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# pics.mana.how — Picture Landing
server {
listen 80;
server_name pics.mana.how;
root /srv/landings/picture;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# zitares.mana.how — Zitare Landing
server {
listen 80;
server_name zitares.mana.how;
root /srv/landings/zitare;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# presis.mana.how — Presi Landing
server {
listen 80;
server_name presis.mana.how;
root /srv/landings/presi;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# clocks.mana.how — Clock Landing
server {
listen 80;
server_name clocks.mana.how;
root /srv/landings/clock;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# manadeck.mana.how — ManaDeck Landing
server {
listen 80;
server_name manadeck.mana.how;
root /srv/landings/manadeck;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# nutriphi.mana.how — NutriPhi Landing
server {
listen 80;
server_name nutriphi.mana.how;
root /srv/landings/nutriphi;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# citycorners.mana.how — CityCorners Landing
server {
listen 80;
server_name citycorners.mana.how;
root /srv/landings/citycorners;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}
# docs.mana.how — Documentation
server {
listen 80;
server_name docs.mana.how;
root /srv/landings/docs;
index index.html;
include /etc/nginx/snippets/landing-common.conf;
}

View file

@ -0,0 +1,24 @@
# Common settings for all landing page server blocks
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cache static assets aggressively (Astro hashes filenames)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback: try file, then directory, then index.html
location / {
try_files $uri $uri/ $uri/index.html /index.html;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}

285
docs/CLOUDFLARE_FALLBACK.md Normal file
View file

@ -0,0 +1,285 @@
# Cloudflare Tunnel Fallback-Plan
> Was tun wenn Cloudflare ausfällt oder den Account sperrt?
## Risiko
Cloudflare Tunnel ist der **einzige** Weg vom Internet zum Mac Mini. Wenn Cloudflare nicht erreichbar ist:
- Alle *.mana.how Subdomains sind offline
- SSH nur im lokalen Netzwerk möglich
- Kein Deployment, kein Monitoring
**Wahrscheinlichkeit:** Gering (Cloudflare hat >99.99% Uptime), aber Accountsperren oder Policy-Änderungen sind ein Risiko.
## Plan B: WireGuard + Caddy auf Hetzner VPS
### Architektur
```
Internet
Hetzner VPS (€3.79/Monat, CX22)
├── Caddy (Reverse Proxy + Auto-TLS)
├── WireGuard Server
└── DNS: *.mana.how → VPS IP
│ WireGuard Tunnel (verschlüsselt)
Mac Mini (WireGuard Client)
├── Alle Services auf localhost
└── Erreichbar über WireGuard-IP (z.B. 10.0.0.2)
```
### Vorteile
- **Kein Vendor Lock-in:** Hetzner ist deutscher Anbieter
- **Eigene IP:** Keine Abhängigkeit von Cloudflare Proxy
- **WireGuard:** Schneller als Cloudflare Tunnel (~10% weniger Latenz)
- **Let's Encrypt:** Caddy macht TLS automatisch
- **Kosten:** €3.79/Monat (CX22: 2 vCPU, 4 GB RAM, 40 GB SSD)
### Einrichtung VPS (einmalig, ~1 Stunde)
#### 1. Hetzner VPS erstellen
```bash
# CX22 (kleinster mit genug RAM für Caddy + WireGuard)
# Standort: Falkenstein (DE) oder Nürnberg (DE)
# OS: Ubuntu 24.04
# SSH Key: Mac Mini public key
```
#### 2. WireGuard installieren
**Auf dem VPS:**
```bash
apt update && apt install -y wireguard
# Keys generieren
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key
# Config erstellen
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = 10.0.0.1/24
PrivateKey = $(cat /etc/wireguard/server_private.key)
ListenPort = 51820
[Peer]
# Mac Mini
PublicKey = <MAC_MINI_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32
EOF
systemctl enable --now wg-quick@wg0
```
**Auf dem Mac Mini:**
```bash
brew install wireguard-tools
# Keys generieren
wg genkey | tee /etc/wireguard/client_private.key | wg pubkey > /etc/wireguard/client_public.key
# Config
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = 10.0.0.2/24
PrivateKey = $(cat /etc/wireguard/client_private.key)
[Peer]
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = <VPS_IP>:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
EOF
wg-quick up wg0
```
#### 3. Caddy installieren (VPS)
```bash
apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy
```
#### 4. Caddyfile erstellen (VPS)
```Caddyfile
# /etc/caddy/Caddyfile
# Alle Domains → Mac Mini via WireGuard
mana.how {
reverse_proxy 10.0.0.2:5000
}
auth.mana.how {
reverse_proxy 10.0.0.2:3001
}
chat.mana.how {
reverse_proxy 10.0.0.2:5010
}
chat-api.mana.how {
reverse_proxy 10.0.0.2:3030
}
todo.mana.how {
reverse_proxy 10.0.0.2:5011
}
todo-api.mana.how {
reverse_proxy 10.0.0.2:3031
}
calendar.mana.how {
reverse_proxy 10.0.0.2:5012
}
calendar-api.mana.how {
reverse_proxy 10.0.0.2:3032
}
clock.mana.how {
reverse_proxy 10.0.0.2:5013
}
clock-api.mana.how {
reverse_proxy 10.0.0.2:3033
}
contacts.mana.how {
reverse_proxy 10.0.0.2:5014
}
contacts-api.mana.how {
reverse_proxy 10.0.0.2:3034
}
storage.mana.how {
reverse_proxy 10.0.0.2:5015
}
storage-api.mana.how {
reverse_proxy 10.0.0.2:3035
}
presi.mana.how {
reverse_proxy 10.0.0.2:5016
}
presi-api.mana.how {
reverse_proxy 10.0.0.2:3036
}
nutriphi.mana.how {
reverse_proxy 10.0.0.2:5017
}
nutriphi-api.mana.how {
reverse_proxy 10.0.0.2:3037
}
photos.mana.how {
reverse_proxy 10.0.0.2:5019
}
photos-api.mana.how {
reverse_proxy 10.0.0.2:3039
}
mukke.mana.how {
reverse_proxy 10.0.0.2:5180
}
picture.mana.how {
reverse_proxy 10.0.0.2:5021
}
picture-api.mana.how {
reverse_proxy 10.0.0.2:3040
}
playground.mana.how {
reverse_proxy 10.0.0.2:5090
}
matrix.mana.how {
reverse_proxy 10.0.0.2:4000
}
element.mana.how {
reverse_proxy 10.0.0.2:4080
}
grafana.mana.how {
reverse_proxy 10.0.0.2:8000
}
stats.mana.how {
reverse_proxy 10.0.0.2:8010
}
glitchtip.mana.how {
reverse_proxy 10.0.0.2:8020
}
```
#### 5. DNS umstellen (Failover-Schritt)
Beim Ausfall von Cloudflare Tunnel:
```bash
# 1. WireGuard-Verbindung prüfen
ssh mana-server "ping -c1 10.0.0.1" # Ping VPS via WireGuard
# 2. DNS bei Cloudflare umstellen (alle *.mana.how → VPS IP)
# Cloudflare Dashboard → DNS → *.mana.how → A Record → <VPS_IP>
# ODER falls Cloudflare komplett down:
# Domain zu anderem DNS-Provider transferieren (vorher vorbereiten!)
# 3. Caddy starten
ssh vps "systemctl start caddy"
# 4. Prüfen
curl https://mana.how # Sollte über VPS → WireGuard → Mac Mini routen
```
## Failover-Checkliste
| # | Schritt | Zeit | Verantwortlich |
|---|---------|------|----------------|
| 1 | Feststellen: Cloudflare Tunnel ist down | Auto (Health Check Alert) | Automatisch |
| 2 | VPS WireGuard-Verbindung prüfen | 1 Min | Admin |
| 3 | DNS auf VPS-IP umstellen | 5 Min | Admin (Cloudflare Dashboard) |
| 4 | Caddy aktivieren | 1 Min | Admin (SSH zu VPS) |
| 5 | TLS-Zertifikate generieren lassen | 2-5 Min | Automatisch (Caddy + Let's Encrypt) |
| 6 | Alle Services testen | 5 Min | Admin |
| **Gesamt** | | **~15 Min** | |
## Vorbereitung (jetzt erledigen)
- [ ] Hetzner Account erstellen
- [ ] VPS bestellen (CX22, €3.79/Monat)
- [ ] WireGuard einrichten (VPS + Mac Mini)
- [ ] WireGuard-Verbindung testen
- [ ] Caddyfile erstellen (alle Domains)
- [ ] DNS-Failover-Prozedur testen (mit Test-Subdomain)
- [ ] Failover-Checkliste ausdrucken / im Wiki speichern
## Plan C: Direkte IP
Falls auch Hetzner nicht verfügbar:
1. ISP kontaktieren für feste IP-Adresse
2. Port-Forwarding auf Router einrichten (80, 443)
3. Let's Encrypt Zertifikat via DNS-Challenge (kein HTTP nötig)
4. DNS bei einem dritten Provider (z.B. Hetzner DNS, Gandi)
**Nachteil:** Consumer-ISPs blockieren oft Port 25 (E-Mail) und Port 80/443 ist nicht garantiert.

View file

@ -0,0 +1,56 @@
#!/bin/bash
# Build all landing pages and copy dist/ to the shared nginx volume
# Run on the Mac Mini after git pull
#
# Usage: ./scripts/mac-mini/build-landings.sh
set -e
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
OUTPUT_DIR="/Volumes/ManaData/landings"
echo "=== Building Landing Pages ==="
echo "Output: $OUTPUT_DIR"
echo ""
mkdir -p "$OUTPUT_DIR"
# Landing pages to build (filter name → dist path → output name)
declare -A LANDINGS=(
["it"]="services/it-landing"
["chat"]="apps/chat/apps/landing"
["picture"]="apps/picture/apps/landing"
["zitare"]="apps/zitare/apps/landing"
["presi"]="apps/presi/apps/landing"
["clock"]="apps/clock/apps/landing"
["manadeck"]="apps/manadeck/apps/landing"
["nutriphi"]="apps/nutriphi/apps/landing"
["citycorners"]="apps/citycorners/apps/landing"
)
cd "$PROJECT_ROOT"
for name in "${!LANDINGS[@]}"; do
dir="${LANDINGS[$name]}"
if [ -d "$dir" ]; then
echo "Building $name ($dir)..."
pnpm --filter "./$dir" build 2>&1 | tail -3
# Copy dist to output
if [ -d "$dir/dist" ]; then
rm -rf "$OUTPUT_DIR/$name"
cp -r "$dir/dist" "$OUTPUT_DIR/$name"
echo "$OUTPUT_DIR/$name ($(du -sh "$OUTPUT_DIR/$name" | cut -f1))"
else
echo " ⚠ No dist/ found for $name"
fi
else
echo " ⚠ Directory not found: $dir"
fi
echo ""
done
echo "=== Done ==="
echo ""
echo "Restart nginx to pick up changes:"
echo " docker restart mana-infra-landings"