36 Commits

Author SHA1 Message Date
6436fac3fc Merge branch 'dev' into master: proxy-forwarded-ips fix
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 26s
2026-04-22 13:08:10 -05:00
fad417697a Merge branch 'fix/proxy-forwarded-ips' into dev: trust X-Forwarded-* from off-host Caddy 2026-04-22 13:08:01 -05:00
5aad7fd48f fix: trust X-Forwarded-* when Caddy is on another host
Site was loading under https but Starlette built logo/static URLs as
http, tripping CSP `img-src 'self'`. Root cause: Dockerfile hardcoded
`--forwarded-allow-ips 127.0.0.1`, but with Caddy on a separate host
(10.10.99.10) reverse-proxying to this VM, the container sees the
request's source IP as the Docker bridge gateway (~172.17.0.1), not
Caddy's real IP — uvicorn discarded X-Forwarded-Proto and fell back
to scheme=http.

Fixes both now and later:

- Dockerfile CMD switched to `sh -c exec uvicorn ... --forwarded-allow-ips
  "${FORWARDED_ALLOW_IPS:-127.0.0.1}"`. PID-1 / signal behaviour preserved
  via exec. Default stays 127.0.0.1 so host-uvicorn dev is unchanged;
  prod sets FORWARDED_ALLOW_IPS=* via .env or compose `command:`.
- docker-compose.prod.yml gets an explicit `command:` override so the
  currently-deployed image (with the hardcoded 127.0.0.1 flag) can be
  fixed without waiting for a rebuild. Also corrected the port binding
  comment + default: Caddy is off-host, so the port must be reachable
  over the LAN — restrict with host firewall / OPNsense instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:07:54 -05:00
