diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3218536
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Tooling caches, not part of the published site.
+.serena/
+__pycache__/
diff --git a/README.md b/README.md
index e601ba1..fcdc115 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,8 @@ npm run verify
- `hands-on/rules/` — dependency-free Guardrails lab; toggles rule sources into the prompt
- `rules/` — bilingual case study of skills, CLI ratchets, Husky, and PR review
- `skills/` — reusable design and rules-case-study skills, plus an interactive package anatomy explorer
+- `skills-review/` — static review desk for submitted skills; its reader vote widget calls the separate `vote-service`
+- `vote-service/` — small Go API + Kubernetes manifests backing the skills-review vote widget (see `vote-service/README.md`)
- `GATES.md` — acceptance ledger for the project
## Publishing
@@ -59,6 +61,16 @@ Server and Actions deployment path.
For the complete authoring, verification, publication, rollback, worktree, and
skill workflow, see [docs/operations-guide.md](docs/operations-guide.md).
+## Reader voting on the skills-review desk
+
+`skills-review/` is static, so its "which draft would you ship?" vote widget
+calls a separate stateful service — `vote-service/`, a small Go API on its
+own pod, one vote per visitor enforced server-side by IP (a MAC address is
+never visible to a server across the internet, so it cannot be used). See
+[vote-service/README.md](vote-service/README.md) for the API, the anti-abuse
+design, and the build/push/deploy steps; `skills-review/index.html` sets
+`window.SKILLS_REVIEW_VOTE_API` to point at it once deployed.
+
## Research
See [docs/references/README.md](docs/references/README.md) for official Claude,
diff --git a/docs/operations-guide.md b/docs/operations-guide.md
index 9d7b05e..1ca2846 100644
--- a/docs/operations-guide.md
+++ b/docs/operations-guide.md
@@ -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.
diff --git a/scripts/verify.mjs b/scripts/verify.mjs
index bd256f4..84d9165 100644
--- a/scripts/verify.mjs
+++ b/scripts/verify.mjs
@@ -22,6 +22,8 @@ const modelsHtml = read('models/index.html');
const agentsHtml = read('agents/index.html');
const skillsHtml = read('skills/index.html');
const reviewCatalog = read('skills-review/catalog.js');
+const reviewVoteJs = read('skills-review/vote.js');
+const voteService = read('vote-service/main.go');
for (const url of ['https://code.claude.com/docs/en/sub-agents','https://code.claude.com/docs/en/skills','https://code.claude.com/docs/en/worktrees','https://git-scm.com/docs/git-worktree.html','https://developers.openai.com/codex/skills']) if (!refs.includes(url)) throw new Error(`missing reference ${url}`);
console.log('content verification passed');
for (const token of ['data-phase="plan"','data-phase="build"','data-phase="review"','data-tree="main"','data-tree="ui"','data-worker="ui"','data-route="plan"','data-model-provider="openai"','data-model-provider="claude"','data-model-provider="gemini"','data-effort="low"','data-effort="medium"','data-effort="high"','data-skill-file="skill"','data-skill-step="observe"','data-skill-step="validate"','data-common-skill="ponytail"','data-common-skill="caveman"','data-common-skill="unlazy"','id="hands-on"','data-copy-target="prompt-install-skills"','data-copy-target="prompt-basic"','data-copy-target="prompt-skills"','hands-on/starter/','additional-reading.md','role="tablist"','
document.querySelector(selector);
@@ -158,7 +159,8 @@ function previewMarkup(entry, available) {
}
function renderDetail() {
const entry = state.selected; const available = packageFiles(entry);
- $('#detail').innerHTML = `
Choose Preview Markdown in the file toolbar to render either version.
Copy or download the version you want.
+
Select a submission, or open an author URL.
Read the gentle review before judging the draft.
Choose Preview Markdown in the file toolbar to render either version.
Copy or download the version you want, then vote for the draft you would ship.
@@ -36,8 +38,8 @@
The recommendations follow the open Agent Skills format: valid frontmatter for discovery, progressive disclosure for context economy, deterministic scripts for fragile repeated mechanics, and behavioral evaluation rather than a checklist of pretty headings.