Design: put the map on its own site (phase one) #1

Merged
forgejo-admin merged 4 commits from design/celilo-hosting into main 2026-09-07 22:42:20 +00:00

Problem

The map is a blob of HTML someone pastes into Squarespace by hand. In 2025 that left the repo with two divergent copies of the page six days apart and no way to tell which was live. Squarespace has no content API and no SSH, so nothing can ever push into it.

Solution

Give the map its own address at bna-yard-sale.org, served by a celilo module living in this repo, and have Squarespace link to it once.

This is a design proposal. Nothing is implemented.

Two changes, because the second one is not decided yet

add-celilo-hosting — phase one, ready to implement. The celilo module (public_web + dns_registrar, no container), the Astro page, the Squarespace link, the snippet generator kept as contingency, and design mocks before any component.

The data stays manual in phase one: export the CSV, run the pipeline, deploy. That is deliberate scope, not an oversight, and it is stated in the proposal and named in the risks.

add-data-refresh — phase two, parked with its decision open. Getting the site up first means this gets designed against something real rather than guessed at. D1 (where the refresh runs and how it gets a credential) and D2 (how a merge reaches the fleet) are written as OPEN, with the candidates recorded and none chosen.

What the research found, kept so it does not get redone

A celilo hook cannot open a pull request. source_forge exposes issue_runner_token and registerRepo and nothing else. Neither yields a token that can push a branch. Scheduling was never the hard part — the dispatcher already emits timer.tick.1m/5m/15m/1h and three production modules subscribe to it. The credential is the hard part, and it is what phase two has to settle.

The build bus is a shipped recipe. celilo/reference/APP_CI_REFERENCE.md covers merge → build → .netapp → registry → poll → deploy, and lunacycle is the first app on it. Four things worth keeping from reading it:

  • The opt-in field is auto_upgrade, not auto_update, and it is operator configuration, not a manifest declaration. The spec is explicit that a module author must not be able to opt their module into unattended upgrades on someone else's fleet. Defaults to false when unset.
  • registerRepo applies branch protection to main, so a PR flow becomes required rather than preferred.
  • The reference release.yml gates publish behind e2e on a Docker builder that is not deployed. The reference names its own workaround: drop needs: [e2e] and publish on the light runner.
  • The reference passes --allow-stale unconditionally, disabling both publish safeguards. celilo#548 tracks 21 of 21 modules with stale manifests.

There is no per-file grant for Google Forms. Its API scopes are account-wide, and every Google Forms MCP server is a community project wanting a Cloud project and a refresh token. Publishing a public-columns-only sheet tab as CSV gives exactly the per-file scope, with no OAuth and nothing to store.

Also in here

docs/one-time-setup.md — click-by-click for the form, publishing responses as CSV without leaking names and emails, the domain, and the forge. The address-validation regex was run against all 34 real 2025 addresses: it rejects none of them, and does reject "I don't know yet" and a bare street name with no number.

Status

bna-yard-sale.org is registered at Namecheap. Hourly cadence agreed. Freeya sponsor block stays. CDN-serving-stale accepted as a stretch goal.

Both changes pass openspec validate. tsc clean, 10 tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PyCzDSqUNGDS5W3bznaQW5

