diff --git a/docs/superpowers/specs/2026-04-25-under-construction-placeholder-design.md b/docs/superpowers/specs/2026-04-25-under-construction-placeholder-design.md new file mode 100644 index 0000000..3b028d0 --- /dev/null +++ b/docs/superpowers/specs/2026-04-25-under-construction-placeholder-design.md @@ -0,0 +1,133 @@ +# Under-construction placeholder — design + +**Date:** 2026-04-25 +**Status:** approved (pending user spec review) + +## 1. Purpose + +A branded "back shortly" page for cameleer.io that the operator can swap into the live origin on demand from Gitea Actions. Used during planned maintenance, incident fallback, or any moment the real site needs to come down without leaving visitors on a broken page. + +## 2. Constraints & non-goals + +- **Same target as the real site.** Deploys to the same Hetzner Webhosting L docroot used by `deploy.yml`. Replaces the live `index.html` and assets at the docroot root. +- **Must work when the main build is broken.** This is the worst-case in which a placeholder is needed. Therefore the placeholder MUST NOT depend on `npm ci`, `astro build`, or any other step that could fail along with the main site's build. +- **Manual trigger only.** Same pattern as `deploy.yml` — `workflow_dispatch` from the Gitea UI. No push/auto-deploy. +- **Cannot race the main deploy.** Both workflows write to the same docroot via `rsync --delete`; concurrent runs would clobber each other. +- **Recovery is the regular deploy.** Triggering `deploy.yml` on any `main` commit restores the site. No bespoke "un-placeholder" workflow. +- **Origin-side headers are not in scope.** Hetzner Webhosting L runs `AllowOverride None`; all response headers are owned by Cloudflare Transform Rules (see `OPERATOR-CHECKLIST.md` §2). The placeholder workflow does NOT need to assert HSTS/CSP/XFO — those headers are origin-agnostic. + +Out of scope: +- Per-environment placeholder variants (staging, etc.). Same target, same content. +- A status page, ETA, or live incident feed. +- Cookie banner, analytics, or any third-party JS. + +## 3. Architecture + +``` +cameleer-website/ +├── placeholder/ +│ ├── index.html # standalone HTML, inlined CSS +│ ├── cameleer-logo.png # copy of public/icons/cameleer-192.png (~36 KB) +│ └── favicon.png # copy of public/icons/cameleer-32.png (~2.4 KB) +├── .gitea/workflows/ +│ ├── ci.yml # unchanged +│ ├── deploy.yml # unchanged +│ └── deploy-placeholder.yml # NEW +└── README.md # NEW section: "Placeholder mode" +``` + +### Why standalone HTML, not Astro + +The placeholder lives outside `src/`, is not picked up by `astro build`, and never enters `dist/`. It is a single self-contained `index.html` with inlined CSS. Two PNG assets ship alongside the HTML in `placeholder/` and are referenced by relative paths (`./cameleer-logo.png`, `./favicon.png`) so the page renders correctly after `rsync --delete` clears the docroot. The repo's full-resolution `public/cameleer-logo.svg` is 1.5 MB (embedded raster data) and is therefore not used here; the 192 px PNG is the right size and weight for a placeholder hero. + +Trade-off accepted: brand tokens (colors, fonts) are hand-mirrored from `tailwind.config.mjs` rather than imported. If those tokens change, the placeholder may visibly drift. Acceptable because (a) the placeholder is rarely shown, (b) the file is short enough to re-sync in two minutes, (c) the alternative — coupling the placeholder to the Astro build — defeats the placeholder's whole reason for existing. + +### Workflow shape + +`deploy-placeholder.yml`: + +- **Trigger:** `workflow_dispatch` only. +- **Concurrency:** `group: deploy-production`, `cancel-in-progress: false` — same group as `deploy.yml`. Gitea will queue, never overlap. +- **Runner:** `ubuntu-latest` (matches `deploy.yml`). +- **Secrets used:** `SFTP_HOST`, `SFTP_USER`, `SFTP_PATH`, `SFTP_KEY`, `SFTP_KNOWN_HOSTS`, `PUBLIC_SALES_EMAIL`. +- **Steps:** + 1. `actions/checkout@v4` + 2. Configure SSH (key + known_hosts; install rsync/openssh if missing) — same logic as `deploy.yml` lines 70–88. + 3. **Inject `PUBLIC_SALES_EMAIL` into the placeholder.** The HTML contains a single literal token `__SALES_EMAIL__` (no hyphens, no other instances anywhere); `sed -i "s|__SALES_EMAIL__|$PUBLIC_SALES_EMAIL|g" placeholder/index.html`. Fail loudly with `: "${PUBLIC_SALES_EMAIL:?...}"` first. Verify replacement by grepping that the token no longer appears. + 4. `rsync -avz --delete --rsync-path=/usr/bin/rsync` over `ssh -p 222` of `placeholder/` → `$SFTP_PATH/` — same flags and SSH options as `deploy.yml` lines 107–109. + 5. **Smoke test:** `curl -s https://www.cameleer.io/ | grep -q 'Routes are remapping'` — placeholder-unique sentinel. Fail the workflow if absent. (Skip the security-headers grep from `deploy.yml`; those headers come from Cloudflare and apply equally to placeholder responses, so they're already covered.) + +### Why `rsync --delete` + +Matches `deploy.yml` behaviour. The docroot reflects exactly what the placeholder ships, with no leftover assets from a previous real-site deploy lingering and being indexed. + +## 4. Placeholder content + +### Markup outline + +Single `` document. Sections, in order: + +1. `
`: + - `