Commit Graph
45 Commits
Author SHA1 Message Date
bmr_bluecap be55c4a27e favicon: restore the original site icon (water drop in circle)
CI / ci (push) Successful in 3m15s
Recovered from the Wayback Machine capture of the WordPress site; adds /favicon.ico,
32/192 PNG icons, apple-touch-icon and the MS tile image.
2026-10-23 16:58:13 -04:00
bmr_bluecap 59caa8e838 perf: responsive AVIF/WebP images; a11y contrast and link-text fixes
- scripts/optimize-images.mjs generates hashed AVIF/WebP variants at build time (sharp)
- Picture.astro + rehype plugin serve them via <picture> with srcset/sizes; hero gets fetchpriority=high
- footer CTA and active service-nav item meet WCAG AA contrast
- homepage Learn More link gets screen-reader-only context
- llms.txt: insights described as coming soon
2026-10-23 16:58:13 -04:00
bmr_bluecap 002845c880 chore: untrack Playwright MCP captures and root screenshot; trim docker build context
Also records the gitea runner durable fix in the launch checklist.
2026-10-23 16:58:13 -04:00
gitea-actions[bot] 76326da0ca ci: deploy 04e0c4d9cd [skip ci] 2026-10-23 19:40:07 +00:00
bmr_bluecap 04e0c4d9cd insights: coming-soon page; block AI training crawlers; fix stale e2e
CI / ci (push) Successful in 1m34s
- /insights/ is a reader-facing Coming soon page; developer cards removed
- robots.txt: Disallow GPTBot, ClaudeBot, Google-Extended, CCBot,
  Applebot-Extended, meta-externalagent, Bytespider. Search and AI
  answer crawlers stay allowed. Corrects the old Google-Extended
  comment: it does not affect Search or AI Overviews.
- e2e: assert the real headings and the header Services link (48/48)
2026-10-23 14:40:51 -04:00
bmr_bluecap e07cb8545e docs: external uptime check via UptimeRobot -> Pushover 2026-10-23 14:33:54 -04:00
bmr_bluecap 5c825b08e9 docs: stats internal cert issued after DNS cache expiry 2026-10-23 14:32:54 -04:00
bmr_bluecap 450bba8bea docs: rewrite launch checklist as post-cutover status
Records the live architecture, launch-day findings (Pangolin cert retry,
health-check Host header, nginx 404 loop, CI rebase, homelab DNS
interception, runner socket) and the remaining open items.
2026-10-23 14:19:52 -04:00
gitea-actions[bot] c7033140f0 ci: deploy 0c2b4a6ac6 [skip ci] 2026-10-23 18:10:59 +00:00
bmr_bluecap 0c2b4a6ac6 feat: add privacy policy; wire GA4 and Umami at build time
CI / ci (push) Successful in 2m13s
- /privacy/ page covering the contact form (Formspree), Umami, GA4 and
  server logs; linked from the footer
- Analytics.astro: GA4 alongside Umami, both limited to the production
  host so staging and local builds are not counted
- Dockerfile ARGs + CI build args from repo Actions variables
  GA_MEASUREMENT_ID and UMAMI_WEBSITE_ID (non-secret)
2026-10-23 13:49:56 -04:00
gitea-actions[bot] 375ee48ab1 ci: deploy 93415c6852 [skip ci] 2026-10-23 17:42:42 +00:00
bmr_bluecap 93415c6852 ci: rebase and retry the image-tag commit when main has moved
CI / ci (push) Successful in 1m34s
A re-run of an older commit (or any push landing mid-build) made the
final push non-fast-forward, so the image was built but never deployed.
2026-10-23 12:37:42 -04:00
bmr_bluecap da1043e6f3 k8s: make www canonical; 301 the apex to www
Adds a Traefik redirectRegex middleware and a dedicated apex ingress
that reuses bluecap-website-tls, and drops the apex from the main
ingress rules.
2026-10-23 11:56:15 -04:00
bmr_bluecap d1c1726796 fix(nginx): return a real 404 instead of a 500 redirect loop
CI / ci (push) Failing after 1m50s
The 404 fallback pointed at /404/index.html, but Astro emits 404.html,
so every unknown URL looped internally and nginx answered 500. The
try_files fallback also served the error page with a 200 status.
Fall through to =404 and point error_page at /404.html.
2026-10-23 11:36:00 -04:00
bmr_bluecap e85edbfe34 docs: production cert issued; record the DNS-01 stall and resolver hardening
Marks the k3s half of section 1.1 done: bluecap-website-tls is Ready for both
production hostnames, valid to 2027-01-07, and the cluster serves it. Verified
by resolving straight to a control-plane node, so the public site stayed on
Cloudflare/Bluehost throughout.