## Problem The map is a blob of HTML someone pastes into Squarespace by hand. In 2025 that left the repo with two divergent copies of the page six days apart and no way to tell which was live. Squarespace has no content API and no SSH, so nothing can ever push into it. ## Solution Give the map its own address at `bna-yard-sale.org`, served by a celilo module living in this repo, and have Squarespace link to it once. This is a design proposal. **Nothing is implemented.** ## Two changes, because the second one is not decided yet **`add-celilo-hosting` — phase one, ready to implement.** The celilo module (`public_web` + `dns_registrar`, no container), the Astro page, the Squarespace link, the snippet generator kept as contingency, and design mocks before any component. The data stays manual in phase one: export the CSV, run the pipeline, deploy. That is deliberate scope, not an oversight, and it is stated in the proposal and named in the risks. **`add-data-refresh` — phase two, parked with its decision open.** Getting the site up first means this gets designed against something real rather than guessed at. D1 (where the refresh runs and how it gets a credential) and D2 (how a merge reaches the fleet) are written as **OPEN**, with the candidates recorded and none chosen. ## What the research found, kept so it does not get redone **A celilo hook cannot open a pull request.** `source_forge` exposes `issue_runner_token` and `registerRepo` and nothing else. Neither yields a token that can push a branch. Scheduling was never the hard part — the dispatcher already emits `timer.tick.1m/5m/15m/1h` and three production modules subscribe to it. The credential is the hard part, and it is what phase two has to settle. **The build bus is a shipped recipe.** `celilo/reference/APP_CI_REFERENCE.md` covers merge → build → `.netapp` → registry → poll → deploy, and lunacycle is the first app on it. Four things worth keeping from reading it: - The opt-in field is **`auto_upgrade`**, not `auto_update`, and it is **operator configuration, not a manifest declaration**. The spec is explicit that a module author must not be able to opt their module into unattended upgrades on someone else's fleet. Defaults to false when unset. - `registerRepo` applies **branch protection to `main`**, so a PR flow becomes required rather than preferred. - The reference `release.yml` gates publish behind e2e on a Docker builder that is not deployed. The reference names its own workaround: drop `needs: [e2e]` and publish on the light runner. - The reference passes `--allow-stale` unconditionally, disabling both publish safeguards. celilo#548 tracks 21 of 21 modules with stale manifests. **There is no per-file grant for Google Forms.** Its API scopes are account-wide, and every Google Forms MCP server is a community project wanting a Cloud project and a refresh token. Publishing a public-columns-only sheet tab as CSV gives exactly the per-file scope, with no OAuth and nothing to store. ## Also in here `docs/one-time-setup.md` — click-by-click for the form, publishing responses as CSV without leaking names and emails, the domain, and the forge. The address-validation regex was run against all 34 real 2025 addresses: it rejects none of them, and does reject "I don't know yet" and a bare street name with no number. ## Status `bna-yard-sale.org` is registered at Namecheap. Hourly cadence agreed. Freeya sponsor block stays. CDN-serving-stale accepted as a stretch goal. Both changes pass `openspec validate`. `tsc` clean, 10 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PyCzDSqUNGDS5W3bznaQW5
Proposes moving the map off the Squarespace paste and onto its own site at
bna-yard-sale.org, deployed as a celilo module living in this repo, with an
hourly refresh that opens a pull request instead of publishing.

Three findings changed the shape of this from the first sketch.

celilo already schedules module work. The event-bus dispatcher emits
timer.tick.1m/5m/15m/1h, and a manifest's subscriptions array binds a
pattern to a hook. technitium, wireguard-manager, and wireguard all run
reconcile work this way today. The earlier idea of an LXC with a systemd
timer would have invented a scheduler celilo already has, and a Forgejo
Actions cron would have put the schedule in a second system.

A static site on celilo needs no machine. celilo-website requires
public_web and dns_registrar and declares no requires.system at all.

Google's Forms API scopes are account-wide, so there is no way to grant
access to one form. Publishing a public-columns-only sheet tab to the web
as CSV gives exactly the per-file scope we wanted, with no OAuth, no stored
credential, and no third-party MCP server.

The refresh opens a PR rather than publishing because the address field is
an oracle: anyone can submit any address and put a marker on a house whose
owner never agreed. Automating the export is worth doing. Automating the
judgement is not.

Keeps the Squarespace snippet generator permanently as the contingency. The
map exists for five hours on one Saturday, and moving it off Squarespace
moves its uptime onto the fleet.

Flags two prerequisites that are not code and have lead time:
bna-yard-sale.org is unregistered as of today, and celilo's only
dns_registrar provider is the namecheap module, so the domain has to live
at Namecheap for the record to be managed automatically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
Review found a hole in D4 that holds. A celilo hook has no way to open a
pull request. source_forge exposes exactly two operations,
issue_runner_token and registerRepo, and neither yields a token that can
push a branch. The timer.tick.1h subscription was the easy half of the
problem and the credential was the hard half, and nothing in the first draft
addressed the hard half.

So the refresh moves to a Forgejo Actions cron in this repo. A CI job runs
inside the repository and is handed a token by the forge, which makes
opening a PR ordinary rather than something to engineer around. Nothing on
the fleet holds a credential that can write to a repository. The celilo
module goes back to doing one thing: serving what is committed.

This makes the first draft's rejection of Forgejo Actions wrong, and the
doc now says so rather than quietly swapping the decision. Two alternatives
are recorded with why they lost: a Forgejo PAT held as a module secret,
which works but puts a repo-writing credential on the fleet and a hosting
module into the git business; and skipping git entirely for a pending-data
file the operator promotes, which needs no credential at all but gives up
reconstructing a publication from a commit. That reconstruction is what
answers "why is there a marker on my house", which this application will be
asked.

