# Deployment

## Local Commands

```bash
npm install
npm run dev
npm run typecheck
npm run build
npm run preview
```

## Configuration

Set these at build time:

```bash
PUBLIC_SITE_URL=https://www.bluecapstrategies.com
PUBLIC_CONTACT_FORM_ENDPOINT=https://your-form-endpoint.example
```

`PUBLIC_CONTACT_FORM_ENDPOINT` can point to Formspree, a webhook, ntfy, or a self-hosted form
receiver. Without it, the contact form validates fields and opens an email draft to
`info@bluecapstrategies.com`.

## Container

```bash
docker build -t bluecap-strategies-website:latest .
docker run --rm -p 8080:80 bluecap-strategies-website:latest
```

## k3s (as-built — deployed & serving in-cluster since 2026-07-17)

Live GitOps flow (all working; see `docs/gitops-deployment-strategy.md` + ADR-0001/0007/0008):

```text
local edit -> git push (Gitea main) -> Gitea Actions: build + seo:lint -> push image (internal
  10.66.15.22:3000) -> bump DEPLOY_IMAGE tag + commit back [skip ci] -> ArgoCD reconciles k3s ->
  k3s pulls git.7tl-homelab.com/... (private, ES pull secret, public HTTPS) -> pods serve
```

- ArgoCD is the reconciler (not Flux); the CI runner never gets kubeconfig or cluster access.
- **Not yet public** — the ingress names `bluecapstrategies.com`/`www` but DNS still points at the
  old WordPress site. Going public = the DNS Cutover below, a deliberate step. See the
  `keep-old-site-live` rule: do nothing that affects the old site until cutover.

### Operate

```bash
# Review the running site locally (no cluster/DNS impact):
kubectl -n bluecap-strategies port-forward svc/bluecap-website 18080:80   # then http://localhost:18080
# or purely off-cluster from source:  npm run dev

# Health / status:
kubectl -n argocd get application bcs-website                 # Synced / Healthy
kubectl -n bluecap-strategies get pods,deploy                 # 2/2 Running
kubectl -n bluecap-strategies get externalsecret gitea-registry   # SecretSynced

# Rollback: `git revert` the tag-bump commit and push (ArgoCD reconciles), or use ArgoCD history.
```

### Troubleshoot (things that actually bit us)

- **CI job never runs / "no matching runner"** → runner offline or mis-scoped. Must be a **global**
  (owner_id=0) runner on `gitea_network` with instance URL `http://gitea:3000`; token must be an
  **instance-level** registration token (Site Admin → Actions → Runners).
- **Push: "server gave HTTP response to HTTPS client"** → runner host dockerd missing
  `insecure-registries: ["10.66.15.22:3000"]` (gitea Ansible role handles it).
- **Push: `unauthorized`** → `CI_TOKEN` repo secret missing/insufficient (`write:package` + `write:repository`).
- **Pod `ImagePullBackOff`** → ExternalSecret not synced (check OpenBao `cluster/bluecap-registry`
  fields) or the `read:package` PAT.
- **`git push` rejected (fetch first)** → CI committed a tag bump to `main`; `git pull --rebase` first
  (`git config pull.rebase true`).

Manual `kubectl apply -k k8s/` is for emergency/bootstrap only, not normal deploys.

Ingress/TLS items to confirm before cutover: whether Pangolin/Newt terminates/passes/proxies TLS
to Traefik and preserves `Host` headers; whether cert-manager's Cloudflare DNS-01 token can issue
for `bluecapstrategies.com`; whether the canonical host is apex or `www`.

## DNS Cutover

Before cutover:

- Build and push the final container image.
- Confirm the GitHub Actions workflow publishes the GHCR image and updates the desired image tag.
- Confirm ArgoCD reconciles the `bluecap-strategies` workload from Git.
- Confirm the ingress or edge route answers for `www.bluecapstrategies.com` and `bluecapstrategies.com`.
- Lower DNS TTL on the existing records.
- Point BlueCap DNS to the existing Pangolin/Newt public edge, or to the chosen production edge if
  that architecture changes.
- Configure apex-to-canonical redirect at the selected layer: Cloudflare, Pangolin/Newt, Traefik,
  or nginx.
- Verify `/`, `/services/`, `/about-us/`, `/contact/`, `/sitemap-index.xml`, and `/robots.txt`
  after DNS propagation.
