interactive-json-form · git:20260728.8451bdd · 2026-07-28 · sha256 70e7e1da9cb68b73
interactive-json-form git:20260728.8451bddB
Immutable. This exact content never changes and is served at /api/v1/blob/70e7e1da9cb68b73.
---
name: interactive-json-form
description: Build a self-contained, single-file interactive HTML page (explanations + multiple-choice votes + rank + free-form comments) whose responses round-trip as downloadable/importable JSON — no backend, no tracking — and publish it privately on eon.25u.com behind noindex + an unguessable path.
allowed-tools: Read, Write, Edit, Bash
---
# Interactive JSON Form — decision/survey pages with a client-side JSON round-trip
> **Self-Evolving Skill**: This skill improves through use. If instructions are
> wrong, parameters drifted, or a workaround was needed — fix this file
> immediately, don't defer. Only update for real, reproducible issues.
A **single self-contained `.html` file** that presents extensive explanations,
lets a small known audience (e.g. a CEO + data scientists) **pick options, rank
them, and comment**, then hands their input back as a **downloadable / copyable /
re-importable JSON** — with **no server, no submit endpoint, and no tracking**.
Published privately on `eon.25u.com` behind a `noindex` header + an unguessable
path, so it is reachable by whoever has the link (incl. from China via the
Telus-direct door) but blocked from crawlers/indexing.
## When to use this skill
- You need a few named humans to make **structured choices + free-form comments**
on a set of options, and you want their answers back as **machine-readable JSON**
you ingest directly.
- Mobile-friendly, re-openable (import a prior JSON to resume), zero tooling for
the responder, and **no backend to stand up**.
- The audience is small and known → a downloadable JSON they send you beats a
hosted collector; a public form would over-expose.
## When NOT to use it
- Anonymous mass surveys / thousands of respondents (use a real survey backend).
- You need server-side aggregation in real time (add a collector, or use Lark Base
— see `../../lark-automation` for the Base decision-harness alternative).
- Multi-page sitemap sites with no JS → use the sibling **`page-template`** skill.
## Why this over Lark Base / Forms / cards (decided by research 2026-07-13)
Lark Base grids truncate long text (row-height/width are NOT API-settable), Forms
are read-only after submit with no create API, and cards are one-shot with a 30-day
window + a callback webhook. A self-contained HTML page has **none** of those
limits: full text wraps natively, edits are unlimited, and the JSON round-trip is
pure client-side. The cost is that responses come back as files, not a live table
— acceptable for a small known audience.
## The pattern (what makes it work)
1. **Data-driven options.** An `OPTIONS` array at the top of the inline `<script>`
is the single source of truth; the DOM is generated from it. To reskin for a
new decision, edit that array only.
2. **`collect()` → JSON.** Reads every control (respondent, per-option vote/rank/
comment, overall) into a versioned object `{schema, respondent, role,
firstPick, overallComment, choices:[{id,title,vote,rank,comment}]}`.
3. **Three return paths, all client-side:**
- **Download**: `Blob` + `URL.createObjectURL` + `a[download]` → a `.json` file.
- **Copy**: `navigator.clipboard.writeText` (with `execCommand` fallback).
- **Import**: `<input type=file>` → `FileReader` → `hydrate()` repopulates the
form. Import→export is **byte-identical** (resumable editing) — assert this.
4. **`schema` version string** on every payload so the ingester can evolve.
5. **`beforeunload`** guard warns on unsaved edits.
6. **Self-contained**: inline `<style>` + `<script>`, no external requests, no
fonts/CDN, `<meta name="robots" content="noindex, nofollow, noarchive,
nosnippet">`, `<meta name="referrer" content="no-referrer">`.
Start from **`templates/index.html`** in this skill — it is the working page
(built for the ODB × time-bar decision) with the mechanism intact; swap the
`OPTIONS` array + the two intro `.note` blocks and you have a new page.
## Verify before you ship (headless, local)
Render + exercise the JS with Playwright against `file://` (no login needed):
fill the form via `page.evaluate` (the radios are visually hidden labels — set
`.checked` + dispatch `change` rather than `page.check`), call `collect()`, then
reload a fresh page, `hydrate()` the JSON, `collect()` again, and assert the two
JSON strings are identical. Screenshot `fullPage:false` and eyeball it. See the
`local-shot.mjs` pattern.
## Publish privately on eon.25u.com (no Caddy change, no restart)
`eon.25u.com:443` on **gpu-host** is a Caddy `static+proxy` front door whose
docroot is `/var/www/eon-25u-com-openclaw-guide` and which **already emits
`header X-Robots-Tag "noindex, nofollow, noarchive, nosnippet"` globally**. So a
static file dropped into a subdir is served instantly with noindex, over the
already-WAN-forwarded `:443` chain — **no Caddyfile edit, no `caddy reload`**
(zero risk to the co-hosted `:8450` inference other collaborators use).
```bash
# 1. upload the built page
scp -q your-page.html gpu-host:/tmp/page.html
# 2. drop it at an UNGUESSABLE path (docroot is caddy-owned → sudo; tca has passwordless sudo)
ssh gpu-host 'set -e
P="<slug>-$(openssl rand -hex 16)" # unguessable path segment
D="/var/www/eon-25u-com-openclaw-guide/$P"
sudo mkdir -p "$D"; sudo cp /tmp/page.html "$D/index.html"
sudo chown -R caddy:caddy "$D"; rm -f /tmp/page.html
echo "URL: https://eon.25u.com/$P/"
curl -sSI --resolve eon.25u.com:443:192.168.0.111 "https://eon.25u.com/$P/" | grep -iE "^HTTP|x-robots-tag"'
# 3. TRUE external reachability (on-LAN 200 does NOT prove the WAN chain — split-DNS short-circuits):
ssh <host> 'curl -sS -o /dev/null -w "EXTERNAL %{http_code} TLS=%{ssl_verify_result}\n" https://eon.25u.com/<PATH>/'
```
**Crawler-blocking layers** (this skill's default): (a) global `X-Robots-Tag
noindex` header (already on the door), (b) page `<meta robots noindex>`, (c) an
unguessable path with no inbound links. A 4th layer — **User-Agent blocking** —
needs a Caddy `@bots` matcher in the edge-route registry (`edge-routes.toml` +
`generate-edge-config.py`); it is a production config change, so treat it as
optional hardening, not default.
Registry SSoT for the door: `~/eon/private-monorepo/.../edge-routes.toml`
(`eon-openclaw-guide-and-installer-https-443`). Precedent for unguessable-path
publishing: the `odb-live-trade-console` capability door ("noindex inherited").
## Ingesting responses
The JSON is exactly `collect()`'s output. Consume it directly; key on
`schema` for versioning and `respondent`/`role` to attribute. Merge multiple
respondents' files by `choices[].id`.
## Hard rules
- One file, no external network requests, no tracking, `noindex` in `<meta>`.
- Never a real submit/collector endpoint unless the user explicitly opts in
(it turns a private artifact into a hosted service + a data-handling duty).
- Never publish to the eon.25u.com door by hand-editing the live Caddyfile — drop
a static file into the docroot (config-free) or change the **registry** + regen.
- Always verify the import→export round-trip is byte-identical before shipping.
## Post-Execution Reflection
After this skill completes, reflect before closing the task:
0. **Locate yourself.** — Find this SKILL.md's canonical path before editing.
1. **What failed?** — Fix the instruction. If the deploy recipe drifted (docroot,
header, sudo), fix it here AND cross-check `edge-routes.toml`.
2. **What worked better than expected?** — If a new control type (multi-select,
drag-rank) recurs, fold it into `templates/index.html`.
3. **What drifted?** — Keep the JSON schema, the deploy commands, and the
crawler-block layers aligned with reality.
4. **Log it.** — Evolution-log entry with trigger, fix, evidence.
Do NOT defer. The next invocation inherits whatever you leave behind.