The whole decision now rests on an unverified assumption, so task 6.1 is a
spike that proves a Forgejo Actions job here can open a PR before any
refresh logic gets written. 6.2 says to stop and reopen D4 if it cannot.

Also resolves the other three review comments. D3 now says explicitly that
the page fetches sales.json from its own origin, same web root, no CORS.
D7 records the CDN-serving-stale idea as the better answer it is, and as
out of scope under a September deadline. D8 records that the Freeya block
stays.

bna-yard-sale.org is registered at Namecheap, so that risk is retired and
replaced by the two things that are still unproven: that the namecheap
module manages the new record, and that Caddy gets a certificate for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyCzDSqUNGDS5W3bznaQW5
Three review comments, and the first one found a real error plus a shipped
capability I had written off.

The field is auto_upgrade, not auto_update, and it is operator configuration
rather than a manifest declaration. openspec/specs/module-auto-upgrade is
explicit that eligibility for unattended upgrade "SHALL be operator
configuration, not a property the module declares about itself", so an
author cannot opt their module into unattended upgrades on someone else's
fleet. It defaults to false when unset. So it is one celilo module config
set, and putting it in the manifest is not the mechanism.

On the .netapp: the previous commit said the build half of the build bus was
unshipped and an operator would run celilo module deploy by hand. That was
half right and the wrong half mattered. celilo/reference/APP_CI_REFERENCE.md
is a complete recipe that exists to be copied, lunacycle is already on it,
and CI publish auth shipped in celilo#209 — source_forge.registerRepo mints
a scoped CELILO_PUBLISH_TOKEN and sets it as the repo's Actions secret. What
is genuinely unshipped is the Docker builder VM, and the reference names its
own workaround: drop needs:[e2e] and publish on the light runner. A static
map has no e2e worth a Docker builder, so that is the right call rather than
a shortcut. New decision D9 records all of it, including the --allow-stale
flag the reference passes unconditionally (celilo#548).

Worth noting registerRepo applies branch protection to main. That makes the
PR flow in D4 required rather than merely preferred, and it is a different
token from the PR-opening one D4 still needs to verify in task 6.1.

docs/one-time-setup.md is the click-by-click the review asked for: the form,
the co-owner, the address validation, the filtered Public tab, publishing it
as CSV, and verifying while signed out that no names or emails come back.
The address validation regex is not a guess — it was run against all 34 real
2025 addresses, rejects none of them, and rejects "I don't know yet" and
"the alley behind Kerby".

The CDN serving stale on 5xx is recorded as an accepted stretch goal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyCzDSqUNGDS5W3bznaQW5
Review said get the main site up and figure the automation out afterward.
Rather than leave one change with half its sections marked TBD, this splits
it in two, so phase one is actually applyable and phase two is a parked
decision sitting where parked decisions belong.

add-celilo-hosting is now only the map on its own site: the celilo module,
the Astro page, the Squarespace link, the snippet kept as contingency, and
the mocks. The data stays manual — export the CSV, run the pipeline, deploy
— which is a deliberate scope, stated as such in the proposal and named in
the risks rather than left to be discovered.

add-data-refresh holds the deferred work with its central decision written
as OPEN rather than pre-answered. D1 is where the refresh runs and how it
gets a credential, with all three candidates recorded and none chosen: a
Forgejo Actions cron, a timer.tick subscription plus a PAT as a module
secret, and a no-git pending-file variant. D2 is merge-to-fleet, carrying
the four things worth keeping from APP_CI_REFERENCE.md — that the field is
auto_upgrade and is operator config not a manifest declaration, that
registerRepo applies branch protection to main, that the reference's e2e
gate needs dropping since no builder is deployed, and that --allow-stale
disables both publish safeguards.

Two decisions in phase two are settled regardless of how D1 and D2 land, so
they are marked settled: the pipeline gets a non-interactive mode rather
than a second implementation, and the sheet is read through a published
public-columns tab. Their tasks are marked as startable any time.

Correcting one thing said in review: the CI path is not blocked on builder
work. The reference names its own workaround (drop needs:[e2e], publish on
the light runner), and a static map has no e2e worth a Docker builder. The
genuine unknown is whether the workflow token here can open a PR, which is
task 1.1 in phase two. Deferring is still right; the reason is narrower.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyCzDSqUNGDS5W3bznaQW5
forgejo-admin changed title from Design: host the map on celilo, refresh on a timer, keep the review to Design: put the map on its own site (phase one) 2026-09-07 22:41:03 +00:00
forgejo-admin deleted branch design/celilo-hosting 2026-09-07 22:42:21 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
celilo/bna-yard-sale!1
No description provided.