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),
})
.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
history: z
.array(

View file

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

View file

@ -18,6 +18,15 @@ scores:
documentation: 95
security: 90
ux: 94
dependencies:
total: 38
outdated: 9
vulnerabilities:
critical: 0
high: 0
moderate: 0
low: 0
lastChecked: '2026-03-24'
lighthouse:
performance: 90
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 */}
{
audit.data.stats && (

View file

@ -172,6 +172,46 @@ const statuses = [...new Set(sortedAudits.map((a) => a.data.status))];
))}
</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 */}
{data.lighthouse && (
<div class="mt-2 flex items-center gap-3">