Improve dashboard health and state recovery

This commit is contained in:
Mikei386
2026-07-13 21:06:09 +02:00
parent 819055cbf0
commit 8f75a04db6
15 changed files with 313 additions and 38 deletions
+41 -7
View File
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"sync"
"time"
"git.casaderoll.de/michael/urbm/internal/model"
)
@@ -51,12 +52,20 @@ func (s *Store) SaveConfig(c model.Config) error {
}
func (s *Store) LoadRuns() ([]model.Run, error) {
s.mu.RLock()
defer s.mu.RUnlock()
s.mu.Lock()
defer s.mu.Unlock()
var runs []model.Run
if err := readJSON(s.runsPath(), &runs); errors.Is(err, os.ErrNotExist) {
return []model.Run{}, nil
} else if err != nil {
var decodeErr *jsonDecodeError
if errors.As(err, &decodeErr) {
quarantined, quarantineErr := quarantineFile(s.runsPath())
if quarantineErr != nil {
return nil, fmt.Errorf("%w; quarantine failed: %v", err, quarantineErr)
}
return nil, fmt.Errorf("%w; corrupt file moved to %s", err, quarantined)
}
return nil, err
}
return runs, nil
@@ -72,8 +81,8 @@ func (s *Store) SaveRuns(runs []model.Run) error {
}
func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
s.mu.Lock()
defer s.mu.Unlock()
path := s.backupMetricsPath()
b, err := os.ReadFile(path)
legacySource := false
@@ -95,7 +104,7 @@ func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
if b[0] == '[' {
var metrics []model.BackupMetric
if err := json.Unmarshal(b, &metrics); err != nil {
return nil, true, fmt.Errorf("decode %s: %w", path, err)
return nil, true, quarantineDecodeError(path, err)
}
return metrics, true, nil
}
@@ -108,7 +117,7 @@ func (s *Store) LoadBackupMetrics() ([]model.BackupMetric, bool, error) {
}
var metric model.BackupMetric
if err := json.Unmarshal(line, &metric); err != nil {
return nil, legacySource, fmt.Errorf("decode %s line %d: %w", path, index+1, err)
return nil, legacySource, quarantineDecodeError(path, fmt.Errorf("line %d: %w", index+1, err))
}
metrics = append(metrics, metric)
}
@@ -169,11 +178,36 @@ func readJSON(path string, target any) error {
return err
}
if err := json.Unmarshal(b, target); err != nil {
return fmt.Errorf("decode %s: %w", path, err)
return &jsonDecodeError{path: path, err: err}
}
return nil
}
type jsonDecodeError struct {
path string
err error
}
func (e *jsonDecodeError) Error() string { return fmt.Sprintf("decode %s: %v", e.path, e.err) }
func (e *jsonDecodeError) Unwrap() error { return e.err }
func quarantineDecodeError(path string, decodeErr error) error {
quarantined, err := quarantineFile(path)
wrapped := &jsonDecodeError{path: path, err: decodeErr}
if err != nil {
return fmt.Errorf("%w; quarantine failed: %v", wrapped, err)
}
return fmt.Errorf("%w; corrupt file moved to %s", wrapped, quarantined)
}
func quarantineFile(path string) (string, error) {
quarantined := path + ".corrupt-" + time.Now().UTC().Format("20060102-150405.000000000")
if err := os.Rename(path, quarantined); err != nil {
return "", err
}
return quarantined, nil
}
func writeJSONAtomic(path string, value any, mode os.FileMode) error {
b, err := json.MarshalIndent(value, "", " ")
if err != nil {