Also records the Pangolin target table for step 2, and adds section 2.7 for
cert-manager validating DNS-01 propagation through CoreDNS rather than public
resolvers. That is not confirmed as the cause of the four-hour stall -
Cloudflare's own authoritative nameservers were returning NXDOMAIN for records
its API reported as present - but it is standard hardening worth doing before
renewals matter.
2026-10-09 15:19:12 -04:00
bmr_bluecap 6f802826df k8s: pre-issue the production TLS cert for the internal hop
Public traffic terminates TLS at the Pangolin VPS, which then proxies to the
control-plane nodes over https:443 — the pattern every other k3s-backed
Pangolin resource follows — so the cluster needs its own valid certificate for
the production hostnames, not just the staging one.

Add www.bluecapstrategies.com and bluecapstrategies.com to the tls block as
bluecap-website-tls. Safe to do before cutover, and deliberately done now:
letsencrypt-prod solves DNS-01 through Cloudflare, so issuance only needs a
temporary TXT record in the zone. No A record has to point at the cluster and
the live WordPress site is untouched. The Cloudflare token was extended to
cover the zone on 2026-10-09.

Both hostnames are listed because the canonical-host decision is still open;
holding a cert for each keeps either choice available, and the redirect is
configured separately.

Pangolin's edge certificate is HTTP-01 and cannot be pre-issued this way; it
is obtained on the first HTTPS request after DNS moves.
2026-10-09 08:05:19 -04:00
bmr_bluecap a76a6da26c docs: replace section 1.1 with a verified cutover runbook
The old entry assumed the cluster could be pointed at directly and that
DNS-01 removed the certificate ordering problem. Both were wrong once the
real traffic path was checked.

Public traffic has to transit the Pangolin VPS, so two certificates are
needed from two different systems: the edge cert from Pangolin's own Traefik
resolver, and the internal-hop cert from cert-manager, because every
k3s-backed Pangolin resource proxies to https:443 (the pattern Authentik,
paperless, immich and dav all follow).

Pangolin's resolver uses HTTP-01 on entrypoint web, not DNS-01, so the edge
cert cannot be pre-issued and a short window at cutover is unavoidable.
cert-manager can still pre-issue the internal cert. There is also only one
Cloudflare token, not two: Pangolin holds no Cloudflare credentials at all.

Records the proxied-vs-DNS-only decision, which earlier advice got backwards
(this zone is already orange-clouded while the homelab zones are not), the
Flexible-SSL redirect-loop risk, the ~5 minute rollback from ttl=auto, and
that mail is unaffected because the MX records are not proxied.

Cloudflare token now covers bluecapstrategies.com.
2026-10-09 08:01:20 -04:00
gitea-actions[bot] b51a243728 ci: deploy 55add6ba17 [skip ci] 2026-10-09 11:07:13 +00:00
bmr_bluecap 55add6ba17 fix(contact): use Formspree's reserved _subject field for the subject line
CI / ci (push) Successful in 2m6s
The hidden field was named `subject`, which Formspree treats as ordinary form
data: notifications would have arrived under a generic "New submission"
subject with "subject" showing up as just another row in the body. The
reserved field is `_subject`, with the underscore.

