ops: phasefinal.com moved to Cloudflare Pages; pfi-ana-webhost destroyed and removed from inventory, DNS and the FortiGate

This commit is contained in:
vh
2026-10-04 17:28:29 -07:00
parent 55e4d01b49
commit 513f6ef38f
19 changed files with 138 additions and 102 deletions
-1
View File
@@ -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` |
-1
View File
@@ -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}
+1 -1
View File
@@ -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.
@@ -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
+5 -5
View File
@@ -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`.
---
+2 -1
View File
@@ -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`
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
lkraven@10.250.50.52
+1 -1
View File
@@ -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/`
+25 -25
View File
@@ -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
+6 -4
View File
@@ -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
-40
View File
@@ -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
-18
View File
@@ -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; }
}
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0f1318">
<title>Page not found — Phase Final, Inc.</title>
<meta name="description" content="Page not found.">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css?v=2026-09-28">
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<div class="site-header-inner">
<a class="brand" href="/"><img src="/assets/pfi-wordmark-duotone-dark.svg" alt="Phase Final, Inc." width="98" height="28"></a>
<nav class="site-nav" aria-label="Site">
<ul>
<li><a href="/services/software/">Applications</a></li>
<li><a href="/services/systems-engineering/">Systems</a></li>
<li><a href="/services/hosting/">Hosting</a></li>
<li><a href="/services/ai/">AI</a></li>
<li><a href="/engagement/">Engagement</a></li>
<li><a href="/track-record/">Track record</a></li>
<li><a href="/contact/">Contact</a></li>
</ul>
</nav>
</div>
</header>
<header class="page-header">
<div class="hero-inner">
<div class="hero-copy">
<h1 class="lede">Page not found.</h1>
<p class="sublede">There is nothing at this address. <a href="/">Return to the home page</a>.</p>
</div>
</div>
</header>
<footer class="site-footer">
<span>Phase Final, Inc.</span>
<nav aria-label="Footer">
<a href="/services/software/">Applications</a>
<a href="/services/systems-engineering/">Systems</a>
<a href="/services/hosting/">Hosting</a>
<a href="/services/ai/">AI</a>
<a href="/engagement/">Engagement</a>
<a href="/track-record/">Track record</a>
<a href="/contact/">Contact</a>
<a href="/privacy/">Privacy</a>
</nav>
</footer>
</body>
</html>
+18
View File
@@ -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
@@ -46,7 +46,7 @@
<ul class="detail-list">
<li><h3>What this site collects</h3><p>Nothing from your browser. Fonts and images are served from this site itself. No advertising or analytics scripts are loaded, so no cross-site profiling occurs on our side.</p></li>
<li><h3>What email you send us</h3><p>Inquiries sent to inquiry@phasefinal.com are retained as ordinary business email for correspondence and engagement records. We do not add you to marketing lists; there are no marketing lists.</p></li>
<li><h3>Hosting</h3><p>This site is served from infrastructure the firm operates, fronted by Cloudflare for TLS and caching. Server logs exist for abuse control and are not joined to anything else.</p></li>
<li><h3>Hosting</h3><p>This site is static files hosted on Cloudflare Pages, which serves it over TLS. The firm keeps no server logs for it; Cloudflare processes requests under its own privacy policy.</p></li>
<li><h3>Questions</h3><p>Write to <a href="mailto:inquiry@phasefinal.com">inquiry@phasefinal.com</a>.</p></li>
</ul>
<p class="related"><a href="/">Home →</a></p>
+19
View File
@@ -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)"