package monitor import ( "context" "fmt" "log" "sync" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/semver" ) // GoldenVouchChecker is R-242's second half (operator ruling 2026-10-12, S4, `09` §3 decision 199): // an operator alarm when the newest PUBLISHED golden is not the one the hub VOUCHES. // // The bake half has a gate (scripts/golden_currency_gate.py), but that gate cannot see the vouch — it // lives only in the hub's settings — so a golden baked and uploaded but never vouched passed every check // and a fresh install would still land on the older controller. Here the hub compares the two things it // can see itself: Gitea's `felhom-golden` package versions and its own manifest's golden_version. // // It waits a grace period after it first sees the gap, because a bake and its vouch are normally minutes // apart in the same session; an alarm then would be noise. Gitea unreachable or an empty list = no // verdict (logged), never an alarm and never a clear. One alarm per unvouched version per process. // Pinned by TestGoldenVouchChecker. type GoldenVouchChecker struct { list func(ctx context.Context) ([]string, error) // newest-first published golden versions vouched func() string // the hub manifest's golden_version onEvent EventNotifyFunc logger *log.Logger now func() time.Time grace time.Duration every time.Duration mu sync.Mutex lastRun time.Time gapSince map[string]time.Time // unvouched newest version → when first seen alertedOn string } const ( eventGoldenNotVouched = "golden_not_vouched" goldenVouchGrace = 2 * time.Hour goldenVouchCheckEvery = 15 * time.Minute goldenVouchListDeadline = 20 * time.Second ) // NewGoldenVouchChecker: list = the Gitea package listing, vouched = the manifest's golden_version. func NewGoldenVouchChecker(list func(ctx context.Context) ([]string, error), vouched func() string, onEvent EventNotifyFunc, logger *log.Logger) *GoldenVouchChecker { return &GoldenVouchChecker{list: list, vouched: vouched, onEvent: onEvent, logger: logger, now: time.Now, grace: goldenVouchGrace, every: goldenVouchCheckEvery, gapSince: map[string]time.Time{}} } // Check runs on the hub's 60 s sweep and throttles itself to one Gitea listing per 15 minutes. func (gc *GoldenVouchChecker) Check() { gc.mu.Lock() defer gc.mu.Unlock() now := gc.now() if !gc.lastRun.IsZero() && now.Sub(gc.lastRun) < gc.every { return } gc.lastRun = now ctx, cancel := context.WithTimeout(context.Background(), goldenVouchListDeadline) defer cancel() vers, err := gc.list(ctx) if err != nil || len(vers) == 0 { gc.logger.Printf("[WARN] Golden vouch check: no verdict (published list: %d version(s), err=%v)", len(vers), err) return } newest, vouched := vers[0], gc.vouched() if vouched != "" && semver.Compare(newest, vouched) <= 0 { if gc.alertedOn != "" { gc.logger.Printf("[INFO] Golden vouch check: vouched %s is current again (was alerting on %s)", vouched, gc.alertedOn) } gc.gapSince, gc.alertedOn = map[string]time.Time{}, "" gc.logger.Printf("[DEBUG] Golden vouch check: newest published %s, vouched %s — ok", newest, vouched) return } first, seen := gc.gapSince[newest] if !seen { gc.gapSince[newest] = now gc.logger.Printf("[INFO] Golden vouch check: golden %s is published, the hub vouches %q — alarm in %s if still so", newest, vouched, gc.grace) return } if now.Sub(first) < gc.grace || gc.alertedOn == newest { return } gc.alertedOn = newest vouchedTxt := vouched if vouchedTxt == "" { vouchedTxt = "none" } msg := fmt.Sprintf("Golden %s is published but NOT vouched (the hub vouches %s, for %s now) — a fresh install still gets the older controller. Vouch it under Configuration → Day-0 artifacts, or record why not.", newest, vouchedTxt, now.Sub(first).Round(time.Minute)) gc.logger.Printf("[INFO] Golden vouch check: %s", msg) gc.onEvent("", eventGoldenNotVouched, "warning", msg, fmt.Sprintf(`{"published":%q,"vouched":%q}`, newest, vouched), "hub") }