This commit is contained in:
ponzischeme89
2026-08-22 11:32:50 +12:00
parent b131932a4e
commit f982d391b9
40 changed files with 25594 additions and 9575 deletions
+3
View File
@@ -366,6 +366,9 @@ func TestServiceStatusCarriesEmbyHealthAndPreferenceRevision(t *testing.T) {
if !ok || len(metadataOrder) == 0 {
t.Fatalf("status response carried no global metadata hero order: %v", body)
}
if body["metadataHeroTimeRemainingColour"] != store.MetadataHeroTimeRemainingColourGreen {
t.Fatalf("status response carried no global time-remaining colour: %v", body)
}
// Likewise present with no store behind it. This is the whole delivery channel for an
// operator's hero change: a television comparing against a missing field would go on
// drawing yesterday's hero until it was next restarted.
+3 -3
View File
@@ -42,7 +42,7 @@ type embyHealthState struct {
retryEvery time.Duration
consecutive int
// version is what Emby last said it was. Kept across a failed probe on purpose: the
// About page reads it, and blanking it during an outage would replace a fact that is
// The Settings rail reads it, and blanking it during an outage would replace a fact that is
// still true with nothing.
version string
}
@@ -65,7 +65,7 @@ func (h *embyHealth) begin(interval time.Duration, now time.Time) {
// retune follows the operator changing the probe's cadence, or switching it off, without
// disturbing what the probe has already found. It is deliberately not `begin`: that
// starts a fresh watch and drops the Emby version with it, which the About page reads and
// starts a fresh watch and drops the Emby version with it, which the Settings rail reads and
// which is still true whatever the interval is now.
func (h *embyHealth) retune(interval time.Duration, now time.Time) {
h.mu.Lock()
@@ -119,7 +119,7 @@ type embyHealthPayload struct {
Since string `json:"since,omitempty"`
CheckedAt string `json:"checkedAt,omitempty"`
RetrySeconds int `json:"retrySeconds"`
// Version is Emby's own, for the television's About page. Omitted rather than sent
// Version is Emby's own, for the television's Settings rail. Omitted rather than sent
// empty, so a client can tell "not known yet" from "known to be blank" — the first
// probe may not have completed, and with the probe switched off none ever will.
Version string `json:"version,omitempty"`
+1 -1
View File
@@ -94,7 +94,7 @@ func TestEmbyHealthPayloadCarriesTheRetryInterval(t *testing.T) {
}
}
// The About page prints Emby's version beside the gateway's, so it has to survive the
// The Settings rail prints Emby's version beside the gateway's, so it has to survive the
// outage that is exactly when somebody goes looking at that page. A probe that fails
// carries no version, and blanking the last known one would replace a fact that is still
// true with nothing.
+6 -4
View File
@@ -130,6 +130,7 @@ func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, ses
}
compatible, compatibilityMessage := compatibilityFor(r)
featurePolicy := s.currentFeaturePolicy(r.Context())
metadataHero := s.metadataHeroSettings.get()
writeJSON(w, http.StatusOK, map[string]any{
"maintenance": state.Enabled,
"quietTime": quiet.Active,
@@ -153,10 +154,11 @@ func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, ses
// fetches /v1/preferences when they differ. That is what turns this poll into the
// delivery channel for an operator pushing someone's settings.
"preferencesRevision": s.preferenceRevisionFor(r, sess),
// Four small ids are the complete household-wide metadata hero composition. They
// ride the existing poll so a saved layout reaches an open launcher immediately,
// without manufacturing a per-user preference revision for a global change.
"metadataHeroContentOrder": s.metadataHeroSettings.get().ContentOrder,
// The small metadata hero presentation document rides the existing poll so a saved
// layout or colour reaches an open launcher immediately, without manufacturing a
// per-user preference revision for a global change.
"metadataHeroContentOrder": metadataHero.ContentOrder,
"metadataHeroTimeRemainingColour": metadataHero.TimeRemainingColour,
// The theme, as an id and a revision rather than the palette itself — the
// preferencesRevision precedent, for the same reason. The set refetches /v1/theme
// only when one of these moves, which is what makes a season arriving at midnight
@@ -86,12 +86,13 @@ func (s *Server) handleAdminMetadataHeroSettings(w http.ResponseWriter, r *http.
return
}
s.metadataHeroSettings.set(stored)
if !slices.Equal(previous.ContentOrder, stored.ContentOrder) {
if !slices.Equal(previous.ContentOrder, stored.ContentOrder) ||
previous.TimeRemainingColour != stored.TimeRemainingColour {
s.publishAdmin(r.Context(), adminevents.Event{
Type: adminevents.TypeSettingsChanged,
Severity: adminevents.SeverityInfo,
Title: "Metadata hero changed",
Summary: "The household-wide metadata hero content order was changed",
Summary: "The household-wide metadata hero presentation was changed",
Actor: operator,
Link: "/admin/metadata-hero",
})
@@ -9,17 +9,27 @@ import (
func TestMetadataHeroSettingsStateHasAUsableGlobalDefault(t *testing.T) {
var state metadataHeroSettingsState
got := state.get().ContentOrder
settings := state.get()
got := settings.ContentOrder
if !reflect.DeepEqual(got, store.DefaultMetadataHeroContentOrder) {
t.Fatalf("content order = %v, want %v", got, store.DefaultMetadataHeroContentOrder)
}
if settings.TimeRemainingColour != store.MetadataHeroTimeRemainingColourGreen {
t.Fatalf("time remaining colour = %q, want green", settings.TimeRemainingColour)
}
}
func TestMetadataHeroSettingsStateNormalisesEveryWrite(t *testing.T) {
var state metadataHeroSettingsState
state.set(store.MetadataHeroSettings{ContentOrder: []string{"summary", "unknown", "summary", "title"}})
state.set(store.MetadataHeroSettings{
ContentOrder: []string{"summary", "unknown", "summary", "title"},
TimeRemainingColour: " WHITE ",
})
want := []string{"summary", "title"}
if got := state.get().ContentOrder; !reflect.DeepEqual(got, want) {
t.Fatalf("content order = %v, want %v", got, want)
}
if got := state.get().TimeRemainingColour; got != store.MetadataHeroTimeRemainingColourWhite {
t.Fatalf("time remaining colour = %q, want white", got)
}
}
+17 -9
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
@@ -15,12 +16,14 @@ import (
const MetadataHeroSettingsKey = "metadata_hero_settings"
const (
MetadataHeroTitle = "title"
MetadataHeroRatings = "ratings"
MetadataHeroFacts = "facts"
MetadataHeroSummary = "summary"
MetadataHeroReason = "recommendation_reason"
MetadataHeroTime = "time_remaining"
MetadataHeroTitle = "title"
MetadataHeroRatings = "ratings"
MetadataHeroFacts = "facts"
MetadataHeroSummary = "summary"
MetadataHeroReason = "recommendation_reason"
MetadataHeroTime = "time_remaining"
MetadataHeroTimeRemainingColourGreen = "green"
MetadataHeroTimeRemainingColourWhite = "white"
)
var DefaultMetadataHeroContentOrder = []string{
@@ -43,9 +46,10 @@ var MetadataHeroContentOptions = []string{
// outside user preferences because the hero is part of the product's shared composition,
// not something that should change when the viewer changes.
type MetadataHeroSettings struct {
ContentOrder []string `json:"contentOrder"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedBy string `json:"updatedBy,omitempty"`
ContentOrder []string `json:"contentOrder"`
TimeRemainingColour string `json:"timeRemainingColour"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedBy string `json:"updatedBy,omitempty"`
}
func NormalizeMetadataHeroSettings(settings MetadataHeroSettings) MetadataHeroSettings {
@@ -62,6 +66,10 @@ func NormalizeMetadataHeroSettings(settings MetadataHeroSettings) MetadataHeroSe
order = append([]string(nil), DefaultMetadataHeroContentOrder...)
}
settings.ContentOrder = order
settings.TimeRemainingColour = strings.ToLower(strings.TrimSpace(settings.TimeRemainingColour))
if settings.TimeRemainingColour != MetadataHeroTimeRemainingColourWhite {
settings.TimeRemainingColour = MetadataHeroTimeRemainingColourGreen
}
return settings
}
@@ -35,3 +35,15 @@ func TestNormalizeMetadataHeroSettingsAcceptsOptionalContent(t *testing.T) {
t.Fatalf("content order = %v, want %v", got, want)
}
}
func TestNormalizeMetadataHeroSettingsNormalisesTimeRemainingColour(t *testing.T) {
if got := NormalizeMetadataHeroSettings(MetadataHeroSettings{}).TimeRemainingColour; got != MetadataHeroTimeRemainingColourGreen {
t.Fatalf("default time remaining colour = %q, want green", got)
}
if got := NormalizeMetadataHeroSettings(MetadataHeroSettings{TimeRemainingColour: " WHITE "}).TimeRemainingColour; got != MetadataHeroTimeRemainingColourWhite {
t.Fatalf("time remaining colour = %q, want white", got)
}
if got := NormalizeMetadataHeroSettings(MetadataHeroSettings{TimeRemainingColour: "orange"}).TimeRemainingColour; got != MetadataHeroTimeRemainingColourGreen {
t.Fatalf("unknown time remaining colour = %q, want green", got)
}
}