refactor(models): split Site into SiteConfig + SiteState
Site now embeds SiteConfig (22 persistent fields) and SiteState (11 ephemeral runtime fields). Field access unchanged via promotion — site.Name and site.Status still work. Store layer deals exclusively in SiteConfig — the DB never sees runtime state. Engine's liveState keeps full Site composites. UpdateSiteConfig reduced from 11-line field-by-field copy to `existing.SiteConfig = cfg`. RunCheck takes SiteConfig (only needs config fields). Checker is now statically prevented from reading/writing runtime state. Backup.Sites changed to []SiteConfig — exports no longer carry zero-valued runtime fields. Import backward-compatible (json ignores unknown fields).
This commit was merged in pull request #109.
This commit is contained in:
+13
-13
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
func TestSortSitesForDisplay_GroupsFirst(t *testing.T) {
|
||||
sites := []models.Site{
|
||||
{ID: 3, Name: "ungrouped", Type: "http", Status: "UP"},
|
||||
{ID: 1, Name: "group-a", Type: "group", Status: "UP"},
|
||||
{ID: 2, Name: "child", Type: "http", Status: "UP", ParentID: 1},
|
||||
{SiteConfig: models.SiteConfig{ID: 3, Name: "ungrouped", Type: "http"}, SiteState: models.SiteState{Status: "UP"}},
|
||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "group-a", Type: "group"}, SiteState: models.SiteState{Status: "UP"}},
|
||||
{SiteConfig: models.SiteConfig{ID: 2, Name: "child", Type: "http", ParentID: 1}, SiteState: models.SiteState{Status: "UP"}},
|
||||
}
|
||||
result := sortSitesForDisplay(sites, nil)
|
||||
if len(result) != 3 {
|
||||
@@ -29,9 +29,9 @@ func TestSortSitesForDisplay_GroupsFirst(t *testing.T) {
|
||||
|
||||
func TestSortSitesForDisplay_CollapsedHidesChildren(t *testing.T) {
|
||||
sites := []models.Site{
|
||||
{ID: 1, Name: "group-a", Type: "group", Status: "UP"},
|
||||
{ID: 2, Name: "child-1", Type: "http", Status: "UP", ParentID: 1},
|
||||
{ID: 3, Name: "child-2", Type: "http", Status: "UP", ParentID: 1},
|
||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "group-a", Type: "group"}, SiteState: models.SiteState{Status: "UP"}},
|
||||
{SiteConfig: models.SiteConfig{ID: 2, Name: "child-1", Type: "http", ParentID: 1}, SiteState: models.SiteState{Status: "UP"}},
|
||||
{SiteConfig: models.SiteConfig{ID: 3, Name: "child-2", Type: "http", ParentID: 1}, SiteState: models.SiteState{Status: "UP"}},
|
||||
}
|
||||
collapsed := map[int]bool{1: true}
|
||||
result := sortSitesForDisplay(sites, collapsed)
|
||||
@@ -45,9 +45,9 @@ func TestSortSitesForDisplay_CollapsedHidesChildren(t *testing.T) {
|
||||
|
||||
func TestSortSitesForDisplay_StatusOrdering(t *testing.T) {
|
||||
sites := []models.Site{
|
||||
{ID: 1, Name: "up-site", Type: "http", Status: "UP"},
|
||||
{ID: 2, Name: "down-site", Type: "http", Status: "DOWN"},
|
||||
{ID: 3, Name: "late-site", Type: "http", Status: "LATE"},
|
||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "up-site", Type: "http"}, SiteState: models.SiteState{Status: "UP"}},
|
||||
{SiteConfig: models.SiteConfig{ID: 2, Name: "down-site", Type: "http"}, SiteState: models.SiteState{Status: "DOWN"}},
|
||||
{SiteConfig: models.SiteConfig{ID: 3, Name: "late-site", Type: "http"}, SiteState: models.SiteState{Status: "LATE"}},
|
||||
}
|
||||
result := sortSitesForDisplay(sites, nil)
|
||||
if result[0].Status != "DOWN" {
|
||||
@@ -63,9 +63,9 @@ func TestSortSitesForDisplay_StatusOrdering(t *testing.T) {
|
||||
|
||||
func TestFilterSites(t *testing.T) {
|
||||
sites := []models.Site{
|
||||
{Name: "Production API"},
|
||||
{Name: "Staging API"},
|
||||
{Name: "Database"},
|
||||
{SiteConfig: models.SiteConfig{Name: "Production API"}},
|
||||
{SiteConfig: models.SiteConfig{Name: "Staging API"}},
|
||||
{SiteConfig: models.SiteConfig{Name: "Database"}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -87,7 +87,7 @@ func TestFilterSites(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFilterSites_EmptyNeedle(t *testing.T) {
|
||||
sites := []models.Site{{Name: "a"}, {Name: "b"}}
|
||||
sites := []models.Site{{SiteConfig: models.SiteConfig{Name: "a"}}, {SiteConfig: models.SiteConfig{Name: "b"}}}
|
||||
got := filterSites(sites, "")
|
||||
if len(got) != 2 {
|
||||
t.Errorf("empty needle should return all, got %d", len(got))
|
||||
|
||||
@@ -38,13 +38,13 @@ func TestSiteOrder(t *testing.T) {
|
||||
site models.Site
|
||||
want int
|
||||
}{
|
||||
{"down", models.Site{Status: "DOWN"}, 0},
|
||||
{"ssl exp", models.Site{Status: "SSL EXP"}, 0},
|
||||
{"late", models.Site{Status: "LATE"}, 1},
|
||||
{"up", models.Site{Status: "UP"}, 2},
|
||||
{"pending", models.Site{Status: "PENDING"}, 3},
|
||||
{"paused up", models.Site{Status: "UP", Paused: true}, 3},
|
||||
{"paused down", models.Site{Status: "DOWN", Paused: true}, 3},
|
||||
{"down", models.Site{SiteState: models.SiteState{Status: "DOWN"}}, 0},
|
||||
{"ssl exp", models.Site{SiteState: models.SiteState{Status: "SSL EXP"}}, 0},
|
||||
{"late", models.Site{SiteState: models.SiteState{Status: "LATE"}}, 1},
|
||||
{"up", models.Site{SiteState: models.SiteState{Status: "UP"}}, 2},
|
||||
{"pending", models.Site{SiteState: models.SiteState{Status: "PENDING"}}, 3},
|
||||
{"paused up", models.Site{SiteConfig: models.SiteConfig{Paused: true}, SiteState: models.SiteState{Status: "UP"}}, 3},
|
||||
{"paused down", models.Site{SiteConfig: models.SiteConfig{Paused: true}, SiteState: models.SiteState{Status: "DOWN"}}, 3},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := siteOrder(tt.site)
|
||||
|
||||
@@ -535,7 +535,7 @@ func (m *Model) submitSiteForm() tea.Cmd {
|
||||
threshold = 7
|
||||
}
|
||||
|
||||
site := models.Site{
|
||||
cfg := models.SiteConfig{
|
||||
ID: m.editID,
|
||||
Name: d.Name,
|
||||
URL: d.URL,
|
||||
@@ -559,11 +559,8 @@ func (m *Model) submitSiteForm() tea.Cmd {
|
||||
st := m.store
|
||||
m.state = stateDashboard
|
||||
if m.editID > 0 {
|
||||
// The engine's in-memory config updates immediately; the DB write
|
||||
// follows in the Cmd. New sites enter the engine via its poll loop
|
||||
// once the insert lands.
|
||||
m.engine.UpdateSiteConfig(site)
|
||||
return writeCmd("Update site", func() error { return st.UpdateSite(context.Background(), site) })
|
||||
m.engine.UpdateSiteConfig(cfg)
|
||||
return writeCmd("Update site", func() error { return st.UpdateSite(context.Background(), cfg) })
|
||||
}
|
||||
return writeCmd("Add site", func() error { return st.AddSite(context.Background(), site) })
|
||||
return writeCmd("Add site", func() error { return st.AddSite(context.Background(), cfg) })
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (*stubErr) Error() string { return "boom" }
|
||||
func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{ID: 1, Name: "site", Status: "DOWN"}}
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}, SiteState: models.SiteState{Status: "DOWN"}}}
|
||||
m.cursor = 0
|
||||
m.state = stateDetail
|
||||
m.termWidth = 120
|
||||
@@ -201,7 +201,7 @@ func TestHandleTabData_DropsStaleSeq(t *testing.T) {
|
||||
func TestHistoryKey_LoadsOffUIGoroutine(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{ID: 7, Name: "site"}}
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 7, Name: "site"}}}
|
||||
m.state = stateDetail
|
||||
m.termWidth, m.termHeight = 120, 40
|
||||
|
||||
@@ -240,7 +240,7 @@ func TestHistoryKey_LoadsOffUIGoroutine(t *testing.T) {
|
||||
func TestSLAData_DropsStaleReply(t *testing.T) {
|
||||
m := newTestModel(&tuiMockStore{})
|
||||
m.termWidth, m.termHeight = 120, 40
|
||||
m.sites = []models.Site{{ID: 3, Status: "UP"}}
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 3}, SiteState: models.SiteState{Status: "UP"}}}
|
||||
|
||||
if cmd := (&m).openSLAView(m.sites[0]); cmd == nil {
|
||||
t.Fatal("openSLAView should return a load Cmd")
|
||||
@@ -264,7 +264,7 @@ func TestSLAData_DropsStaleReply(t *testing.T) {
|
||||
func TestConfirmDelete_WritesOffUIGoroutine(t *testing.T) {
|
||||
ms := &tuiMockStore{}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{ID: 4, Name: "s"}}
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 4, Name: "s"}}}
|
||||
m.state = stateConfirmDelete
|
||||
m.deleteTab = 0
|
||||
m.deleteID = 4
|
||||
@@ -312,7 +312,7 @@ func TestWriteDoneMsg_LogsErrorAndReloads(t *testing.T) {
|
||||
func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
|
||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||
m := newTestModel(ms)
|
||||
m.sites = []models.Site{{ID: 5, Name: "site"}}
|
||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 5, Name: "site"}}}
|
||||
|
||||
m.state = stateDashboard
|
||||
if (&m).detailRefreshCmd() != nil {
|
||||
|
||||
Reference in New Issue
Block a user