feat: add reader vote widget and vote-service
The skills-review desk is static, so "which draft would you ship?" needs a stateful counterpart. vote-service is a small Go API on its own pod backed by a JSON file on a ReadWriteOnce PVC, with one active vote per skill per source IP as the anti-abuse rule and CORS (ALLOWED_ORIGIN) as the caller boundary. Deployment notes that differ from the obvious path, all confirmed against the live cluster: the image is side-loaded with `ctr image import` plus `imagePullPolicy: Never` because kubelet has no credentials for the Nexus ref; the pod is pinned to `kubernets` because the hostpath PV takes a nodeAffinity for whichever node first binds it; and public exposure is Caddy on the VPS, not the cloudflared tunnel. The ingress controller runs with `use-forwarded-headers` off, so nginx overwrites X-Forwarded-For with its own peer — every visitor would collapse into one voter and each skill would cap at one vote overall. Caddy stamps the true remote address into X-Client-IP, which nginx forwards untouched, and clientIP() reads that first. Scoped to this app rather than flipping the global flag, which would change client-IP handling for every other ingress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ worktree practices taught by the presentation fit together.
|
||||
| Published branch | `pages` |
|
||||
| Local verification | `npm run verify` |
|
||||
| SilverBullet page | `Guides/AI For Dummies Presentation` |
|
||||
| Skills-review vote API | `vote-service/` — separate pod, see `vote-service/README.md` |
|
||||
|
||||
## How the site is built
|
||||
|
||||
@@ -148,6 +149,54 @@ https://netcracker.pages.marcospaulo.dev.br/ai-for-dummies/
|
||||
produce `ERR_SSL_PROTOCOL_ERROR` because it does not match the wildcard TLS
|
||||
certificate.
|
||||
|
||||
## Skills-review vote service
|
||||
|
||||
`skills-review/` is served by the same static Pages Server as the rest of
|
||||
this site, so it cannot itself remember votes. `vote-service/` is a separate
|
||||
Go API on its own pod for that: one JSON file as the store, one vote per
|
||||
visitor enforced by IP (a MAC address never reaches a server across the
|
||||
internet). It is deployed independently of `main`/`pages` — the site can be
|
||||
republished without touching it, and vice versa.
|
||||
|
||||
```bash
|
||||
cd vote-service
|
||||
docker build -t localhost:30892/ai-for-dummies-vote-service:latest .
|
||||
docker push localhost:30892/ai-for-dummies-vote-service:latest
|
||||
|
||||
# kubelet cannot pull that ref (no certs.d/hosts.toml for localhost:30892 →
|
||||
# `no basic auth credentials`), so side-load into containerd instead and let
|
||||
# `imagePullPolicy: Never` skip the network pull. Use microk8s's bundled ctr.
|
||||
docker save localhost:30892/ai-for-dummies-vote-service:latest -o /tmp/vote-service.tar
|
||||
/snap/microk8s/current/bin/ctr --address /var/snap/microk8s/common/run/containerd.sock \
|
||||
--namespace k8s.io image import /tmp/vote-service.tar
|
||||
|
||||
microk8s kubectl apply -f deploy/deployment.yaml # namespace + Deployment + PVC + Service
|
||||
microk8s kubectl apply -f deploy/ingress.yaml
|
||||
microk8s kubectl -n ai-for-dummies rollout restart deploy ai-for-dummies-vote
|
||||
```
|
||||
|
||||
Namespace `ai-for-dummies`, `ingressClassName: public`, no per-ingress TLS.
|
||||
The Deployment is pinned to node `kubernets` with a `nodeSelector`: the
|
||||
`microk8s-hostpath` PV carries a `nodeAffinity` for whichever node first binds
|
||||
it, so scheduling and storage have to agree on one node.
|
||||
|
||||
The vote widget's browser-side `fetch` calls must reach the API over the public
|
||||
internet — a cluster-internal-only Service would be unreachable from a
|
||||
visitor's browser even if the Pages Server happens to run on the same
|
||||
network. Exposure is therefore public, terminated by **Caddy on the Oracle VPS
|
||||
over Tailscale** (the same path as every other public host here, not the
|
||||
cloudflared tunnel), with `ALLOWED_ORIGIN`/CORS as the boundary that restricts
|
||||
which site's script may call it. After deploying, keep
|
||||
`window.SKILLS_REVIEW_VOTE_API` in `skills-review/index.html` in sync with
|
||||
`ALLOWED_ORIGIN` on the service.
|
||||
|
||||
One cluster-wide gotcha worth knowing before reading the vote code: the ingress
|
||||
controller runs with `use-forwarded-headers` off, so nginx *overwrites*
|
||||
`X-Forwarded-For`/`X-Real-IP` with the VPS's tailnet address. Caddy stamps the
|
||||
true client address into `X-Client-IP` instead. Full rationale, the Caddy block,
|
||||
and the anti-abuse design are in
|
||||
[vote-service/README.md](../vote-service/README.md).
|
||||
|
||||
## Adding or changing a presentation section
|
||||
|
||||
1. Add semantic HTML and stable `data-*` hooks in the focused chapter or `full-guide/index.html`; keep `index.html` as the short route map.
|
||||
|
||||
Reference in New Issue
Block a user