Skip to content

Poll only what can change, and deploy production from a release - #154

Merged
thalida merged 7 commits into
mainfrom
fix/issue-153-live-updates-local-only
Aug 9, 2026
Merged

Poll only what can change, and deploy production from a release#154
thalida merged 7 commits into
mainfrom
fix/issue-153-live-updates-local-only

Conversation

@thalida

@thalida thalida commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Two things: the live-updates fix from #153, and the Forgejo deploy path.

Closes #153

Live updates only where they mean something

Live updates defaulted on and polled /api/manifest every 5s for the app's lifetime regardless of what was loaded.

That only makes sense for a working tree. The poll compares content_signature, which tracks the tree's mtime / size / dirty state, and a remote source is cloned once via ensure_clone and never re-fetched — so its signature cannot move. Every poll against a clone was a scan that could not report anything, and the UI implied otherwise: the footer dot heartbeated, and the Auto-refresh settings promised the city would re-render "whenever the project's files change".

CURRENT_SOURCE_IS_LOCAL says whether the applied source is a tree on disk; LIVE_UPDATES_ACTIVE combines it with the toggle. The timer never starts for a clone rather than ticking and returning early, and because the effect tracks the source, switching between a clone and a tree starts or stops it without a reload. The footer and the settings section read that same signal, so neither can advertise watching that isn't happening.

The Auto-refresh section also says which case you're in, since its controls look identical either way. That needed SectionNode.description to widen from string to ComponentChildren, so a section can explain itself from state rather than only carrying static prose.

Deploy

A tagged release built and published the image and stopped there; deploying was a manual click in Forgejo's UI. release.yml now dispatches the deploy workflow itself, in a job that needs the release job — the image has to exist before the deploy pulls it. It no-ops with a notice when the deploy secrets aren't configured, so a fork's release still succeeds, and so does this repo's before the secrets are set.

just deploy is the same dispatch by hand, for redeploying without cutting a release. It is deliberately not wired into just release: that recipe only pushes a tag, so calling it there would fire the deploy before the image was built.

Two things I got wrong first and corrected, both the same mistake:

  • FORGEJO_HOST / FORGEJO_REPO started in the tracked .env. This repo is public, so that would have committed the host and layout of a private homelab. They live in .local/deploy.env now, seeded from a committed deploy.env.example by just setup.
  • In the workflow they started as repository variables, which aren't masked, and the step echoed both. Public repo means public Actions logs. They're secrets now, and nothing echoes either one.

Setup this PR does not do

Settings → Secrets and variables → Actions → Secrets: FORGEJO_HOST, FORGEJO_REPO, FORGEJO_TOKEN. The token needs repository → Read and Write on Forgejo and nothing else.

Verification

just test (370 pytest, 2899 vitest) and just lint green; the pre-push gate passed. 7 new tests cover the predicate and the combined gate, including that a switch flips it and that the toggle still wins for a local source.

just deploy's guard path is exercised: with nothing configured it names all three missing values and where each belongs, rather than failing on an unset variable. just setup is idempotent — a second run leaves a filled-in .local/deploy.env untouched.

Not verified: the dispatch itself. I have no access to the Forgejo instance, so the API path, the token scope, and the app input name are reasoned from the pasted workflow and Forgejo's API shape. just deploy locally is the cheap way to confirm the chain before trusting it on a real release.

🤖 Generated with Claude Code

thalida and others added 6 commits August 8, 2026 21:54
Live updates defaulted on and polled /api/manifest every 5s for the
app's lifetime regardless of what was loaded. That only makes sense for
a working tree: the poll compares content_signature, which tracks the
tree's mtime/size/dirty state, and a remote source is cloned once via
ensure_clone and never re-fetched, so its signature cannot move. Every
poll against a clone was a scan that could not report anything.

CURRENT_SOURCE_IS_LOCAL says whether the applied source is a tree on
disk; LIVE_UPDATES_ACTIVE combines it with the toggle. The timer now
never starts for a clone rather than ticking and returning early, and
because the effect tracks the source, switching between a clone and a
tree starts or stops it without a reload.

The footer's heartbeat and the Auto-refresh section read the same
signal, so neither can advertise watching that isn't happening. The
section also says which case you are in, since its controls look
identical either way.

Section descriptions widen to ComponentChildren so a section can explain
itself from state rather than only carrying static prose.

Closes #153

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tagged release built and published the image and stopped there; the
deploy was a manual click in Forgejo's UI. release.yml now dispatches
the deploy workflow itself, in a job that needs the release job, since
the image has to exist before the deploy pulls it. It no-ops with a
notice when the Forgejo target isn't configured, so a fork's release
still succeeds.

`just deploy` is the same dispatch by hand, for redeploying without
cutting a release. It deliberately isn't wired into `just release`:
that recipe only pushes a tag, so calling it there would fire the deploy
before the image was built.

Host, repo and default app go in .env, which is tracked; the token goes
in .local/deploy.env, which is not. The recipe names whichever is
missing and where it belongs rather than failing on an unset variable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FORGEJO_HOST and FORGEJO_REPO were in .env, which is committed. This
repo is public, so together they would have published the host and
layout of a private homelab. They move to .local/deploy.env alongside
the token; .env keeps only which app to deploy, which is this project's
own name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Host and repo were repository variables, which aren't masked, and the
step echoed both. This repo is public, so its Actions logs are too, and
a release would have printed the homelab's host and repo path on every
tag. They're secrets now, and nothing echoes either one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
deploy.env.example is committed as the template for .local/deploy.env,
which isn't, and the README's Release section gains a Deploy subsection:
what a tagged release does on its own, how to redeploy without one, and
which of the four values is safe to track.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One less thing to remember after a clone: setup copies the example into
.local/ when it isn't already there, and leaves a filled-in one alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida thalida linked an issue Aug 9, 2026 that may be closed by this pull request
@thalida

thalida commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Manually verified in a dev container with local repos enabled: the live-updates behaviour reads correctly for a local working tree and for a clone, including switching between them.

The Forgejo dispatch is still untested end to end — the API path, token scope and app input name remain reasoned from the pasted workflow rather than confirmed against the instance. just deploy is the cheap way to confirm that chain once the secrets are in.

A release publishes an image and now deploys production, so a v* tag
from anyone with write access would ship to prod. The release job runs
only for the repo owner, and the deploy job needs it, so one guard
covers both.

This is the second line of defence, not the first: a tag ruleset
restricting who can create v* stops the tag itself rather than the run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thalida
thalida merged commit 3d5b882 into main Aug 9, 2026
1 check passed
@thalida
thalida deleted the fix/issue-153-live-updates-local-only branch August 9, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Only run live updates for local sources

1 participant