v0.282.0: after_setup (the app's own sign-up switch), close sign-up now (decision 49), probes read lists + done_status (R-715)
gates / gates (push) Successful in 27s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-29 16:19:12 +02:00
parent 6fc3059167
commit 4110da50e9
25 changed files with 1864 additions and 36 deletions
+33 -9
View File
@@ -134,6 +134,9 @@ func (m *Manager) OpenSignupWindow(name string) (string, error) {
if err := m.removeSignupBlockFile(name); err != nil {
return "", err
}
if st.Meta.AfterSetup != nil && len(st.Meta.AfterSetup.Env) > 0 { // v0.282.0: the app's own switch opens too (a restart)
m.goNativeLock(name, false, "the household's window")
}
m.logger.Printf("[INFO] [stacks] %s: the household opened sign-up until %s — the loop closes it again", name, until)
return until, nil
}
@@ -178,9 +181,27 @@ func (m *Manager) reconcileSignupBlocks() {
if err := m.writeSignupBlock(n, w.hosts, w.fragment); err != nil {
m.logger.Printf("[ERROR] [stacks] %s: the sign-up block could not be written: %v — sign-up is OPEN until it is", n, err)
}
// v0.282.0: the app's own switch back on (after the household's window), or a retry of a failed attempt —
// at most every nativeLockRetry, so a switch that cannot be set does not restart the app every tick.
if st, ok := m.GetStack(n); ok && st.Meta.AfterSetup != nil && st.AppConfig != nil && st.AppConfig.SetupGate != nil &&
st.AppConfig.SetupGate.NativeLock != NativeLockApplied {
last := st.AppConfig.AfterSetup
due := last == nil || last.OK
if !due {
if t, err := time.Parse(time.RFC3339, last.At); err != nil || m.now().Sub(t) > nativeLockRetry {
due = true
}
}
if due {
m.goNativeLock(n, true, "the loop (window ended or retry)")
}
}
}
}
// nativeLockRetry: how often the loop retries an app's own switch that could not be set.
var nativeLockRetry = 30 * time.Minute
// SetupGateProbe asks the app's own "setup done" status once (the household's button asks it first, Part A of the
// 2026-09-29 afternoon brief). has=false: the template declares no probe.
func (m *Manager) SetupGateProbe(name string) (has bool, done bool, got string, err error) {
@@ -192,17 +213,20 @@ func (m *Manager) SetupGateProbe(name string) (has bool, done bool, got string,
if p == nil || p.URL == "" {
return false, false, "", nil
}
body, err := setupGateProbeGet(p.URL)
if err != nil {
return true, false, "", err
}
done, got = probeSaysDone(body, p.Field, p.Done)
return true, done, got, nil
done, got, err = probeOnce(p)
return true, done, got, err
}
// SetSetupGateProbeGetForTest swaps the probe's HTTP read (a test seam for other packages); returns the restore.
// f's error means "unreadable"; a nil error is a 200 with that body.
func SetSetupGateProbeGetForTest(f func(url string) ([]byte, error)) func() {
old := setupGateProbeGet
setupGateProbeGet = f
return func() { setupGateProbeGet = old }
old := setupGateProbeFetch
setupGateProbeFetch = func(url string) (int, []byte, error) {
b, err := f(url)
if err != nil {
return 0, nil, err
}
return 200, b, nil
}
return func() { setupGateProbeFetch = old }
}