v0.22.0: First-run setup wizard, local infra backup, hub verification
New controller features:
- Web-based setup wizard replaces docker-setup.sh interactive config
- Dual listener: :8080 (Traefik) + :8081 (direct HTTP for LAN)
- Drive scanner finds .felhom-infra-backup/ on all block devices
- Hub recovery pull (GET /api/v1/recovery/{id}) with retrieval password
- Fresh install: Hub config download or manual wizard
- CSRF protection, state persistence, Hungarian UI
- Local infra backup written to all connected drives after each backup cycle
- .felhom-infra-backup/backup.json + metadata.json with SHA256 checksum
- Hub verification: parse customer_blocked from report push response
- Limited mode after 7 days without verification
- Recovery info page on Settings + recovery-info.txt file generation
- Pending events queue: DR events sent to Hub on next report push
- docker-setup.sh v6.0.0: removed interactive wizard, minimal controller.yaml only
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,17 @@ type Settings struct {
|
||||
|
||||
// Cross-drive restic repo password (auto-generated on first use)
|
||||
CrossDriveResticPassword string `json:"cross_drive_restic_password,omitempty"`
|
||||
|
||||
// Hub verification state
|
||||
HubVerified bool `json:"hub_verified,omitempty"`
|
||||
HubVerifiedAt string `json:"hub_verified_at,omitempty"` // RFC3339
|
||||
HubLastCheck string `json:"hub_last_check,omitempty"` // RFC3339
|
||||
|
||||
// Recovery credentials (saved from setup wizard input)
|
||||
RetrievalPassword string `json:"retrieval_password,omitempty"`
|
||||
|
||||
// Pending events (queued for next Hub push)
|
||||
PendingEvents []PendingEvent `json:"pending_events,omitempty"`
|
||||
}
|
||||
|
||||
// AppBackupPrefs holds per-app backup toggle state.
|
||||
@@ -96,6 +107,15 @@ var DefaultEnabledEvents = []string{
|
||||
"expected_dbdump_missed",
|
||||
}
|
||||
|
||||
// PendingEvent is an event queued for the next Hub push cycle.
|
||||
type PendingEvent struct {
|
||||
EventType string `json:"event_type"`
|
||||
Severity string `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details"` // JSON string
|
||||
CreatedAt string `json:"created_at"` // RFC3339
|
||||
}
|
||||
|
||||
// DBValidationCache holds cached DB dump validation results.
|
||||
type DBValidationCache struct {
|
||||
ValidatedAt string `json:"validated_at"` // RFC3339
|
||||
@@ -672,3 +692,102 @@ func (s *Settings) GetDecommissionedPaths() []StoragePath {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Hub Verification ---
|
||||
|
||||
// GetHubVerified returns the hub verification state.
|
||||
func (s *Settings) GetHubVerified() (verified bool, verifiedAt string) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.HubVerified, s.HubVerifiedAt
|
||||
}
|
||||
|
||||
// SetHubVerified updates the hub verification state and saves to disk.
|
||||
func (s *Settings) SetHubVerified(verified bool, at time.Time) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.HubVerified = verified
|
||||
s.HubVerifiedAt = at.UTC().Format(time.RFC3339)
|
||||
s.HubLastCheck = at.UTC().Format(time.RFC3339)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// SetHubLastCheck updates the last Hub check timestamp without changing verification status.
|
||||
func (s *Settings) SetHubLastCheck(at time.Time) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.HubLastCheck = at.UTC().Format(time.RFC3339)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// IsLimitedMode returns true if the controller should operate in limited mode
|
||||
// (new deployments blocked). This happens when:
|
||||
// - Never verified AND >7 days since controller started, OR
|
||||
// - Hub explicitly set customer as blocked (HubVerified=false after a successful check)
|
||||
func (s *Settings) IsLimitedMode() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.HubVerified {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we have a last check timestamp and it says not verified, limited mode
|
||||
if s.HubLastCheck != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Never checked yet — check if grace period (7 days) expired
|
||||
if s.HubVerifiedAt == "" {
|
||||
// No verification timestamp at all — not yet in limited mode (grace period from startup)
|
||||
return false
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, s.HubVerifiedAt)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return time.Since(t) > 7*24*time.Hour
|
||||
}
|
||||
|
||||
// --- Retrieval Password ---
|
||||
|
||||
// GetRetrievalPassword returns the stored retrieval password (thread-safe).
|
||||
func (s *Settings) GetRetrievalPassword() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.RetrievalPassword
|
||||
}
|
||||
|
||||
// SetRetrievalPassword updates the retrieval password and saves to disk.
|
||||
func (s *Settings) SetRetrievalPassword(password string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.RetrievalPassword = password
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// --- Pending Events ---
|
||||
|
||||
// AddPendingEvent queues an event for the next Hub push cycle.
|
||||
func (s *Settings) AddPendingEvent(event PendingEvent) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.PendingEvents = append(s.PendingEvents, event)
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// DrainPendingEvents returns and clears all pending events (thread-safe).
|
||||
func (s *Settings) DrainPendingEvents() []PendingEvent {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.PendingEvents) == 0 {
|
||||
return nil
|
||||
}
|
||||
events := make([]PendingEvent, len(s.PendingEvents))
|
||||
copy(events, s.PendingEvents)
|
||||
s.PendingEvents = nil
|
||||
if err := s.save(); err != nil {
|
||||
s.log.Printf("[ERROR] Failed to save after draining pending events: %v", err)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user