diff --git a/modules/meos-gateway.nix b/modules/meos-gateway.nix index 524b99d..85e6c06 100644 --- a/modules/meos-gateway.nix +++ b/modules/meos-gateway.nix @@ -8,16 +8,249 @@ let in { options.services.meos-gateway = { enable = mkEnableOption "MeOS Gateway — nginx reverse proxy for os.scoble.me"; + tailscaleIP = mkOption { type = types.str; - description = "n97-1 Tailscale IP for nginx to bind to."; + description = "n97 Tailscale IP for nginx to bind to."; }; }; config = mkIf cfg.enable { - # ── Nginx — SSL-terminating reverse proxy ── + # ── Homepage Dashboard config files ── + # NOTE (scobleclaw#27): widgets.yaml is intentionally empty (no widgets). + # The default "resources" widget hits /api/resources which 404s, producing + # the 3 "API Error" chips at the top of the page. We replace the + # operational surface with client-side per-service health probes injected + # via /var/www/scobleclaw-assets/homepage-fixes.{css,js} (see the + # scobleclaw-assets nginx location below). + environment.etc."homepage/widgets.yaml".text = "[]"; + + environment.etc."homepage/settings.yaml".text = '' + --- + title: os.scoble.me + theme: dark + color: slate + background: + opacity: 50 + showStats: false + favicon: /icon.svg + ''; + + environment.etc."homepage/bookmarks.yaml".text = '' + --- + - Speckl: + - Wiki: + - abbr: WK + href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/ + description: Business plans, whitepaper, strategy, leads + - Landing Page: + - abbr: LP + href: https://speckl.scoble.me/ + description: Software provenance standard site + - Compiler Repo: + - abbr: GH + href: https://os.scoble.me/forgejo/sscoble/speckl + description: Speckl spec and compiler source + - Consulting: + - abbr: CS + href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Consulting + description: $5K pilot offer and engagement model + - Strategy: + - abbr: ST + href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Strategy + description: Phased rollout and exit thesis + - Meetup Kit: + - abbr: MK + href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Meetup-Reference-Card + description: Event cheat sheet and follow-up templates + - Lead Tracker: + - abbr: LT + href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Lead-Tracker + description: Live tracking of prospect conversations + - Whitepaper: + - abbr: WP + href: https://os.scoble.me/forgejo/sscoble/speckl/wiki/Dark-Provenance-Whitepaper + description: Dark Provenance methodology paper + - Social: + - YouTube: + - abbr: YT + href: https://youtube.com/ + - Sites: + - Scott Scoble: + - abbr: SS + href: https://scott.scoble.me/ + description: Speaker website + - Speckl: + - abbr: SP + href: https://speckl.scoble.me/ + description: Software provenance standard + ''; + environment.etc."homepage/services.yaml".text = '' + --- + # Layout restructured in scobleclaw#24 (cycle 128, agent-cos dispatch): + # - "Infrastructure" split into "Operations" (observability) and "Tools" (end-user/operator tools) + # - "Health" renamed "Family" (single tile — Family Health) + # - "Status" merged into "Operations" (Uptime Kuma + Service Health + Grafana + Prometheus) + # - Promoted Prometheus to a tile (k3s Service is healthy 1/1, ClusterIP 10.43.250.126:9090) + # - Dropped Pool Manager tile (k3s pod ImagePullBackOff 5h+; service has no endpoints) + # - Dropped Harbor tile (CrashLoopBackOff across core/db/exporter; half the stack down) + # - Dropped Location tile (gps-share on :3993 returns 404 on every path; keep proxy block as + # debug-only surface — removal requires nixos-rebuild which is currently blocked) + # - "Spatial Office" in the proposed Tools list has no corresponding service/port on this host + # (3987 = forgejo-mcp, 3988 = openbrain-mcp, both internal MCP servers); omitted pending + # a real public URL. + - Development: + - Design System: + href: https://os.scoble.me/system/ + description: Scobleclaw design tokens, primitives, and patterns + icon: mdi-palette-swatch + - Forgejo: + href: https://os.scoble.me/forgejo + description: Self-hosted Git service + icon: mdi-git + - Presentations: + - Podium: + href: https://os.scoble.me/podium/ + description: Presentation manager + icon: mdi-presentation + - Education: + - WGU Speedrun: + href: https://os.scoble.me/wgu/ + description: Course tracker and journal + icon: mdi-run-fast + - Homeschool OS: + href: https://os.scoble.me/homeschool/ + description: Charlotte Mason curriculum management + icon: mdi-school + - Flashcards: + href: https://os.scoble.me/flashcards/ + description: SRS spaced repetition flashcard app + icon: mdi-card-text-outline + - Library: + href: https://os.scoble.me/library/ + description: Scobleclaw family library and catalog + icon: mdi-bookshelf + - Tutor: + href: https://os.scoble.me/tutor/ + description: AI tutor for homeschooling + icon: mdi-robot-happy + - Food: + - Mealie: + href: https://os.scoble.me/mealie/ + description: Recipe management and meal planning + icon: mdi-silverware + - Family: + - Family Health: + href: https://os.scoble.me/health/ + description: Family health tracking dashboard + icon: mdi-heart-pulse + - Operations: + - Uptime Kuma: + href: https://os.scoble.me/kuma/ + description: Real-time uptime and incident monitoring + icon: mdi-pulse + - Service Health: + href: https://os.scoble.me/health/deps/ + description: Per-service health endpoint (probed every 60s) + icon: mdi-heart-pulse + - Grafana: + href: https://os.scoble.me/grafana/ + description: Infrastructure monitoring dashboards + icon: mdi-chart-bar + - Prometheus: + href: https://os.scoble.me/prometheus/ + description: Metrics, alerts, and PromQL (operators) + icon: mdi-chart-line-variant + - Tools: + - Vault Drop: + href: https://os.scoble.me/drop + description: Upload files to vault + icon: mdi-cloud-upload + - Tickets: + href: https://os.scoble.me/tickets/ + description: Ticket status board + icon: mdi-ticket + - Style Prefs: + href: https://os.scoble.me/style-prefs/ + description: Shoppable style preference voting + icon: mdi-tshirt-crew + - Preview: + href: https://os.scoble.me/preview/ + description: Live preview and prototyping + icon: mdi-eye + - Umami: + href: https://os.scoble.me/umami/ + description: Web analytics + icon: mdi-chart-line + - Travel: + - Trips: + href: https://os.scoble.me/trips/ + description: Trip planning and itinerary manager + icon: mdi-airplane + - External Sites: + - Scott Scoble: + href: https://scott.scoble.me/ + description: Speaker & author website + icon: mdi-web + - Speckl: + href: https://speckl.scoble.me/ + description: Software provenance standard + icon: mdi-shield-check + ''; + + # ── Homepage Dashboard systemd service ── + systemd.services.homepage-dashboard = { + description = "Homepage Dashboard — landing page for os.scoble.me"; + after = [ "network.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.homepage-dashboard}/bin/homepage"; + Restart = "on-failure"; + RestartSec = "5"; + DynamicUser = true; + StateDirectory = "homepage"; + WorkingDirectory = "/var/lib/homepage"; + }; + preStart = '' + # Copy config from read-only /etc into writable /var/lib/homepage + cp /etc/homepage/settings.yaml /var/lib/homepage/settings.yaml + cp /etc/homepage/services.yaml /var/lib/homepage/services.yaml + cp /etc/homepage/bookmarks.yaml /var/lib/homepage/bookmarks.yaml + cp /etc/homepage/widgets.yaml /var/lib/homepage/widgets.yaml + # Copy skeleton files from nix store so homepage can initialize + for f in ${pkgs.homepage-dashboard}/share/homepage/src/skeleton/*.yaml; do + base=$(basename "$f") + if [ ! -f "/var/lib/homepage/$base" ]; then + cp "$f" "/var/lib/homepage/$base" + fi + done + mkdir -p /var/lib/homepage/logs + ''; + environment = { + PORT = "8082"; + HOMEPAGE_CONFIG_DIR = "/var/lib/homepage"; + LOG_TARGETS = "stdout"; + HOMEPAGE_ALLOWED_HOSTS = "os.scoble.me"; + }; + }; + + # ── Nginx — SSL-terminating reverse proxy with rate limiting ── services.nginx = { enable = true; + additionalModules = [ pkgs.nginxModules.subsFilter ]; + + # Rate limit zones shared across the virtual host + commonHttpConfig = '' + # Login endpoint: max 5 req/s per IP, burst up to 10, delay excess + limit_req_zone $binary_remote_addr zone=forgejo_login:10m rate=5r/s; + + # General API: max 30 req/s per IP + limit_req_zone $binary_remote_addr zone=forgejo_api:10m rate=30r/s; + + limit_req_status 429; + ''; + virtualHosts."os.scoble.me" = { addSSL = true; sslCertificate = "${certDir}/fullchain.pem"; @@ -26,11 +259,44 @@ in { { addr = cfg.tailscaleIP; port = 443; ssl = true; } ]; + # ── Branded error pages (scobleclaw#36) ── + # Catch framework-leaked 404/502/503/504 and serve Scobleclaw-branded + # recovery pages from /var/www/scobleclaw/errors/. + # Files are static HTML (no upstream), served via an internal location. + # `proxy_intercept_errors on` ensures upstream 4xx/5xx (Next.js, app + # frameworks) are also rewritten — otherwise we'd only catch errors + # nginx generates itself. + extraConfig = '' + proxy_intercept_errors on; + recursive_error_pages on; + + # Brand the status codes that framework apps leak by default + error_page 404 /_scobleclaw/errors/404.html; + error_page 502 503 504 /_scobleclaw/errors/5xx.html; + + # Internal: only nginx can fetch these (clients cannot request them + # directly and we don't want them indexed by search engines). + location ^~ /_scobleclaw/errors/ { + internal; + alias /var/www/scobleclaw/errors/; + # Cache-bust on every request so a content edit shows up immediately, + # but allow downstream gzip. Files are tiny (<8KB). + add_header Cache-Control "public, max-age=300" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + gzip on; + gzip_types text/html; + gzip_min_length 256; + expires 5m; + } + ''; + locations = { - "/forgejo/" = { - proxyPass = "http://k3s-forgejo.kube-system.svc.cluster.local:3000/"; - proxyWebsockets = true; + "/forgejo/user/login" = { + proxyPass = "http://127.0.0.1:3000/user/login"; extraConfig = '' + # Rate limit login attempts aggressively + limit_req zone=forgejo_login burst=10 nodelay; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -38,37 +304,467 @@ in { proxy_set_header X-Forwarded-Prefix /forgejo; ''; }; - "/umami" = { - proxyPass = "http://k3s-umami.kube-system.svc.cluster.local:8069"; + "/forgejo/user/sign_up" = { + proxyPass = "http://127.0.0.1:3000/user/sign_up"; + extraConfig = '' + limit_req zone=forgejo_login burst=5 nodelay; + 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; + proxy_set_header X-Forwarded-Prefix /forgejo; + ''; + }; + "/forgejo/" = { + proxyPass = "http://127.0.0.1:3000/"; + proxyWebsockets = true; + extraConfig = '' + client_max_body_size 50M; + 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; + + proxy_set_header X-Forwarded-Prefix /forgejo; + ''; + }; + "/preview/" = { + proxyPass = "http://127.0.0.1:8085/"; proxyWebsockets = true; extraConfig = '' 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; + + sub_filter_once off; + sub_filter_types text/html; + sub_filter 'href="/' 'href="/preview/'; + sub_filter 'src="/' 'src="/preview/'; + sub_filter 'srcset="/' 'srcset="/preview/'; + sub_filter ', /' ', /preview/'; + sub_filter 'url("/' 'url("/preview/'; + ''; + }; + "/podium/" = { + proxyPass = "http://127.0.0.1:8090"; + proxyWebsockets = true; + extraConfig = '' + 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; + + proxy_set_header X-Forwarded-Prefix /podium; + ''; + }; + "/drop/" = { + proxyPass = "http://127.0.0.1:3002/"; + proxyWebsockets = true; + extraConfig = '' + 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; + + ''; + }; + "/location/" = { + proxyPass = "http://127.0.0.1:3993/"; + extraConfig = '' + 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; + client_max_body_size 1M; + ''; + }; + "/wgu/" = { + proxyPass = "http://127.0.0.1:3005/"; + proxyWebsockets = true; + extraConfig = '' + 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; + + ''; + }; + "/flashcards/" = { + alias = "/var/www/flashcards.scoble.me/"; + index = "index.html"; + tryFiles = "$uri $uri/ /flashcards/index.html"; + extraConfig = '' + add_header Cache-Control "public, max-age=3600"; + add_header X-Frame-Options "SAMEORIGIN" always; + ''; + }; + + "/library/" = { + proxyPass = "http://127.0.0.1:8100/"; + extraConfig = '' + 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; + ''; + }; + + "/trips/" = { + proxyPass = "http://127.0.0.1:8200/"; + extraConfig = '' + 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; + ''; + }; + + + "/tickets/" = { + proxyPass = "http://127.0.0.1:8099/"; + extraConfig = '' + 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; + proxy_set_header X-Forwarded-Prefix /tickets; + proxy_hide_header Upgrade; + proxy_http_version 1.1; + ''; + }; + + "/tickets/wsocket" = { + proxyPass = "http://127.0.0.1:8096/"; + proxyWebsockets = true; + extraConfig = '' + 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; + ''; + }; + + "/kuma/api/" = { + proxyPass = "http://127.0.0.1:3001/api/"; + extraConfig = '' + 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; + ''; + }; + + "/kuma/" = { + proxyPass = "http://127.0.0.1:3001/"; + proxyWebsockets = true; + extraConfig = '' + 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; + proxy_redirect / /kuma/; + proxy_redirect /dashboard /kuma/dashboard; + sub_filter_once off; + sub_filter_types text/html; + sub_filter '' ''; + sub_filter 'href="/' 'href="/kuma/'; + sub_filter 'src="/' 'src="/kuma/'; + ''; + }; + + "/grafana/" = { + # scobleclaw#24 (cycle 128): was routing via traefik-ingress at 10.43.3.233:80 + # which 404s in the new cluster. Direct to scobleclaw-monitoring/grafana Service. + proxyPass = "http://10.43.150.72:3000"; + proxyWebsockets = true; + extraConfig = '' + 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; + ''; + }; + + # scobleclaw#24 (cycle 128): new Prometheus tile. Direct to Service — same rationale + # as Grafana above (avoid the traefik-ingress hop; the Service ClusterIP is reachable + # from the gateway host because flannel.1 is in the 10.42.2.0/24 subnet of this node). + # Subpath-friendly routing: use the same // convention as /kuma/. + "/prometheus/" = { + proxyPass = "http://10.43.250.126:9090/"; + extraConfig = '' + 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; + # Prometheus emits absolute redirects from / to /graph etc. when behind a subpath. + # Proxy_redirect rewrites the Location header back into our /prometheus/ prefix. + proxy_redirect / /prometheus/; + # sub_filter rewrites embedded hrefs in the HTML so links resolve through the gateway. + sub_filter_once off; + sub_filter_types text/html; + sub_filter 'href="/' 'href="/prometheus/'; + sub_filter 'src="/' 'src="/prometheus/'; + ''; + }; + + "/socket.io/" = { + proxyPass = "http://127.0.0.1:3001/socket.io/"; + proxyWebsockets = true; + extraConfig = '' + 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; + ''; + }; + + + "/assets/" = { + proxyPass = "http://127.0.0.1:3001/assets/"; + extraConfig = '' + add_header Cache-Control "public, max-age=31536000, immutable"; + ''; + }; + + "/health/deps/" = { + proxyPass = "http://127.0.0.1:8101"; + extraConfig = '' + 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; + ''; + }; + + "/health/" = { + alias = "/var/www/health.scoble.me/"; + index = "index.html"; + tryFiles = "$uri $uri/ /health/index.html"; + extraConfig = '' + add_header Cache-Control "public, max-age=3600"; + add_header X-Frame-Options "SAMEORIGIN" always; + ''; + }; + + "/homeschool/" = { + proxyPass = "http://127.0.0.1:3007"; + proxyWebsockets = true; + extraConfig = '' + 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; + + ''; + }; + "/umami" = { + proxyPass = "http://127.0.0.1:8069"; + proxyWebsockets = true; + extraConfig = '' + 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; + + # Disable upstream gzip so sub_filter can match uncompressed content + proxy_set_header Accept-Encoding ""; + + # Inject Scobleclaw design system into Umami HTML + sub_filter_once off; + sub_filter_types text/html; + sub_filter '' ''; ''; }; "/umami/" = { - proxyPass = "http://k3s-umami.kube-system.svc.cluster.local:8069"; + proxyPass = "http://127.0.0.1:8069"; proxyWebsockets = true; extraConfig = '' 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; + + # Disable upstream gzip so sub_filter can match uncompressed content + proxy_set_header Accept-Encoding ""; + + # Inject Scobleclaw design system into Umami HTML + sub_filter_once off; + sub_filter_types text/html; + sub_filter '' ''; ''; }; - "/dashboard/" = { - proxyPass = "http://k3s-dashboard.kube-system.svc.cluster.local:3000/"; - proxyWebsockets = true; + "/style-prefs/" = { + proxyPass = "http://10.43.40.183:8199/"; + extraConfig = '' + 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; + proxy_set_header X-Forwarded-Prefix /style-prefs; + ''; }; "/" = { proxyPass = "http://127.0.0.1:8082"; proxyWebsockets = true; + extraConfig = '' + 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; + + # Disable upstream gzip so sub_filter can match uncompressed content + proxy_set_header Accept-Encoding ""; + + # Inject Scobleclaw design system into the homepage HTML, + # plus the per-ticket scobleclaw#27 fix overlay (CSS + JS). + sub_filter_once off; + sub_filter_types text/html; + sub_filter '' ''; + ''; }; + +# Scobleclaw design system explorer (canonical reference + CDN) + # Serves the explorer at /system/ and acts as the CDN for tokens.css + primitives.js + # under /system/assets/. Every scobleclaw app can import from this path. + "/system/" = { + alias = "/var/www/scobleclaw-design/"; + extraConfig = '' + add_header Cache-Control "public, max-age=300"; + add_header X-Frame-Options "SAMEORIGIN" always; + ''; + }; + "/system/assets/" = { + alias = "/var/www/scobleclaw-design/assets/"; + extraConfig = '' + # Short max-age with revalidation. These assets are actively developed + # (tokens.css, primitives.js), so we can't use `immutable` — that would + # pin stale versions in every browser until manual cache clear. + # version.json + ETag/Last-Modified drive revalidation; ?v=X.X.X in + # index.html forces fresh fetches when we need an unblock. + add_header Cache-Control "public, max-age=300, must-revalidate"; + add_header X-Frame-Options "SAMEORIGIN" always; + ''; + }; + + # Static asset mount for the Scobleclaw design system (tokens.css + primitives.js) + "/scobleclaw-assets/" = { + alias = "/var/www/scobleclaw-assets/"; + extraConfig = '' + add_header Cache-Control "public, max-age=3600"; + add_header X-Frame-Options "SAMEORIGIN" always; + ''; + }; + + # Homepage dashboard XHR endpoints — fetched client-side after initial render. + # Must beat the catch-all /api/ below (which routes to Mealie for Nuxt JS). + "/api/services" = { + proxyPass = "http://127.0.0.1:8082/api/services"; + extraConfig = '' + 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; + '';}; + "/api/bookmarks" = { + proxyPass = "http://127.0.0.1:8082/api/bookmarks"; + extraConfig = '' + 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; + '';}; + "/api/widgets" = { + proxyPass = "http://127.0.0.1:8082/api/widgets"; + extraConfig = '' + 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; + '';}; + "/api/hash" = { + proxyPass = "http://127.0.0.1:8082/api/hash"; + extraConfig = '' + 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; + '';}; + "/api/config/custom.css" = { + proxyPass = "http://127.0.0.1:8082/api/config/custom.css"; + extraConfig = '' + add_header Cache-Control "public, max-age=300"; + '';}; + "/api/config/custom.js" = { + proxyPass = "http://127.0.0.1:8082/api/config/custom.js"; + extraConfig = '' + add_header Cache-Control "public, max-age=300"; + '';}; + # Root-level /api/ needed for Nuxt JS XHR calls that don't include /mealie/ prefix + "/api/" = { + proxyPass = "http://10.43.253.234:9000/api/"; + extraConfig = '' + 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; + ''; + }; + + # Root-level /icons/ needed for Nuxt JS icon requests from manifest + "/icons/" = { + proxyPass = "http://10.43.253.234:9000/icons/"; + extraConfig = '' + 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; + ''; + }; + + "/mealie/" = { + proxyPass = "http://10.43.253.234:9000/"; + proxyWebsockets = true; + extraConfig = '' + 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; + proxy_set_header X-Forwarded-Prefix /mealie; + + # Disable upstream gzip so sub_filter can match uncompressed content + proxy_set_header Accept-Encoding ""; + + sub_filter_once off; + sub_filter_types text/html; + sub_filter '' ''; + sub_filter 'href="/' 'href="/mealie/'; + sub_filter 'src="/' 'src="/mealie/'; + sub_filter 'srcset="/' 'srcset="/mealie/'; + sub_filter ', /' ', /mealie/'; + sub_filter 'url("/' 'url("/mealie/'; + sub_filter 'SUB_PATH:""' 'SUB_PATH:"/mealie"'; + sub_filter 'baseURL:"/"' 'baseURL:"/mealie/"'; + ''; + }; + + # /tutor/ - host systemd unit (port 8300) is decommissioned and no + # matching k8s Service exists yet. Until a ClusterIP is provisioned, + # serve a maintenance page so the tile no longer 502s. + # Closes engineering#68 (also addresses scobleclaw#23 partially). + "/tutor/" = { + alias = "/var/www/scobleclaw/maintenance/"; + index = "tutor.html"; + tryFiles = "$uri $uri/ /tutor/tutor.html"; + extraConfig = '' + add_header Cache-Control "no-cache, must-revalidate"; + ''; + }; + }; }; + # HTTP -> HTTPS redirect virtualHosts."os.scoble.me-http" = { listen = [ { addr = cfg.tailscaleIP; port = 80; } ]; @@ -80,7 +776,9 @@ in { # ACME with Cloudflare DNS challenge security.acme = { acceptTerms = true; - defaults.email = "scott@scoble.me"; + defaults = { + email = "scott@scoble.me"; + }; certs."os.scoble.me" = { dnsProvider = "cloudflare"; environmentFile = "/var/lib/acme/cloudflare.env"; @@ -91,4 +789,4 @@ in { # Firewall networking.firewall.allowedTCPPorts = [ 80 443 ]; }; -} +} \ No newline at end of file