Personal portfolio site
Find a file
Alan 8fab5439e4 docs(session): TLS DNS-01 attempt/revert handoff; archive portfolio-refresh
Homelab-infra session anchored here (Traefik ACME cert renewal); no site code
changed, still v0.2.0. Records the DNS-01 to HTTP-01 revert and the cert
topology (Cloudflare edge wildcard vs Traefik origin/duckdns certs), and
archives the shipped portfolio-refresh notes to a dated file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNEDPqEwRrr1xEuFxarWCF
2026-07-06 00:45:54 -04:00
design_handoff Initial commit: alangaudet.dev project setup 2026-05-05 04:14:55 -04:00
docs docs(session): TLS DNS-01 attempt/revert handoff; archive portfolio-refresh 2026-07-06 00:45:54 -04:00
e2e test(portfolio): resync e2e to 9 projects + E1/E4/E5 coverage; refresh docs 2026-07-05 20:22:46 -04:00
public post-launch iteration: typography polish, support page, case study diagrams 2026-05-05 19:41:26 -04:00
src feat(hero): reconcile Hero copy to general engineering + bump to v0.2.0 2026-07-05 20:49:00 -04:00
tests/components post-launch iteration: typography polish, support page, case study diagrams 2026-05-05 19:41:26 -04:00
.env.example feat: add Docker infrastructure (Nginx, DDNS, Uptime Kuma) 2026-05-05 05:58:21 -04:00
.gitignore test(portfolio): resync e2e to 9 projects + E1/E4/E5 coverage; refresh docs 2026-07-05 20:22:46 -04:00
AGENTS.md docs: track project charter (AGENTS/GEMINI symlink) and README 2026-06-28 21:18:40 -04:00
astro.config.mjs feat: scaffold Astro project with design tokens and WOFF2 fonts 2026-05-05 05:32:59 -04:00
CLAUDE.md test(portfolio): resync e2e to 9 projects + E1/E4/E5 coverage; refresh docs 2026-07-05 20:22:46 -04:00
DESIGN.md test(portfolio): resync e2e to 9 projects + E1/E4/E5 coverage; refresh docs 2026-07-05 20:22:46 -04:00
docker-compose.yml chore(docker): restart:always + finalize uptime-kuma migration 2026-06-28 21:18:40 -04:00
GEMINI.md docs: track project charter (AGENTS/GEMINI symlink) and README 2026-06-28 21:18:40 -04:00
nginx.conf fix: repeat security headers in nginx HTML location block 2026-05-05 13:51:17 -04:00
package-lock.json build(check): add astro check type gate (Task 0) 2026-07-05 19:31:29 -04:00
package.json feat(hero): reconcile Hero copy to general engineering + bump to v0.2.0 2026-07-05 20:49:00 -04:00
playwright.config.ts test: add Playwright E2E tests for all routes and key interactions 2026-05-05 06:29:24 -04:00
README.md docs: sync handoff + README to the now-pushed Codeberg state 2026-06-28 23:47:25 -04:00
TODOS.md docs: session handoff for v0.1.0 launch + archive planning notes 2026-05-05 14:18:30 -04:00
tsconfig.json feat: scaffold Astro project with design tokens and WOFF2 fonts 2026-05-05 05:32:59 -04:00
vitest.config.ts refactor(projects): remove CI-badge subsystem (Task 1) 2026-07-05 19:35:57 -04:00

alangaudet.dev

Personal portfolio site for Alan Gaudet. Astro 5.x static site with React 19 islands, served by Nginx Alpine, fronted by Traefik v3 with Let's Encrypt HTTPS, Cloudflare DNS proxy, and off-box Uptime Kuma monitoring (on the RTX 3080).

Live: https://alangaudet.dev Status: https://status.alangaudet.dev

For design rationale and the full architecture diagram, read DESIGN.md. For the deferred backlog, read TODOS.md. For the change history of recent sessions, read docs/SESSION_NOTES.md and the dated archives next to it.


Quick Reference

The four commands you'll run most:

cd /mnt/TempNVME/projects/alangaudet-dev

# Local dev with HMR
npm run dev                              # http://localhost:4321

# Build + deploy to live (the only deploy step)
npm run build && docker compose restart nginx

# Run tests before deploying anything risky
npm test                                 # Vitest unit tests
npm run test:e2e                         # Playwright E2E

# Check what's actually live
docker compose ps

