0.2.45 - Advanced analytics, logout old versions

This commit is contained in:
ponzischeme89
2026-08-10 20:24:22 +12:00
parent 63f0768507
commit 56c1167382
32 changed files with 1125 additions and 452 deletions
+50 -3
View File
@@ -17,6 +17,12 @@ import (
// speaks, next to the build's own version.
const ProtocolVersion = 1
// forcedUpdateFloor retires builds whose update behaviour is no longer reliable enough
// to leave optional. The floor only takes effect once an enabled policy points at this
// version (or a newer one) and carries a download URL, so deploying the gateway before
// publishing the APK cannot lock televisions out.
const forcedUpdateFloor = "0.2.44"
// updatePolicyCache keeps the policy in memory. It is read on every home request, and a
// database round trip per home load to answer "nothing to say" would be wasteful.
type updatePolicyCache struct {
@@ -90,13 +96,54 @@ func compatibilityFor(r *http.Request) (bool, string) {
return true, ""
}
// effectiveUpdatePolicy applies the server-owned emergency floor without weakening a
// higher minimum the operator has already selected.
func effectiveUpdatePolicy(policy appupdate.Policy) appupdate.Policy {
if !policy.Enabled || strings.TrimSpace(policy.DownloadURL) == "" ||
appupdate.CompareVersions(policy.LatestVersion, forcedUpdateFloor) < 0 {
return policy
}
if strings.TrimSpace(policy.MinimumVersion) == "" ||
appupdate.CompareVersions(policy.MinimumVersion, forcedUpdateFloor) < 0 {
policy.MinimumVersion = forcedUpdateFloor
}
return policy
}
func (s *Server) updateDecision(r *http.Request) appupdate.Decision {
return appupdate.Decide(effectiveUpdatePolicy(s.updatePolicy.get()), clientVersion(r))
}
// mustRetireForUpdate is narrower than "mandatory": an operator may temporarily force a
// newer release without wanting every otherwise supported session destroyed. Only builds
// below the permanent compatibility floor are signed out.
func mustRetireForUpdate(decision appupdate.Decision, version string) bool {
return decision.Status == appupdate.StatusMandatory && decision.DownloadURL != "" &&
appupdate.CompareVersions(version, forcedUpdateFloor) < 0
}
// requireSupportedClient prevents a retired build from signing straight back in after
// the authenticated gate has removed its old session. Its public update check remains
// available and will keep returning the actionable mandatory verdict.
func (s *Server) requireSupportedClient(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
decision := s.updateDecision(r)
if mustRetireForUpdate(decision, clientVersion(r)) {
w.Header().Set("X-Memby-Update-Required", decision.Version)
writeJSON(w, http.StatusUpgradeRequired, decision)
return
}
next(w, r)
}
}
// handleUpdate answers the client's version check.
//
// Its own public endpoint rather than a field on /v1/home: update policy belongs to the
// app build, not a viewer or login. The only client input is its build-version header and
// the answer comes from memory, so checking it never reads or mutates a user session.
// app build, not a viewer or login. The verdict comes from memory; when a bearer token is
// present the route resolves it only to attribute an offered update to the affected viewer.
func (s *Server) handleUpdate(w http.ResponseWriter, r *http.Request) {
decision := appupdate.Decide(s.updatePolicy.get(), clientVersion(r))
decision := s.updateDecision(r)
// Only a verdict that asks a television to do something is worth a line. Every TV
// checks on every launch, and "nothing to say" logged each time would bury the
// launch where an update was actually offered — or forced.