build 0/0, contact e2e 4 passed.
2026-10-09 07:04:59 -04:00
bmr_bluecap 120ffb7445 docs: correct section 1.4 - the form endpoint is a build-time value, not runtime
The entry said to set PUBLIC_CONTACT_FORM_ENDPOINT in the ConfigMap. That
ConfigMap is not consumed by the Deployment, and a static Astro build inlines
PUBLIC_* at image build time regardless, so there is no runtime knob to set.
Record the build-arg wiring that replaced it and the two steps that still
need the owner: creating the Formspree form and adding the
CONTACT_FORM_ENDPOINT Actions secret.
2026-10-09 07:04:59 -04:00
bmr_bluecap 0f769d7bdd feat(contact): wire the form to a real endpoint at build time
The contact form could not be made to work by configuration. Two reasons,
both fixed here.

The endpoint never reached the browser. k8s/configmap.yaml carried
PUBLIC_CONTACT_FORM_ENDPOINT, but the Deployment has no env/envFrom pointing
at that ConfigMap, so it was dead config. Wiring it up would not have helped
either: this is a static Astro build, so import.meta.env.PUBLIC_* is inlined
when the image is built and the pod only serves pre-built HTML. Anything the
browser needs has to arrive as a build arg. Add ARG/ENV to the Dockerfile and
pass --build-arg from CI, sourced from a CONTACT_FORM_ENDPOINT Actions
secret. Rewrite the ConfigMap as documentation of what the build expects,
with an explicit warning not to add envFrom.

Submission behaviour was also wrong for visitors. A native POST would have
navigated them to the form provider's page; post in the background with
fetch() and send them to our own /thank-you/ instead. On failure the form now
stays put, re-enables the button and offers the phone and email fallback,
rather than silently losing the inquiry. Add a honeypot (_gotcha, which
Formspree treats as spam) positioned off-screen rather than display:none, so
bots that skip hidden fields still fill it.

Read data-endpoint at submit time rather than caching it at init, keeping the
attribute the single source of truth.

Tests: replace the old contact assertion, which faked a native GET
navigation, with one that mocks the endpoint and exercises the real fetch
path including the posted payload. Add a case for endpoint failure.

typecheck 0/0/0, build 0/0, seo:lint clean on 20 pages, e2e 44 passed.
The 4 remaining failures are the pre-existing stale nav assertions in
docs/launch-checklist.md section 2.1, untouched here.
2026-10-09 07:04:59 -04:00
gitea-actions[bot] 8a4c4bccec ci: deploy 5bfc5981cd [skip ci] 2026-09-23 13:50:52 +00:00
bmr_bluecap 5bfc5981cd docs: record contact/draft fixes, staging URL, and DNS-01 cutover finding
CI / ci (push) Successful in 1m29s
Marks sections 1.3 and 1.5 fixed, adds section 2.6 for the staging URL, and
records that letsencrypt-prod solves via DNS-01 against Cloudflare - which
removes the HTTP-01 ordering problem from section 1.1, leaving only the
question of whether bluecapstrategies.com is reachable by the cert-manager
Cloudflare token.

Adds section 2.5 for developer-facing copy found on the /insights/ index
while fixing the draft leak. Left unfixed on purpose: what that page should
say depends on the owner decision in section 1.5.
2026-09-23 09:48:40 -04:00
bmr_bluecap 4e354d2721 fix: stop publishing draft insights; remove developer copy from contact page
Two defects that would have shipped to visitors at cutover.

Draft posts were reaching the build. The insights schema declares a `draft`
flag and rss.xml.ts filters on it, but [slug].astro called getCollection()
unfiltered, so /insights/example-draft/ was generated and listed in
sitemap-0.xml. "Example draft insight" would have been crawled and indexed.
Filter in getStaticPaths, matching the existing pattern in rss.xml.ts. Build
drops from 21 to 20 pages and the URL is gone from the sitemap.

The contact page rendered two notes written for developers, not visitors: one
explaining that the form "posts to the configured endpoint when
PUBLIC_CONTACT_FORM_ENDPOINT is set", and a "Discovery note" about the
WordPress page containing placeholder contact values. Replace the first with
a line addressed to the reader and drop the second; the deviation it
described is already recorded in docs/content-deviations.md, which is where
it belongs.