The site has no CI/CD pipeline yet (deferred in TODOS.md). Deploys are manual: run npm run build to update dist/, then docker compose restart nginx to make Nginx pick up the new files. Nginx bind-mounts ./dist into the container as read-only, so a fresh build is visible after a restart.


Project Layout

/mnt/TempNVME/projects/alangaudet-dev/
  README.md              (this file)
  DESIGN.md              (architecture + design source-of-truth)
  TODOS.md               (deferred backlog)
  CLAUDE.md              (scope rules for AI agents working here)
  docker-compose.yml     (nginx + ddns services)
  nginx.conf             (security headers + cache rules)
  .env                   (CF_API_TOKEN, chmod 600, gitignored)
  .gitignore
  package.json
  astro.config.mjs
  tsconfig.json
  playwright.config.ts
  vitest.config.ts
  docs/
    SESSION_NOTES.md             (current handoff for next session)
    SESSION_NOTES_*.md           (dated archives of prior sessions)
    superpowers/                 (specs/plans for AI agents)
  src/
    layouts/Layout.astro         (global head, fonts, nav, footer)
    pages/                       (route files)
      index.astro                (home)
      projects/index.astro       (project list)
      projects/[id].astro        (case study template)
      resume.astro
      contact.astro
      support.astro              (crypto donations w/ QR codes)
      404.astro
    components/                  (Astro static + React islands)
      diagrams/                  (case study architecture SVGs)
    content/projects.ts          (project data; CATEGORICAL stats only,
                                  no precise counts. See CLAUDE.md.)
    styles/
      tokens.css                 (canonical type scale + colour tokens)
      project-card.css
      print.css                  (resume page)
    assets/fonts/                (Inter + Liberation Mono, WOFF2)
    test/                        (Vitest setup)
  tests/
    components/                  (Vitest unit tests for React islands)
  e2e/                           (Playwright E2E specs)
  public/qr/                     (BTC + Doge donation QR SVGs)
  dist/                          (Astro build output, bind-mounted to Nginx)
  design_handoff/                (historical design prototype, reference only)

dist/ is the only directory Nginx serves. Everything outside dist/ is source. The path /mnt/TempNVME/projects/alangaudet-dev/ is also reachable via the symlink ~/docker-containers/alangaudet-dev/ if you prefer the docker-services tree.


Architecture in One Glance

Browser
   |
   v
Cloudflare Edge  (DNS proxy, CDN, DDoS, "orange cloud" enabled)
   |
   v   (A record alangaudet.dev kept current by `ddns` container)
Home router  (port-forwards 80/443 to the host)
   |
   v
Traefik v3  (runs in a SEPARATE compose at ~/docker-containers/alanbot/.
             Joins the `alanbot_default` external network. Owns Let's
             Encrypt HTTP challenge. NOT managed from this repo.)
   |
   |--->  nginx container (this compose)         alangaudet.dev
   |--->  Uptime Kuma on RTX 3080 (file provider) status.alangaudet.dev
   |--->  Seerr, Tautulli, Wizarr, ...           (other docker-containers/)

Two containers are defined in this repo's docker-compose.yml:

  1. nginx — serves dist/ on port 80 inside the network. Traefik labels route alangaudet.dev to it over HTTPS.
  2. ddnsfavonia/cloudflare-ddns:1, hardened (read_only, no caps, no-new-privileges). Updates the A record when the home IP changes. Reads CLOUDFLARE_API_TOKEN from .env. PROXIED=true keeps the Cloudflare orange cloud on. status.alangaudet.dev is no longer served from this compose. Uptime Kuma moved to the always-on RTX 3080 (10.10.10.206:3001) on 2026-06-19 so monitoring doesn't share fate with the box it watches. Traefik here proxies the hostname to it via the alanbot file provider (~/docker-containers/alanbot/dynamic/rtx3080.yml). Manage Kuma on the 3080.

The alanbot_default network is external: true in the compose file, which means it's owned by the separate alanbot compose. If that network goes away, none of these containers can route. See "Site won't load" below.


Deploy Workflow

The site has no CI. Deploys are manual and idempotent.

Standard content change

cd /mnt/TempNVME/projects/alangaudet-dev

# 1. Make your edits in src/

# 2. Build (compiles TS, bundles React islands, writes to dist/)
npm run build

# 3. Restart nginx to pick up the new dist/
docker compose restart nginx

# 4. Verify
curl -sI https://alangaudet.dev | head -5

