Files
felhom.eu/hub/internal/offsite/delivery.go
T

98 lines
4.4 KiB
Go

package offsite
import (
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-70 delivery-state detector — ONE implementation, all consumers read it (customer card, the
// monitor checker's stuck event + R-71c self-heal, and any future surface). Derived from the two
// signals the hub already holds and previously read nowhere:
// - the one_time_secrets row (exists? consumed_at?) — the delivery ledger;
// - report offsite-object presence — the box's own testimony that an offbox target is applied
// (the report builder attaches `offsite` only when a target is configured).
//
// Origin: DIAG-f10-demo-hp-offsite-2026-07-23 — demo-hp sat 2 days in consumed_awaiting_apply
// (the burned-credential shape) while the operator card said "Provisioned" from static copy.
// DeliveryState is the customer's offsite last-mile state.
type DeliveryState string
const (
// DeliveryApplied — the latest report carries an offsite status object: the tier is live on
// the box. Wins over every secret-row shape (precedence rule; an applied box with a stale
// staged secret is APPLIED — see StaleStagedSince).
DeliveryApplied DeliveryState = "applied"
// DeliveryConsumedAwaitingApply — the one-time password was consumed but no report since has
// ever shown an offbox target: the burned-credential shape (normal for seconds, wrong for
// hours; the F10 box sat here for 2 days).
DeliveryConsumedAwaitingApply DeliveryState = "consumed_awaiting_apply"
// DeliveryStagedAwaitingConsume — a secret is staged and not yet consumed: delivery pending
// (normal for minutes while the box re-pulls config, wrong for days).
DeliveryStagedAwaitingConsume DeliveryState = "staged_awaiting_consume"
// DeliveryNoSecret — offsite is enabled in the config but no secret row exists at all: a
// mis-state (e.g. a RESET purged the row without deprovisioning). Rendered needs-attention.
DeliveryNoSecret DeliveryState = "no_secret"
)
// DeliveryStatus is the derived last-mile state plus the timestamps every consumer must surface
// ("mióta" — R-70's whole point is that nothing about this plumbing stays invisible).
type DeliveryStatus struct {
State DeliveryState
// Since anchors the state's age: consumed_at for consumed_awaiting_apply, created_at for
// staged_awaiting_consume, the latest report time for applied, zero for no_secret.
Since time.Time
// StaleStagedSince is non-zero ONLY in the applied+stale-staged edge (the demo-felhom shape):
// the box is applied via an earlier generation while an unconsumed secret sits staged. The
// caller renders an info line; the self-heal must NEVER touch this shape (R-39(a)).
StaleStagedSince time.Time
// ReportsSinceConsume / OffsiteReportsSinceConsume count reports received after consumed_at
// (consumed_awaiting_apply only; zero otherwise). The R-71c trigger requires
// ReportsSinceConsume >= N with OffsiteReportsSinceConsume == 0 — "consecutive without".
ReportsSinceConsume int
OffsiteReportsSinceConsume int
}
// DeliveryStateFor derives the customer's offsite delivery state. Callers gate on the descriptor
// (offsite enabled) themselves — this function only reads the ledger and the reports; age math is
// the caller's (against its own clock seam).
func DeliveryStateFor(st *store.Store, customerID string) (DeliveryStatus, error) {
found, receivedAt, hasOffsite, err := st.LatestReportOffsitePresence(customerID)
if err != nil {
return DeliveryStatus{}, err
}
secret, err := st.GetOneTimeSecretInfo(customerID)
if err != nil {
return DeliveryStatus{}, err
}
// Precedence: applied wins. The box's own report is the strongest evidence there is.
if found && hasOffsite {
status := DeliveryStatus{State: DeliveryApplied, Since: receivedAt}
if secret != nil && secret.ConsumedAt.IsZero() {
status.StaleStagedSince = secret.CreatedAt // demo-felhom shape: applied + stale staged
}
return status, nil
}
if secret == nil {
return DeliveryStatus{State: DeliveryNoSecret}, nil
}
if !secret.ConsumedAt.IsZero() {
total, withOffsite, err := st.CountReportsOffsiteSince(customerID, secret.ConsumedAt)
if err != nil {
return DeliveryStatus{}, err
}
return DeliveryStatus{
State: DeliveryConsumedAwaitingApply,
Since: secret.ConsumedAt,
ReportsSinceConsume: total,
OffsiteReportsSinceConsume: withOffsite,
}, nil
}
return DeliveryStatus{State: DeliveryStagedAwaitingConsume, Since: secret.CreatedAt}, nil
}