build 0/0, seo:lint clean on 20 pages. e2e unchanged at 42/46 — the 4
failures are the pre-existing stale assertions tracked in
docs/launch-checklist.md section 2.1.
2026-09-23 09:48:07 -04:00
bmr_bluecap 171c8c96aa k8s: add LAN-only staging host bcs.int.7tl-homelab.com with TLS
There was no way to view the rebuild in a browser: the ingress answered only
on websecure for the production hostnames, which have no cert and still
belong to the live WordPress site. Review meant a port-forward.

Add bcs.int.7tl-homelab.com as a staging route. The *.int zone resolves to
the internal traefik LB (10.66.15.30) and is not reachable from the internet,
so this exposes nothing publicly and leaves the real domain untouched. It
follows the same pattern as the existing argocd/lidarr/tautulli ingresses.

Enable cert-manager on the ingress, but scope the tls block to the staging
host only. The production hostnames stay out of it deliberately: requesting a
cert for names the live site still serves is a cutover step, not a
prerequisite.

Fix the issuer name while here. The commented-out placeholder said
"letsencrypt-production"; the ClusterIssuer is actually "letsencrypt-prod",
so the old hint would have silently failed to issue.

Note for cutover: letsencrypt-prod solves via DNS-01 against Cloudflare, so
the production cert can be issued before DNS moves, avoiding the HTTP-01
ordering problem. Conditional on bluecapstrategies.com being reachable by the
cert-manager Cloudflare token, which is not yet verified.
2026-09-22 15:57:09 -04:00
gitea-actions[bot] 5331e7d717 ci: deploy c19b2b2af4 [skip ci] 2026-09-22 19:49:14 +00:00
bmr_bluecap c19b2b2af4 ci: install docker + kustomize as static binaries, not distro packages
CI / ci (push) Successful in 2m37s
CI has been failing silently since 2026-08-03. The run for 9f1cbca did
execute (Gitea action_run #6, status=failure, 13m) but died before pushing an
image, so no package version exists for that commit and ArgoCD has been
serving the 2026-07-20 build ever since.

Cause: the "Ensure docker + kustomize are available" step ran
`apt-get install -y docker.io`. The runner's job image was node:18-bullseye
and Debian 11 went EOL, so debian-security stopped serving those package
versions. `apt-get update` still succeeds, so the failure surfaces only at
install time as 404s on docker.io, containerd, systemd and libcap2.

Fetch both tools as pinned static binaries instead. These have no distro
archive dependency, so the same rot cannot recur when bookworm ages out.
Verified in the real job environment: the static client installs and reaches
the host daemon through the mounted socket.

The runner job image has separately been moved to node:22-bookworm, which
also resolves the immediate breakage, but the static-binary approach is kept
as the durable fix.

Also refresh the prerequisites header: items 3 and 4 still described the
pre-ADR-0008 registry design (k3s now pulls over public HTTPS, and the
private package's pull auth comes from External Secrets).
2026-09-22 15:45:57 -04:00
bmr_bluecap 79e7fd4b2b docs: add maintained launch checklist; de-duplicate stale task lists
Cutover tasks were scattered across three sources that disagreed with each
other and with reality: planning/parity-checklist.md (git-ignored, content
only), docs/seo-aeo-optimization.md (SEO only, listed the Zod schema,
seo-lint and breadcrumbs as open when all three are done, and called for
Uptime Kuma which ADR-0004 replaced with monylog), and session notes.

Add docs/launch-checklist.md as the single source of truth, verified against
the working tree, the cluster and ArgoCD rather than inherited from the older
docs. Point the SEO doc's two checkbox blocks at it and keep only reference
detail there.

Also records blockers that were not tracked anywhere: TLS never wired on the
ingress, developer-facing copy rendered on the contact page, the contact form
having no endpoint, and the draft insight post building into the sitemap
because the draft flag is declared in Zod but never filtered on.
2026-09-22 15:45:48 -04:00
bmr_bluecap 9f1cbca1b1 Website cnages trying to get it to be an exact copy of what is currently out on the web via Bluehost
CI / ci (push) Failing after 13m9s
2026-08-03 09:45:10 -04:00
gitea-actions[bot] 042e80ac2c ci: deploy fc64909f3d [skip ci] 2026-07-20 12:30:05 +00:00
gitea_admin fc64909f3d docs: reconcile to as-built (ADR-0008, deploy runbook); clean text hero on detail pages
CI / ci (push) Successful in 1m56s
- ADR-0008: private package, External-Secrets pull over public HTTPS (no k3s restart);
  CI auth via CI_TOKEN PAT; Playwright dropped from CI. Marks ADR-0007 partially superseded.
- gitops-deployment-strategy + deployment.md updated to as-built flow + operate/troubleshoot runbook
- content-deviations: detail-page heroes are text-only (no image)
- service/focus detail pages: clean text hero (drop shared photo/icon)
2026-07-20 08:27:35 -04:00
gitea-actions[bot] 99d4a3bea9 ci: deploy b9f2c72bd4 [skip ci] 2026-07-17 11:38:52 +00:00
gitea_admin b9f2c72bd4 deploy: private package pull via External Secrets; pull over public HTTPS (no k3s restart)
CI / ci (push) Successful in 1m55s
- k3s pulls git.7tl-homelab.com/... over valid TLS (no registries.yaml/node config)
- CI still pushes internally to 10.66.15.22:3000 (fast, insecure-trusted)
- ExternalSecret materializes gitea-registry dockerconfigjson from OpenBao
- deployment uses imagePullSecrets: gitea-registry
- .env added to .dockerignore
2026-07-17 07:36:22 -04:00
gitea_admin 466a01f217 ci: drop Playwright e2e from CI (run locally); keep build + seo:lint 2026-07-17 07:33:42 -04:00
gitea-actions[bot] 504b517409 ci: deploy 79f2e8a78b [skip ci] 2026-07-16 21:53:40 +00:00
gitea_admin 79f2e8a78b ci: retry registry push on transient timeout
CI / ci (push) Successful in 6m26s
2026-07-16 17:43:44 -04:00
gitea_admin 28438dcde6 ci: use CI_TOKEN PAT for registry push and tag-bump
CI / ci (push) Failing after 20m4s
2026-07-16 16:02:00 -04:00
gitea_admin b92b29980c ci: note workflow in astro config (real change to trigger CI)
CI / ci (push) Failing after 3m3s
2026-07-16 09:01:13 -04:00
bmr_bluecap 3b49b3ba8a ci: trigger run 4 2026-07-16 08:46:51 -04:00
bmr_bluecap 3174666196 ci: trigger run 3 2026-07-16 08:29:39 -04:00
bmr_bluecap 33273f4779 ci: trigger run 2 2026-07-16 08:27:37 -04:00
bmr_bluecap 40b654c9d5 ci: trigger run 2026-07-16 08:19:28 -04:00
bmr_bluecap f62c34de88 ci: trigger run 2026-07-16 07:34:27 -04:00
gitea_adminandClaude Opus 4.8 3dab3bc9fb Add Gitea Actions CI, ArgoCD Application, and Gitea Packages wiring
- .gitea/workflows/ci.yml: build → seo:lint → e2e → image build/push →
  bump k8s image tag and commit back (paths-ignore + [skip ci] guard)
- argocd/bcs-website-application.yaml: ArgoCD App (internal Gitea repoURL, k8s path)
- k8s: image → Gitea Packages path; kustomization images newTag stanza
- ADR-0007 (Gitea Actions + Packages), supersedes GHCR/GitHub bits of ADR-0001
- Reconcile gitops-deployment-strategy.md to Gitea

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 06:29:01 -04:00
gitea_adminandClaude Opus 4.8 cd91c4b97c Initial commit: BlueCap Strategies Astro site
Static Astro 5 site for BlueCap Strategies with exact live-site content
parity, deployed to k3s via GitOps (ArgoCD).

- Page copy in Markdown content collections (services, focus-areas, pages,
  insights) with a Zod SEO schema enforced at build time
- Homepage + About restored to exact live copy; real live-site imagery
- Build-time SEO linter (scripts/seo-lint.mjs) and Playwright e2e suite
- Multi-stage Dockerfile (nginx serves dist/) and Kustomize manifests (k8s/)
- Per-agent robots.txt; config-driven PostHog + Umami analytics scaffold
- ADRs and engineering docs under docs/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:40:46 -04:00