npm run build is the only step that touches dist/. The Nginx container restart is what makes the change visible (Nginx caches metadata at startup; a SIGHUP would also work, but docker compose restart is simpler and honest).

Total deploy time: typically 3-5 seconds for the build, 1-2 seconds for the restart.

# Type check + bundle dry run
npm run build

# Unit tests for React islands
npm test

# E2E across all routes (slower, ~30s)
npm run test:e2e

The Vitest suite covers the four React islands (TrajectoryBand, ProjectCard, CaseStudyTOC, EmailCopyButton). Playwright covers all 7 routes on desktop + mobile (Pixel 7 profile, not iPhone, since only Chromium is installed locally).

Local preview without deploying

npm run dev          # http://localhost:4321 with HMR
# OR
npm run build && npm run preview     # static preview of dist/

npm run dev is the right tool for iterating. Don't use it for production parity checks — Nginx + dist/ is what visitors see.

Rolling back a bad deploy

There is no automatic rollback. Two options:

# Option A: revert the source change, rebuild, restart
git revert HEAD
npm run build && docker compose restart nginx

# Option B: rebuild from a known-good commit
git checkout <good-sha> -- src/
npm run build && docker compose restart nginx
git checkout HEAD -- src/      # restore your working state

Option A is safer because it leaves a forward audit trail in git. Option B is faster if you need to mitigate first and investigate later.


Maintenance

Daily

Nothing required. Containers are set to restart: always, so a host reboot brings them back automatically once the Docker daemon is up. They were previously restart: unless-stopped, which does NOT survive a full reboot (only a daemon restart) — that left the site down for ~17h on 2026-06-28. See docs/SESSION_NOTES.md.

Weekly

Open https://status.alangaudet.dev and confirm the HTTPS monitor is green and the cert expiry monitor still shows >14 days. If it shows <14 days, the renewal probably failed; see "SSL cert problems" below.

Monthly

# Container image freshness
docker compose pull           # pulls newer tags for nginx:alpine, ddns:1
docker compose up -d          # recreates only the changed images

# Astro/React/dep freshness (review before applying)
npm outdated                  # shows minor/major drift

For dep updates, prefer npm update for patch/minor and stage majors as their own commits with a build + test run. React 19 + Astro 5 is the current floor; downgrading either is not supported.

Quarterly

Review TODOS.md. Items there are deliberately deferred, but the "Cloudflare Tunnel migration" and "External uptime monitoring (uptimerobot backup)" are good candidates for the first quarterly slot you have.


Logs and Observability

# Live tail of both containers
docker compose logs -f

# Just nginx (most useful for HTTP issues)
docker compose logs -f nginx

# Last 200 lines of ddns (for "is the IP being updated" questions)
docker compose logs --tail 200 ddns

# Uptime Kuma (status page) now runs on the RTX 3080, not here:
#   ssh 10.10.10.206 'docker logs --tail 100 uptime-kuma'

# Container health, ports, restart counts
docker compose ps
docker stats --no-stream      # CPU + memory snapshot