a65ff61da6 Merge branch 'dev' 2026-04-22 12:59:29 -05:00
45271e15ac Merge branch 'master' of ssh://git.sneakygeek.net:222/ptarrant/chicken_babies_site
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 29s
2026-04-22 12:58:33 -05:00
81cd4eb803 docker for prod 2026-04-22 12:58:23 -05:00
0adecb908a Merge pull request 'dev into master' (#1) from dev into master
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m2s
Reviewed-on: #1
2026-04-22 13:04:31 +00:00
fbd822e8dd edits to .env.example for new port 2026-04-22 08:01:05 -05:00
07d36fe73c Merge branch 'chore/prod-port-8080' into dev: container port 8080 2026-04-22 07:59:22 -05:00
62de2685d7 chore: move prod container port from 8000 to 8080
- Dockerfile: EXPOSE + HEALTHCHECK + uvicorn --port all shift to 8080.
- docker-compose.yml: host:container mapping shifts to 8080:8080.
- run_dev.sh: new host-dev launcher; default PORT=8080 to stay in
  sync with the prod shape.

Caddy upstream config on the VM needs to follow to 127.0.0.1:8080.
PUBLIC_BASE_URL in the production .env should reflect the public
hostname (unaffected by the internal port change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:59:17 -05:00
daeef6f3ed Merge branch 'chore/phase-6-hardening' into dev: Phase 6 complete 2026-04-22 07:38:31 -05:00
f9f90d408e chore: phase 6 hardening — CSP/HSTS, access log, docker, backup, CI
Ships the cross-cutting hardening set:

- SecurityHeadersMiddleware: per-request nonce-based CSP, HSTS
  (production only), Referrer-Policy, Permissions-Policy,
  X-Content-Type-Options, frame-ancestors 'none', form-action 'self'.
- AccessLogMiddleware: one http_request INFO event per request
  (method/path/status/duration_ms/ip/ua). Skips /healthz, redacts
  /admin/auth/consume/<token> paths, logs 500 + re-raises on
  downstream exceptions.
- Public base.html inline nav-toggle script gets a nonce so it
  passes strict CSP without relaxing to 'unsafe-inline'.
- Dockerfile: non-root app user (uid/gid 10001) + stdlib-only
  HEALTHCHECK against /healthz.
- scripts/backup.sh: sqlite3 .backup + tar data/media with
  14-entry retention; host-side cron install documented.
- .gitea/workflows/build-image.yml: on push to master /
  workflow_dispatch, builds and publishes
  git.sneakygeek.net/ptarrant/chicken_babies_site:latest +
  sha-<short>, with GIT_COMMIT_SHA threaded as a build-arg so
  /healthz keeps reporting the right commit in deployed images.
- 8 new tests (security headers + access log).

Pre-existing dev failures (logo asset rename + RESEND env
pollution) remain unchanged; verified not Phase 6 regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:38:23 -05:00
f4dc6c266d Merge branch 'fix/post-detail-route' into dev: add missing post detail route 2026-04-22 06:53:06 -05:00
149c6580f4 fix: add /posts/{slug} detail route so post titles resolve
Post cards on the home page have linked to /posts/<slug> since
Phase 1 (per the partial's inline comment), but the matching route
and template were never registered — clicking a post title returned
a JSON 404 from FastAPI. This adds:

- PostService.get_published_by_slug() — status-filtered, parameterized
  read that treats "draft" and "unknown slug" as the same 404 so
  unpublished titles cannot be enumerated via URL guessing.
- GET /posts/{slug} public route that 404s on miss.
- public/post.html detail template mirroring about.html's safe-render
  pattern for the bleach-sanitized body_html_cached.
- Supporting .page-article__date / .page-article__back CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:53:01 -05:00
1e5e3252c6 Merge branch 'feat/phase-5-contact-form' into dev: Phase 5 complete 2026-04-22 06:47:11 -05:00
d9090f5055 feat: phase 5 contact form — hCaptcha, honeypot, rate limit, notify
Working /contact POST flow: honeypot → hCaptcha server-verify →
field validation → SlowAPI 3/hr IP rate limit → contact_submissions
row → best-effort Resend notification (Reply-To = submitter) →
generic success page. Spam paths don't persist and render the same
success page (anti-enumeration). Send failures don't break the
request path — the row is already durable.

New services: HCaptchaService (async httpx + dev fallback),
ContactService. EmailService gains send_contact_notification.
Production config validator now requires ADMIN_CONTACT_EMAIL,
HCAPTCHA_SECRET, HCAPTCHA_SITE_KEY. 23 new tests, all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 06:47:06 -05:00
67c848f329 Merge branch 'fix/admin-header-logo' into dev 2026-04-21 20:45:43 -05:00
8b9c77c3fb fix: admin header uses logo-mark + wordmark to match public site
The brand-contrast polish that renamed logo.* to logo-mark.* (and paired
it with a styled wordmark span) only touched the public layout. Admin
still pointed at the old asset paths, which broke visually after Phase 3
and then carried forward into Phase 4. Mirror the public header exactly
so Head Hen sees the same brand chrome inside the CMS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:45:39 -05:00
1b4619a01b Merge branch 'chore/phase-4-admin-cms' into dev: Phase 4 complete 2026-04-21 20:42:06 -05:00
9a8506970c feat: phase 4 admin CMS — dashboard, editor, media, CSRF
Head Hen CMS end-to-end: dashboard lists all posts (drafts + published),
Markdown editor with live preview + drag-drop image upload, Pillow media
pipeline re-encoding every upload to JPEG, post CRUD + publish toggle +
hard delete, About page edit, and double-submit CSRF cookie enforced on
every admin mutating endpoint (Phase 3's TODO markers resolved).

Slug auto-generated on create and server-locked once a post has been
published. Unpublish preserves `published_at` so re-publish keeps
original date ordering. Every admin write invalidates the read-side
Post/Page TTL caches and records an `auth_events` audit row.

CSRF middleware is narrow by design — issues/refreshes the `cb_csrf`
cookie only on `GET /admin*`, and mutating endpoints opt in via
`require_csrf_form` or `require_csrf_header` Depends. Public routes,
healthz, and pre-auth login stay untouched.

64 new tests cover slugs, CSRF, media, admin posts/pages services, and
end-to-end CMS routes. Tests never mock the DB — real temp SQLite files
per the CLAUDE.md mandate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:42:01 -05:00
76875a455e Merge branch 'fix/brand-contrast-polish' into dev 2026-04-21 20:07:35 -05:00
f5098c05f5 fix: brand contrast — chick-only header mark, ink callouts, nav polish
Phase 1 brand palette reused --c-sky for both the header background and
the word "Babies" inside the logo art, erasing it visually. Same class
of problem hit the contact page mailto callout.

- Split logo into chick mark + HTML site title so the wordmark colors
  no longer need to coexist with the header surface. Generator gains
  build_logo_mark_{png,webp} with a widest-gap column scan to crop.
- Header moves to --c-wheat; nav active state flips to ink pill with
  cream text; muted Shop link reads as coming-soon (italic + dim +
  not-allowed).
- Contact page mailto callout reskinned to ink/cream (strong CTA) and
  form note shifts from pale sky-deep to ink at 70% opacity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:07:30 -05:00
cd87db8e07 Merge branch 'chore/phase-3-admin-auth' into dev: Phase 3 complete 2026-04-21 16:20:58 -05:00
59dea99079 feat: phase 3 admin magic-link auth — tokens, sessions, rate limits, audit
End-to-end passwordless admin auth. /admin/login accepts an email, POSTs
mint a 256-bit magic-link token stored only as SHA-256 in
magic_link_tokens (15-min TTL, single-use via atomic rowcount UPDATE).
Resend delivers the link; in dev with no API key, EmailService logs a
structured magic_link_dev_fallback event with the URL so the flow works
offline. /admin/auth/consume/{token} verifies, upserts a users row
(display_name from email local-part), creates a sessions row, and drops
an itsdangerous-signed cb_session cookie (HttpOnly, SameSite=Lax, Secure
in prod). /admin renders a placeholder "Welcome, <name>" page pending
Phase 4 CMS. /admin/logout flips revoked_at rather than deleting the row
to preserve the audit trail.

Rate limits use SlowAPI's in-memory limiter (5/15min/IP on login,
20/15min/IP on consume) plus a DB per-email count to catch
IP-rotating abuse. ADMIN_EMAILS enforces allowlist; non-allowlisted
submissions return the same "check your inbox" page with no token
inserted and no email sent (anti-enumeration). Every event lands in
auth_events via AuditService: link_requested, link_consumed,
consume_failed, session_created, session_revoked, rate_limited.

Add a production config validator refusing empty RESEND_API_KEY,
RESEND_FROM, or ADMIN_EMAILS; add PUBLIC_BASE_URL for email link
construction. CSRF deferred to Phase 6 per roadmap scoping; logout
handler marked # TODO(phase-6-csrf).

Mark Phase 3 complete in docs/ROADMAP.md.
2026-04-21 16:20:51 -05:00
4b088e5045 Merge branch 'chore/phase-2-content-model' into dev: Phase 2 complete 2026-04-21 15:40:40 -05:00
0306f71763 feat: phase 2 content model + cache — SQLite schema, markdown, TTL
Stand up the full SQLite content layer: all 7 tables from the authoritative
schema with WAL + foreign-keys enforced per-connection, entity dataclasses
plus row mappers, hand-rolled versioned migrations tracked in
schema_migrations, and an idempotent Python seed (system user + welcome
post + About page).

Add a Markdown->HTML service using markdown-it-py with a strict bleach
allowlist (tables intentionally omitted on both sides). Add a typed
in-process TTLCache[K,V] and wire it into real DB-backed PostService and
PageService, both exposing invalidate_all() for Phase 4 admin writes.

Rewire / and /about to read from the DB; homepage renders the seeded
welcome post, About renders page.title + sanitized body_html_cached.
Update the Phase 1 route tests accordingly.

Mark Phase 2 complete in docs/ROADMAP.md.
2026-04-21 15:40:35 -05:00
28168f57b6 Merge branch 'chore/phase-1-public-skeleton' into dev: Phase 1 complete 2026-04-21 15:21:27 -05:00
f77da87eaa feat: phase 1 public site skeleton — layout, routes, CSS, logo pipeline
Ship base Jinja layout (header/nav/main/footer with skip link and aria-current),
mobile-first single-file CSS using the ROADMAP palette tokens, and four public
routes: /, /about, /contact, /shop. Blog index renders via a stable
PostService.list_published() stub returning [] — Phase 2 only swaps the body.
About is static placeholder copy, /contact ships an inert form plus a mailto:
link driven by ADMIN_CONTACT_EMAIL, /shop shows a "Coming soon" card.

Adds a Pillow-based scripts/generate_static_assets.py producing resized logo
PNG + WebP, multi-size favicon.ico, and a 180x180 apple-touch-icon on a cream
background. Outputs committed for a reproducible build.

Also ship docs/MANUAL_TESTING.md with per-route / responsive / a11y / static-
asset checklists, and mark Phase 1 complete in docs/ROADMAP.md.
2026-04-21 15:21:21 -05:00
e830e5da50 Merge branch 'chore/phase-0-foundation' into dev: Phase 0 complete 2026-04-21 14:44:46 -05:00
78dd1ac243 feat: phase 0 foundation — FastAPI scaffold + /healthz
Scaffold app/ package, pinned requirements.txt, multi-stage Dockerfile,
docker-compose.yml, and .env.example. Add typed pydantic-settings loader
with full env contract and a production validator that refuses the
dev-sentinel SECRET_KEY. Wire structlog with an APP_ENV-driven renderer
(console in dev, JSON in prod). Ship a minimal unauthenticated /healthz
returning {status, version, commit_sha} with commit SHA fed through a
GIT_COMMIT_SHA build arg.

Also mark Phase 0 complete in docs/ROADMAP.md.
2026-04-21 14:44:41 -05:00
cd3b2e0694 Merge branch 'dev' into master: project bootstrap 2026-04-21 14:25:13 -05:00
22f357f3e8 Merge branch 'chore/docs-reorg' into dev 2026-04-21 14:25:00 -05:00
a376207243 chore: move code_guidelines and security under docs/
Keeps repo root lean: CLAUDE.md is the only doc at root. All
reference/architecture material lives under docs/.

Also updates all cross-references in CLAUDE.md, docs/README.md,
and the FastAPI override note in code_guidelines.md so links stay
valid after the move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:24:54 -05:00
5f3bd69e95 Merge branch 'docs/bootstrap' into dev 2026-04-21 14:22:39 -05:00
bf4352231a docs: bootstrap project instructions and roadmap
Add CLAUDE.md with authoritative stack (FastAPI + Jinja2 + SQLite),
deployment topology (CF-proxied -> OPNsense -> Caddy -> VM),
security must-haves, magic-link auth, and project git flow.

Add docs/ROADMAP.md with phased build plan, dataclasses, SQL schema,
caching strategy, visual design tokens (light-blue farm palette),
and .env contract.

Commit generic code_guidelines.md and security.md, note FastAPI
per-project override. Add docs/README.md. Commit logo assets and
.gitignore (venv, .env, data/, SQLite, caches).

No app code in this pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:22:34 -05:00
2f84a6327f first commit 2026-04-21 13:54:59 -05:00