diff --git a/CLAUDE.md b/CLAUDE.md index 50bbee0..9d56edd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -345,7 +345,6 @@ Observed and standardized across servers: | fv-ml1 | 10.251.50.54 | Fountain Valley (`10.251.0.0/16`) | GPU / AI inference (bare metal, **4× RTX PRO 6000 Blackwell Max-Q, 96 GB each = 391 GB VRAM**) | `servers/fv-ml1/README.md` | | irv-ml1 | 100.64.0.6 (headscale mesh); 10.6.110.50 (Irvine LAN, routed via mesh) | Irvine — via headscale mesh; wg0 lifeline (10.100.79.3) dormant post-cutover | GPU / AI inference (bare metal, RTX 3090 + RTX A6000, native stacks) | `servers/irv-ml1/README.md` | | ana-docker | 10.250.50.70 | Anaheim | General-purpose Docker host (non-GPU VM on pfi-pve) | `servers/ana-docker/README.md` | -| pfi-ana-webhost | 10.250.50.52 | Anaheim | VM on pfi-pve (VMID 110) — web workload | `servers/pfi-ana-webhost/README.md` | | ana-filebot | 10.250.50.53 | Anaheim | LXC on pfi-pve (CT 112) — file-task automation | `servers/ana-filebot/README.md` | | pfi-pteradactyl | 10.250.50.55 | Anaheim | VM on pfi-pve (VMID 107) — Pterodactyl game panel | `servers/pfi-pteradactyl/README.md` | | pfi-tacticalrmm | 10.250.50.57 | Anaheim | VM on pfi-pve (VMID 111) — TacticalRMM | `servers/pfi-tacticalrmm/README.md` | diff --git a/dns/internal.yaml b/dns/internal.yaml index e87ece7..530d3f1 100644 --- a/dns/internal.yaml +++ b/dns/internal.yaml @@ -61,7 +61,6 @@ hosts: - {name: ana-wg, site: ana, v4: 10.250.50.252, note: WireGuard host} - {name: corviduo-dev, site: ana, v4: 10.250.50.152, note: Worldtree-team dev VM (PFI-hosted)} - {name: pbs-ana, site: ana, v4: 10.250.50.90, note: Proxmox Backup Server — fleet primary} - - {name: pfi-ana-webhost, site: ana, v4: 10.250.50.52, note: web workload} - {name: pfi-postgres, site: ana, v4: 10.250.50.80, note: shared Postgres} - {name: pfi-pteradactyl, site: ana, v4: 10.250.50.55, note: game panel} - {name: pfi-tacticalrmm, site: ana, v4: 10.250.50.57, note: TacticalRMM} diff --git a/docs/pfi/proxmox-vms.md b/docs/pfi/proxmox-vms.md index c29659e..b985653 100644 --- a/docs/pfi/proxmox-vms.md +++ b/docs/pfi/proxmox-vms.md @@ -34,7 +34,7 @@ every NIC. Linux = virtio, Windows = e1000. CTs take DHCP (reserved). | 105 | PFI-Postgres | 10.250.50.80 | 16 (4×4) | 8 G | 80G ospool | Debian 11 | yes / order 3, up 60s | shared Postgres (vaultwarden/gitea/paperless); PGDATA local since 2026-04-23. `servers/pfi-postgres/` | | 106 | corviduo-dev | 10.250.50.152 | 8 (2×4, x86-64-v2-AES) | 8 G | 80G ospool | Debian 13 | yes / order 10 | Worldtree-team dev VM, PFI-hosted. `servers/corviduo-dev/` | | 107 | PFI-Pteradactyl | 10.250.50.55 | 8 (2×4, host) | 8 G | 256G ospool | Debian 11 | yes / — | Pterodactyl game panel. `servers/pfi-pteradactyl/` | -| 110 | PFI-ANA-Webhost | 10.250.50.52 | 16 (4×4) | 4 G (balloon 1 G) | 250G ospool | Debian 11 | yes / order 30, up/down 120s | web workload. `servers/pfi-ana-webhost/` | +| ~~110~~ | ~~PFI-ANA-Webhost~~ **destroyed 2026-10-04** (last image `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`) | ~~10.250.50.52~~ | 16 (4×4) | 4 G (balloon 1 G) | 250G ospool | Debian 11 | yes / order 30, up/down 120s | web workload. `servers/pfi-ana-webhost/` | | 111 | pfi-tacticalrmm | 10.250.50.57 | 16 (4×4) | 8 G | 256G ospool | Linux | yes / order 20 | TacticalRMM. `servers/pfi-tacticalrmm/` | All VMs have the QEMU guest agent enabled. diff --git a/servers/pfi-ana-webhost/README.md b/docs/retired/pfi-ana-webhost/README.md similarity index 73% rename from servers/pfi-ana-webhost/README.md rename to docs/retired/pfi-ana-webhost/README.md index 22db8ca..b119bc9 100644 --- a/servers/pfi-ana-webhost/README.md +++ b/docs/retired/pfi-ana-webhost/README.md @@ -1,4 +1,6 @@ -# pfi-ana-webhost — RETIRED 2026-10-04 +# pfi-ana-webhost — DESTROYED 2026-10-04 + +> **Destroyed 2026-10-04 ~1726 (Prime: "retire the webhost — we may want one back in the future, and if so we'll build it fresh").** `qm destroy 110 --purge` on pfi-pve. Last image kept in PBS: `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z` (+ the two before it). Cleaned up with it: FortiGate policy 18, address `ana-webhost-ip`, VIPs `web-to-webhost` + `ssh-to-webhost`, service `ssh-mapped-2223`, MAC object `pfi-ana-webhost`, DHCP reservation (server 5 entry 11); public A `pfi-ana-webhost.phasefinal.com`; internal `pfi-ana-webhost.ana.internal`. **38.120.12.45 is now unmapped.** FortiGate backups on nh3-dev: `/var/tmp/ana-gw-config-20261004T235515Z-pre-webhost-retire.conf`, `/var/tmp/ana-gw-config-20261005T002204Z-pre-webhost-objects-delete.conf`. > **Retired 2026-10-04 (Prime: "retire dragonfirepro").** VM 110 shut down, `onboot 0`, out of the nightly vzdump > (last backup `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`; the VM is kept, not destroyed). FortiGate policy 18 diff --git a/servers/pfi-ana-webhost/system-details.txt b/docs/retired/pfi-ana-webhost/system-details.txt similarity index 100% rename from servers/pfi-ana-webhost/system-details.txt rename to docs/retired/pfi-ana-webhost/system-details.txt diff --git a/docs/runbooks/disaster-recovery.md b/docs/runbooks/disaster-recovery.md index 2eea2c6..1888b05 100644 --- a/docs/runbooks/disaster-recovery.md +++ b/docs/runbooks/disaster-recovery.md @@ -84,7 +84,7 @@ pfi-pve during morning backup window. Backup suspected as trigger ### pfi-pve (Proxmox VE, 10.250.250.31) **Blast radius — everything hosted on it:** -- ana-docker (VM), ana-nas (CT 109), pfi-postgres (VM 105), pfi-ana-webhost, ana-filebot, pfi-pteradactyl, pfi-tacticalrmm, ana-wg, PBS-ANA (VM), + others +- ana-docker (VM), ana-nas (CT 109), pfi-postgres (VM 105), ana-filebot, pfi-pteradactyl, pfi-tacticalrmm, ana-wg, PBS-ANA (VM), + others Essentially **all Anaheim primary services** go offline. Because ana-nas lives here too, Tier-0 cascade applies simultaneously. @@ -197,11 +197,11 @@ Essentially **all Anaheim primary services** go offline. Because ana-nas lives h **Recovery:** `pct start 112`. -### pfi-ana-webhost (VM on pfi-pve) — web workload +### ~~pfi-ana-webhost~~ — destroyed 2026-10-04 -**Blast radius:** hosted website(s) offline. - -**Recovery:** VM restart; web server auto-start. +Retired and destroyed (Prime). Nothing in the fleet depends on it. Its sites live elsewhere: phasefinal.com on Cloudflare +Pages (`stacks/phasefinal-web/`), dragonfireacoustics.com on Cloudflare Pages in the client's account. Last image in PBS: +`pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`. --- diff --git a/persistent-memory.md b/persistent-memory.md index da81903..8c09345 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -119,7 +119,8 @@ _As of 2026-10-04 ~0546 PT. The newest subsection is first; older subsections ca ### 2026-10-03/04: live now (as of 2026-10-04 ~0546 PT) -- **pfi-ana-webhost RETIRED 2026-10-04 1657 (Prime: "retire dragonfirepro")**: VM 110 stopped, `onboot 0`, out of vzdump (backups kept); FortiGate policy 18 (WAN → HTTP/HTTPS/SSH to it) disabled, VIPs left inert; config backup on nh3-dev `/var/tmp/ana-gw-config-20261004T235515Z-pre-webhost-retire.conf`. ⭐ **dragonfireacoustics.com is NOT ours to serve:** Cloudflare Pages in the client's account since 2026-09-24 (source only on nh3-dev `~/development/adhoc/dragonfireacoustics-site`, no git remote; creds `nh3-dev/.config/cloudflare/dragonfireacoustics-com`); domain on Cloudflare Registrar to 2035-10-30, transfer-locked. **Supersedes the two 2026-09-17 dragonfire entries below** (expiry risk resolved; webhost no longer involved). → `servers/pfi-ana-webhost/README.md` +- **pfi-ana-webhost DESTROYED 2026-10-04 ~1726 (Prime: retire, build fresh if ever needed)**: `qm destroy 110 --purge`; last image `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`. All FortiGate objects for it deleted (policy 18, VIPs, address, service, DHCP reservation; **38.120.12.45 now unmapped**), public + internal DNS records removed, inventory moved to `docs/retired/pfi-ana-webhost/`. ⭐ dragonfireacoustics.com = Cloudflare Pages in the CLIENT's account (source only on nh3-dev `~/development/adhoc/dragonfireacoustics-site`, no remote). **Supersedes the two 2026-09-17 dragonfire entries.** +- **phasefinal.com on Cloudflare Pages since 2026-10-04 ~1720** (project `phasefinal`, our account `vh@phasefinal`): www + apex CNAME → `phasefinal.pages.dev`; nginx container on ana-docker retired. Privacy page hosting line rewritten (Prime-approved wording). `_headers` + `404.html` added. ⚠ **Apex no longer 301s to www** (both serve) and the migration used **jackdaw's** Pages token: **Prime to mint `infra-ops-pages-token`** (Account Pages Edit + zone phasefinal.com DNS Edit + Single Redirect Edit), then add the apex→www Redirect Rule. Deploy: `stacks/phasefinal-web/deploy-pages.sh`. - **VM 101 PFI-ANA-DC (pfi-pve) and VM 104 nh3-laser (nh3-pve) RETIRED from running (Prime 2026-10-04 1634: "we aren't running the DC or the remote laser")**: 101 shut down cleanly, both `onboot 0`, both excluded from their nightly vzdump (last backups kept). That also ends the PVE 9 Secure Boot/VirtIO backup warnings. ⚠ If either is ever started again: enroll the Microsoft 2023 certs first (`qm enroll-efi-keys`, BitLocker check). - **infra-ops login on pbs-ana + pbs-nh3** (Prime 2026-10-04): uid 850, fleet key, NOPASSWD + `log_output`, bootstrapped via guest agent; `servers/pbs-*/ssh-target` now infra-ops. - **PBS 3 → 4: DONE 2026-10-04 1144** (Prime via Miranda): pbs-nh3 then pbs-ana, both PBS 4.2.7 / Debian 13.7. VM backups first (pbs-ana had none anywhere), disk snapshots `pre-pbs4`, read-only datastores, detached dist-upgrades. Proven: v3→v4 sync, PVE 9→PBS 4 write (ct/113), v4→v4 sync. ⚠ My first prep killed apt mid-dpkg on both (qemu-guest-agent restarted itself under the in-band apt); repaired in ~5 min, PBS never stopped serving. **Open:** sfsrv-ana (PVE 8) 04:00 run on 10-05 is the one untested pair; drop `pre-pbs4` snapshots after it; vzdumps until ~10-11. → `docs/runbooks/pbs-pbs4-upgrade-plan.md` diff --git a/scripts/bootstrap-infra-ops-fleet.sh b/scripts/bootstrap-infra-ops-fleet.sh index f4bc7ab..3a60981 100755 --- a/scripts/bootstrap-infra-ops-fleet.sh +++ b/scripts/bootstrap-infra-ops-fleet.sh @@ -26,7 +26,7 @@ PLAYBOOK=playbooks/bootstrap-infra-ops-user.yaml # Tier 1 — compute / docker hosts (where the agent recurs) TIER1=(irv-ml1 ana-ml2 ana-docker nh3-docker nh3-dev) # Tier 2 — PFI app VMs / LXCs -TIER2=(pfi-ana-webhost ana-filebot pfi-pteradactyl pfi-tacticalrmm ana-nas) +TIER2=(ana-filebot pfi-pteradactyl pfi-tacticalrmm ana-nas) # Tier 3 — sensitive infra (DB, backup, network, hypervisors): opt-in via PFI_TIER=all TIER3=(pfi-postgres pbs-ana pbs-nh3 ana-wg pfi-pve nh3-pve) diff --git a/scripts/fleet-ownership-audit.sh b/scripts/fleet-ownership-audit.sh index b6a1a9f..b77af2f 100755 --- a/scripts/fleet-ownership-audit.sh +++ b/scripts/fleet-ownership-audit.sh @@ -50,7 +50,7 @@ declare -A TARGET=( [nh3-docker]=10.100.50.40 [nh3-extdev]=10.100.50.42 [nh3-nas]=syncuser@10.100.50.50 [nh3-pve]=10.100.250.60 [pbs-ana]=lkraven@10.250.50.90 [pbs-nh3]=lkraven@10.100.50.90 - [pfi-ana-webhost]=10.250.50.52 [pfi-gx10]=10.100.50.60 + [pfi-gx10]=10.100.50.60 [pfi-postgres]=lkraven@10.250.50.80 [pfi-pteradactyl]=10.250.50.55 [pfi-pve]=10.250.250.31 [pfi-tacticalrmm]=10.250.50.57 [vm-esh-nas]=lkraven@10.0.50.154 diff --git a/servers/pfi-ana-webhost/ssh-target b/servers/pfi-ana-webhost/ssh-target deleted file mode 100644 index f2b5be4..0000000 --- a/servers/pfi-ana-webhost/ssh-target +++ /dev/null @@ -1 +0,0 @@ -lkraven@10.250.50.52 diff --git a/servers/pfi-pve/README.md b/servers/pfi-pve/README.md index 74f9c1a..7b7df85 100644 --- a/servers/pfi-pve/README.md +++ b/servers/pfi-pve/README.md @@ -37,7 +37,7 @@ hypervisor): - **VM 105** — `pfi-postgres` (PFI-Postgres; PGDATA on local VM disk as of 2026-04-23 migration off ana-nas NFS) — `servers/pfi-postgres/` - **VM 106** — `corviduo-dev` (Worldtree-team dev VM, PFI-hosted) — `servers/corviduo-dev/` - **VM 107** — `pfi-pteradactyl` (game panel) — `servers/pfi-pteradactyl/` -- **VM 110** — `pfi-ana-webhost` — **RETIRED 2026-10-04: stopped, `onboot 0`, out of vzdump** — `servers/pfi-ana-webhost/` +- ~~**VM 110** — `pfi-ana-webhost`~~ — **destroyed 2026-10-04** (Prime); last image `pbs-ana:backup/vm/110/2026-10-04T10:28:18Z`; history in `docs/retired/pfi-ana-webhost/` - **VM 111** — `pfi-tacticalrmm` — `servers/pfi-tacticalrmm/` - **CT 109** — `ana-nas` (Debian LXC serving NFS/SMB; bind-mounts from ospool; data-serving SPOF — see `memory/incident_ana_nas_spof.md`) — `servers/ana-nas/` diff --git a/stacks/phasefinal-web/README.md b/stacks/phasefinal-web/README.md index 15cad63..cde145d 100644 --- a/stacks/phasefinal-web/README.md +++ b/stacks/phasefinal-web/README.md @@ -1,7 +1,11 @@ # phasefinal-web -The Phase Final, Inc. corporate site — nine static pages served by nginx on -**ana-docker**, fronted by Traefik at `www.phasefinal.com`. +The Phase Final, Inc. corporate site — nine static pages on **Cloudflare Pages** +(project `phasefinal`, account `vh@phasefinal`, `phasefinal.pages.dev`), at +`www.phasefinal.com` and `phasefinal.com`. Moved off ana-docker 2026-10-04 (Prime); +the nginx container there was retired (`/opt/docker/{compose,conf}/phasefinal-web.retired-20261004`). +`conf/site/` is the deployed tree, including `_headers` (the response headers nginx used +to set) and `404.html` (without it Pages serves the home page for every unknown path). Built from the design brief at `docs/design/` (boothed 2026-08-29); the markup and CSS came from a Claude Design session and are checked in verbatim under @@ -21,35 +25,31 @@ requests rule. Both must be undone on import: Without these the page silently falls back to system fonts and looks wrong rather than broken, which is the failure mode you won't notice. -## Routing — and the healthcheck trap that looks like a routing bug - -Routers come from the compose labels: `phasefinal-web` serves -`www.phasefinal.com`; `phasefinal-apex` 301s `phasefinal.com` to it. - -⚠ **Traefik silently skips containers Docker reports as unhealthy.** There is no -error, no log line, and no router — it looks exactly like a broken provider. The -first deploy of this stack hit that: the healthcheck used -`http://localhost/`, which resolves to `::1` in `nginx:alpine`, and nginx listens -on IPv4 only, so the check got "Connection refused" forever and the container -never left `unhealthy`. **Use `127.0.0.1`, never `localhost`, in a healthcheck -for an IPv4-only listener** — and if a correctly-labelled container never appears -in `/api/http/routers`, check `docker inspect --format '{{.State.Health.Status}}'` -*before* suspecting Traefik. - ## Deploy ```bash -scripts/deploy-stack.sh ana-docker phasefinal-web -ssh infra-ops@10.250.50.70 'cd /opt/docker/compose/phasefinal-web && docker compose up -d' +stacks/phasefinal-web/deploy-pages.sh # conf/site → Pages production (branch main) ``` -## DNS +Needs a vaulted Cloudflare token with Account → Cloudflare Pages: Edit (see the script). +⚠ The 2026-10-04 migration used jackdaw's token as a one-off; a dedicated +`nh3-dev/.config/cloudflare/infra-ops-pages-token` is pending Prime. Check the result on +the per-deploy preview URL wrangler prints before trusting the custom domains. -`www.phasefinal.com` → A `38.120.12.44` (the Anaheim public IP, forwarded to -ana-docker's Traefik). The apex `phasefinal.com` has **no** record — the site is -www-only by operator decision. Cloudflare-proxied is the intended end state, for -edge caching and the automatic email-address obfuscation the contact block -relies on. +## DNS and redirects + +`www.phasefinal.com` and `phasefinal.com` → CNAME `phasefinal.pages.dev`, Cloudflare-proxied +(both are Pages custom domains). Cloudflare-proxied is also what provides the email-address +obfuscation the contact block relies on. ⚠ **The apex no longer 301s to www**: Traefik used +to do that, Pages `_redirects` cannot redirect between hostnames, and no vaulted token may +create a zone Redirect Rule. Both hostnames serve the site until Prime mints a token with +Single Redirect: Edit; then add the rule `phasefinal.com/* → https://www.phasefinal.com/${1}` (301). + +## Lesson from the ana-docker era (2026-08-29) + +Traefik silently skips containers Docker reports as unhealthy: no error, no router. The first +deploy's healthcheck used `http://localhost/`, which is `::1` in `nginx:alpine` while nginx +listened on IPv4 only. Use `127.0.0.1` in healthchecks for IPv4-only listeners. ## Content constraints diff --git a/stacks/phasefinal-web/cloudflare/README.md b/stacks/phasefinal-web/cloudflare/README.md index 64cb8a0..67cde5d 100644 --- a/stacks/phasefinal-web/cloudflare/README.md +++ b/stacks/phasefinal-web/cloudflare/README.md @@ -7,11 +7,13 @@ reviewable and reproducible rather than living only in someone's dashboard. | name | type | value | proxied | |---|---|---|---| -| `www.phasefinal.com` | A | `38.120.12.44` | yes | -| `phasefinal.com` | A | `38.120.12.44` | yes | +| `www.phasefinal.com` | CNAME | `phasefinal.pages.dev` | yes | +| `phasefinal.com` | CNAME | `phasefinal.pages.dev` | yes | -`38.120.12.44` is the Anaheim public IP, forwarded to ana-docker's Traefik. -The apex 301s to `www` via a Traefik middleware, not a Cloudflare rule. +Since 2026-10-04 both are Cloudflare Pages custom domains (project `phasefinal`). They used to be +A `38.120.12.44` → ana-docker's Traefik, which also 301'd the apex to www; that redirect is gone +until a Redirect Rule can be created (see `../README.md`). The cache rule below still applies and +is harmless with Pages as the origin (it respects origin headers). ## Cache ruleset diff --git a/stacks/phasefinal-web/compose.yaml b/stacks/phasefinal-web/compose.yaml deleted file mode 100644 index ad6c6f2..0000000 --- a/stacks/phasefinal-web/compose.yaml +++ /dev/null @@ -1,40 +0,0 @@ -services: - web: - image: nginx:alpine - container_name: phasefinal-web - restart: unless-stopped - volumes: - - /opt/docker/conf/phasefinal-web/site:/usr/share/nginx/html:ro - - /opt/docker/conf/phasefinal-web/nginx.conf:/etc/nginx/conf.d/default.conf:ro - networks: - - tnet - healthcheck: - # 127.0.0.1, NOT localhost: in this image `localhost` resolves to ::1 and - # nginx listens on IPv4 only, so `localhost` gives "Connection refused" - # forever. Traefik SKIPS unhealthy containers, so a wrong healthcheck here - # silently means "no route" rather than "unhealthy service". - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"] - interval: 30s - timeout: 5s - retries: 3 - start_period: 10s - labels: - - traefik.enable=true - - traefik.http.routers.phasefinal-web.rule=Host(`www.phasefinal.com`) - - traefik.http.routers.phasefinal-web.tls=true - - traefik.http.routers.phasefinal-web.tls.certresolver=anaprod - - traefik.http.services.phasefinal-web.loadbalancer.server.port=80 - # apex -> www, 301 - - traefik.http.routers.phasefinal-apex.rule=Host(`phasefinal.com`) - - traefik.http.routers.phasefinal-apex.tls=true - - traefik.http.routers.phasefinal-apex.tls.certresolver=anaprod - - traefik.http.routers.phasefinal-apex.service=phasefinal-web - - traefik.http.routers.phasefinal-apex.middlewares=phasefinal-to-www - - traefik.http.middlewares.phasefinal-to-www.redirectregex.regex=^https?://phasefinal\.com/(.*) - - traefik.http.middlewares.phasefinal-to-www.redirectregex.replacement=https://www.phasefinal.com/$${1} - - traefik.http.middlewares.phasefinal-to-www.redirectregex.permanent=true - -networks: - tnet: - name: traefik-net - external: true diff --git a/stacks/phasefinal-web/conf/nginx.conf b/stacks/phasefinal-web/conf/nginx.conf deleted file mode 100644 index b49bb5e..0000000 --- a/stacks/phasefinal-web/conf/nginx.conf +++ /dev/null @@ -1,18 +0,0 @@ -server { - listen 80; - server_name _; - root /usr/share/nginx/html; - index index.html; - - # Static site; long cache on immutable assets, short on the document so - # a dictated edit goes live on the next deploy without a purge. - location = / { add_header Cache-Control "public, max-age=300"; try_files /index.html =404; } - location ~* \.(woff2|svg|png|ico)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } - location ~* \.css$ { add_header Cache-Control "public, max-age=3600"; } - - add_header X-Content-Type-Options nosniff always; - add_header X-Frame-Options DENY always; - add_header Referrer-Policy no-referrer always; - - location / { try_files $uri $uri/ =404; } -} diff --git a/stacks/phasefinal-web/conf/site/404.html b/stacks/phasefinal-web/conf/site/404.html new file mode 100644 index 0000000..aba912c --- /dev/null +++ b/stacks/phasefinal-web/conf/site/404.html @@ -0,0 +1,55 @@ + + + + + + +Page not found — Phase Final, Inc. + + + + + + + + + + + + + + diff --git a/stacks/phasefinal-web/conf/site/_headers b/stacks/phasefinal-web/conf/site/_headers new file mode 100644 index 0000000..d5747a7 --- /dev/null +++ b/stacks/phasefinal-web/conf/site/_headers @@ -0,0 +1,18 @@ +# Cloudflare Pages response headers (ported from the retired nginx.conf on ana-docker, 2026-10-04). +/* + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + Referrer-Policy: no-referrer + ! Access-Control-Allow-Origin + +/fonts/* + Cache-Control: public, max-age=31536000, immutable + +/assets/* + Cache-Control: public, max-age=31536000, immutable + +/favicon.svg + Cache-Control: public, max-age=31536000, immutable + +/style.css + Cache-Control: public, max-age=3600 diff --git a/stacks/phasefinal-web/conf/site/privacy/index.html b/stacks/phasefinal-web/conf/site/privacy/index.html index 7dd1311..aff16fb 100644 --- a/stacks/phasefinal-web/conf/site/privacy/index.html +++ b/stacks/phasefinal-web/conf/site/privacy/index.html @@ -46,7 +46,7 @@ diff --git a/stacks/phasefinal-web/deploy-pages.sh b/stacks/phasefinal-web/deploy-pages.sh new file mode 100755 index 0000000..f83619b --- /dev/null +++ b/stacks/phasefinal-web/deploy-pages.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Deploy phasefinal.com to Cloudflare Pages (project `phasefinal`, account vh@phasefinal). +# stacks/phasefinal-web/deploy-pages.sh +# The site is the static tree in conf/site/ (with its _headers and 404.html); there is no build step. +# Token: a Cloudflare API token with Account → Cloudflare Pages: Edit, read from the vault. +# PAGES_TOKEN_KEY defaults to the dedicated infra-ops token (to be minted by Prime); +# the 2026-10-04 migration used nh3-dev/development/jackdaw/cloudflare-api-token as a one-off. +set -euo pipefail +here=$(cd "$(dirname "$0")" && pwd) +key=${PAGES_TOKEN_KEY:-nh3-dev/.config/cloudflare/infra-ops-pages-token} +CLOUDFLARE_API_TOKEN=$(secret get "$key") || { echo "no token at vault key $key (set PAGES_TOKEN_KEY)" >&2; exit 2; } +export CLOUDFLARE_API_TOKEN +CLOUDFLARE_ACCOUNT_ID=$(curl -s -m 15 -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" https://api.cloudflare.com/client/v4/accounts \ + | python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]; print(next(a["id"] for a in r if a["name"]=="vh@phasefinal"))') +export CLOUDFLARE_ACCOUNT_ID +# run wrangler from a scratch dir so it never drops .wrangler/ state into the repo +cd "$(mktemp -d)" +npx -y wrangler@4 pages deploy "$here/conf/site" --project-name phasefinal --branch main --commit-dirty=true \ + --commit-message "deploy from eshpfi-management $(git -C "$here" rev-parse --short HEAD)"