Telegram alerts route through @jarvisthethirteenth_bot (configured inside Uptime Kuma's web UI on the RTX 3080). If the HTTPS monitor fails for 60s with 3 retries, you'll get a Telegram message in Alan's DMs.

The Cloudflare DDNS container will print this warning on every cycle:

WARN: API token appears invalid

This is benign. The favonia container probes a general API endpoint that the scoped Zone:DNS:Edit token can't read; the actual DNS update operations succeed. Confirm with dig alangaudet.dev returning the home IP. Documented in the v0.1.0 launch session notes.


Troubleshooting

Each section starts with the symptom, then the first check, then root causes ordered most-to-least likely.

Site won't load (browser shows nothing or "can't connect")

First check:

docker compose ps                     # both containers Up?
dig +short alangaudet.dev             # DNS resolves to the home IP?
curl -vI https://alangaudet.dev 2>&1 | head -20

Root causes (most to least likely):

  1. Nginx container not running. docker compose ps shows Exit or nothing. Fix: docker compose up -d nginx. If it crashes immediately, check docker compose logs nginx for an Nginx config syntax error (usually a typo in nginx.conf).
  2. alanbot_default network gone. Compose error like "network alanbot_default declared as external, but could not be found". This happens if the alanbot compose was torn down. Fix: bring alanbot back up first (cd ~/docker-containers/alanbot && docker compose up -d), then docker compose up -d here.
  3. Traefik routing broken. This compose's labels look correct (Host(\alangaudet.dev`), tls.certresolver=letsencrypt), but Traefik itself runs from a separate compose. Check Traefik's dashboard or logs (cd ~/docker-containers/alanbot && docker compose logs -f traefik`) for routing errors.
  4. Home IP changed and DDNS lagged. Verify with dig +short alangaudet.dev vs curl -s ifconfig.me. If they differ, check ddns logs: docker compose logs --tail 50 ddns. The container updates on each cycle (5 min default).
  5. Router port forward broken. Test from outside the network (phone on cellular). If it works inside-LAN but not outside, the router's 80/443 forward is gone or pointing to the wrong host IP.
  6. Cloudflare proxy issue. Cloudflare dashboard → DNS → confirm alangaudet.dev A record is proxied (orange cloud). Try toggling to DNS-only (grey cloud) for a minute to bypass; if it works grey-only, Cloudflare's edge is the issue.

Site loads but shows stale content

# What's the timestamp on the deployed HTML?
docker exec alangaudet-dev-nginx-1 stat -c '%y %n' /usr/share/nginx/html/index.html

# Compare to your local dist/
stat -c '%y %n' dist/index.html

If the two timestamps match but the browser shows old content, it's browser cache. Force a hard reload (Ctrl+Shift+R / Cmd+Shift+R). Note that nginx.conf sets Cache-Control: no-cache on *.html and max-age=31536000, immutable on /_astro/*, so HTML should never stick in cache, but compiled JS/CSS bundles do (by design — they have content hashes in their filenames).

If the timestamps differ, you forgot one of these:

npm run build                         # writes new dist/
docker compose restart nginx          # makes Nginx see it

SSL cert problems

Symptom: Browser shows "Your connection is not private" or curl returns "SSL certificate problem".

First check:

echo | openssl s_client -connect alangaudet.dev:443 -servername alangaudet.dev 2>/dev/null \
  | openssl x509 -noout -dates -issuer

Root causes:

  1. Traefik couldn't complete the HTTP challenge. Let's Encrypt needs port 80 reachable from the internet to issue/renew. Check the router port forward and docker compose logs traefik in the alanbot compose. Rate limits: 5 failures per hour, 50 issuances per week per registered domain.
  2. Cloudflare SSL/TLS mode is "Strict" but the origin doesn't have a valid cert. This is the chicken-and-egg case during initial issuance. Set Cloudflare SSL/TLS to "Full" temporarily (not Strict, not Flexible) while the cert provisions, then back to Full once it's valid.
  3. Cert expired (>90 days, didn't renew). Should never happen because Traefik renews at 30-day mark, but verify with openssl s_client. If expired, the renewal failed silently — fix the cause (port 80, rate limit, etc.) and force a renewal by deleting Traefik's acme.json entry for this domain.

Cloudflare DDNS isn't updating

First check:

docker compose logs --tail 100 ddns | grep -iE "updated|error|fail"
dig +short alangaudet.dev
curl -s ifconfig.me                   # actual home IP

Root causes:

  1. .env missing or has a stale token. cat .env should show CLOUDFLARE_API_TOKEN=.... If empty or wrong, regenerate the token in Cloudflare → My Profile → API Tokens (scope: Zone:DNS:Edit for alangaudet.dev only) and update .env. Then docker compose restart ddns.
  2. Token revoked or expired. Cloudflare → API Tokens → check the token's status. Re-create if needed (don't reuse, scope it freshly).
  3. The "token appears invalid" warning is NOT this case. That warning prints on every cycle and is benign; the actual update operations work. Look for an updated log line as confirmation.

Status page (status.alangaudet.dev) won't load

Uptime Kuma runs on the RTX 3080 (10.10.10.206:3001), proxied here by Traefik's file provider — so checks span two boxes.

First check:

dig +short status.alangaudet.dev
ssh 10.10.10.206 'docker ps --filter name=uptime-kuma'   # Kuma up on the 3080?
curl -sI https://status.alangaudet.dev | head -3          # Traefik -> 3080?

Root causes:

  1. Wildcard CNAME * missing in Cloudflare DNS. Without the wildcard, status.alangaudet.dev resolves NXDOMAIN. Cloudflare → DNS → add CNAME * alangaudet.dev (proxied).
  2. Kuma not running on the 3080. Bring it up over SSH: ssh 10.10.10.206 'docker compose -f ~/docker-containers/kuma/docker-compose.yml up -d'.
  3. File-provider route missing or returning 502. The route lives in ~/docker-containers/alanbot/dynamic/rtx3080.yml (status-alangaudet router → service kumahttp://10.10.10.206:3001). A 502 means Traefik can't reach the 3080 — check the direct link and that Kuma is up.

Build fails (npm run build errors out)

First check: read the error. Astro errors usually point to a specific file:line. The common shapes:

  1. TypeScript type error. Fix the type. Common: missing interface Props declaration on an Astro component, or stale React 18 types trying to call into React 19. Run npx tsc --noEmit for a clean type check.
  2. Missing dependency. "Cannot find module X". Fix: npm install to sync. If npm install fails on a peer-dep conflict, delete node_modules/ and package-lock.json and reinstall fresh.
  3. Astro config error (rarer). astro.config.mjs syntax issue or a removed integration. Check the Astro 5 changelog if you recently upgraded.
  4. Out-of-memory during bundling. On a constrained box, NODE_OPTIONS=--max-old-space-size=4096 npm run build is the workaround. We have not hit this here yet.

Tests fail

Vitest: npm test

The Vitest tests live in tests/components/. Common failures:

  1. JSDOM mismatch. Vitest runs against jsdom; if a React island uses IntersectionObserver or matchMedia, the test must mock them. See src/test/setup.ts for the canonical mock setup.
  2. React 19 strict-mode double render. If a test depends on render count, switch to behavioural assertions (text appearance) instead of counting.

Playwright: npm run test:e2e

Common failures:

  1. Chromium not installed. Run npx playwright install chromium. Don't install other browsers — the test config only targets Chromium (and a Chromium-based Pixel 7 profile for mobile). iPhone profiles would need webkit, which isn't installed.
  2. Site is down. Playwright tests run against npm run dev (HMR server), not the live site. If dev doesn't start cleanly, fix the build first.
  3. Print stylesheet test flake. The /resume print test triggers window.print(); in headless mode the dialog never appears, so the test asserts state changes around the call, not the print preview itself.

Disk pressure (docker compose restart slow or OOM)

docker system df              # how much Docker is using
df -h /mnt/TempNVME           # the disk this project lives on

This project's footprint is modest (~50 MB for dist/ + a few hundred MB for node_modules/). If df shows the partition >90% full, the cause is elsewhere. Common offenders on this host:

  1. Old Docker containers/images. docker system prune (interactive) to clean up stopped containers and dangling images. Don't use -a unless you're sure no other compose project needs the cached images.
  2. Plex transcode cache if this is the plex-server box.
  3. Snapper/btrfs snapshots on the root disk. See your global feedback_root_drive_full_triage.md for the order of triage.

Known Quirks

These bit somebody once and will bite again. Documented so you don't re-debug from scratch:

  1. Nginx security headers must be repeated in *.html location blocks. add_header in a location block overrides ALL server-level add_header directives. The current nginx.conf repeats X-Content-Type-Options, Referrer-Policy, and Permissions-Policy inside the *.html block for this reason. If you add a new location block, repeat the headers there too. (Fix from commit f84b860.)
  2. DDNS "token appears invalid" warning is benign. The favonia container validates against a general API endpoint the scoped token can't read. Actual DNS update operations succeed. Confirm by checking for updated log lines and a correct dig result.
  3. Wildcard CNAME * is required for status.alangaudet.dev. Created manually via Cloudflare API during initial deploy. If you tear down DNS and recreate, remember to recreate CNAME * alangaudet.dev (proxied).
  4. Playwright mobile profile is Pixel 7, not iPhone. Chromium-only environment; iPhone profiles need webkit which isn't installed.
  5. Liberation Mono TTF in design_handoff/ is zero-filled. Build uses the system /usr/share/fonts/liberation/ source, converted to WOFF2. Don't try to use the design_handoff TTF as a font source.
  6. Stat phrasing on project cards is intentionally categorical, not numeric. The CLAUDE.md in this repo enforces this — precise test counts and LoC numbers go stale at commit cadence and invite expensive verification (one agent OOM-froze the box trying to verify a count). Edit src/content/projects.ts with phrases, not numbers.
  7. tokens.css is the canonical type scale. The homepage section h2s override --ag-text-h2 inline (32px) instead of changing the token (still 24px). This is deliberate; case study prose <h2>s elsewhere stay 24px. See DESIGN.md → "Typography Hierarchy".
  8. Remote is Codeberg (public). originhttps://codeberg.org/alan090/alangaudet-dev (wired up 2026-06-28). Daily push is just git push; auth uses the stored token in ~/.git-credentials. See "Pushing to a Remote" below for how it was created.

Backup and Recovery

This repo has no automated backup. The recovery posture is:

  1. Source code: rebuild from any git commit. If the local clone is lost, re-clone from Codeberg (origin, see below) or restore from filesystem backup.
  2. Container state: nginx is stateless. ddns is stateless. No stateful container remains in this compose — Uptime Kuma (the only one with state) runs on the RTX 3080 now, and its volume on this box was removed after the migration. Back Kuma up on the 3080 (below).
  3. .env: the Cloudflare API token. Reproducible from the source-of- truth ~/.config/cloudflare/credentials (chmod 600). If lost, copy from there and chmod 600 .env.

Uptime Kuma's data now lives on the RTX 3080 (its alangaudet-dev_uptime-kuma-data volume on this box was deleted after the 2026-06-19 migration). Back it up there:

ssh 10.10.10.206
KUMA_VOL=$(docker volume ls -q | grep kuma)     # confirm the 3080 volume name
docker run --rm -v "$KUMA_VOL":/source:ro -v "$PWD":/backup alpine \
  tar czf /backup/uptime-kuma-$(date +%F).tar.gz -C /source .

Pushing to a Remote (how it was set up)

origin is configured to Codeberg (public), wired up 2026-06-28 with the commands below — kept for reference and for re-creating the remote if needed:

# Codeberg (preferred per memory: GitHub keeps flagging Alan's repos)
curl -s -X POST "https://codeberg.org/api/v1/user/repos" \
  -H "Content-Type: application/json" \
  -H "Authorization: token $(grep codeberg ~/.git-credentials | sed 's|.*://alan090:\(.*\)@.*|\1|')" \
  -d '{"name":"alangaudet-dev","description":"Personal portfolio site","private":false}'

git remote add origin https://codeberg.org/alan090/alangaudet-dev.git
git push -u origin main

# Optional: GitHub mirror
gh repo create alan09086/alangaudet-dev --public --description "Personal portfolio site"
git remote add github https://github.com/alan09086/alangaudet-dev.git
git push -u github main

The day-to-day push is now just git push. No GitHub mirror was set up — Codeberg only (GitHub keeps flagging Alan's repos); the optional gh/github lines above are how you'd add one.

.env is gitignored. Verify with git ls-files | grep -i env returning nothing before the first push.


Emergency Procedures

Whole site is down, you have 5 minutes to get it back

# 1. Bring everything back to a known state
cd /mnt/TempNVME/projects/alangaudet-dev
docker compose down
docker compose up -d
docker compose ps              # all three Up?

# 2. If alanbot_default network is missing
cd ~/docker-containers/alanbot
docker compose up -d
cd /mnt/TempNVME/projects/alangaudet-dev
docker compose up -d

# 3. If still broken, last-resort: serve dist/ directly on a known port
docker run --rm -d -p 8080:80 -v "$PWD/dist":/usr/share/nginx/html:ro \
  --name alangaudet-emergency nginx:alpine
# then point an upstream LB / DNS at the host:8080 until you can debug

You pushed a broken build and visitors are seeing it

# Roll forward by reverting the last commit and rebuilding
git revert HEAD --no-edit
npm run build && docker compose restart nginx

# Verify
curl -sI https://alangaudet.dev | head -3

You committed .env by accident

The token is now in git history. Treat the token as compromised:

  1. Revoke it immediately in Cloudflare → API Tokens.
  2. Generate a new scoped token (Zone:DNS:Edit, alangaudet.dev only).
  3. Write the new token into .env, chmod 600 .env.
  4. docker compose restart ddns.
  5. Rewrite history with git filter-repo (or BFG) to scrub .env. If you've already pushed to a remote, force-push and consider the leak as already-public.

Versioning

Currently v0.1.0 (initial release). The package.json version field is the source of truth. There's no CHANGELOG.md yet; commit messages serve that purpose until the site sees enough churn to warrant one.

When you ship a new version:

# Bump package.json version manually
# Then commit:
git commit -am "release v0.X.Y: <one-line summary>"
git tag v0.X.Y
git push origin main --tags

Major version bump (1.0.0) is reserved for the next significant restructure (e.g. new sections, blog/writing surface, design system v2).


Where to Get More Detail