fix: make LoadConfig read-only to prevent panic on read-only filesystems
LoadConfig() tried to create directories and write opal.yml as a side effect of loading config. On the server (where /etc/opal is in systemd ReadOnlyPaths), this failed, returning nil. All internal GetConfig() callers discarded the error, passing nil to BuildUrgencyCoefficients() which panicked on nil dereference. Redesign the config system with layered, read-only loading: - Defaults (always present) → YAML file (if exists) → OPAL_ env vars - LoadConfig never writes to the filesystem or returns nil - File creation moved to explicit InitConfig() for CLI first-run/setup - SaveConfig uses yaml.Marshal instead of manual field-by-field Viper calls, eliminating the three-place maintenance burden Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-6
@@ -244,16 +244,20 @@ func initializeApp() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load config
|
||||
// On first run, create the config file with defaults
|
||||
if isFirstRun {
|
||||
if err := engine.InitConfig(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
showFirstRunMessage()
|
||||
}
|
||||
|
||||
// Load config (reads file if present, otherwise uses defaults)
|
||||
if _, err := engine.LoadConfig(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Show first-run message after config is created
|
||||
if isFirstRun {
|
||||
showFirstRunMessage()
|
||||
}
|
||||
}
|
||||
|
||||
func showFirstRunMessage() {
|
||||
|
||||
+11
-57
@@ -121,33 +121,6 @@ func runQuickSetup() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create config
|
||||
cfg := engine.Config{
|
||||
DefaultFilter: "status:pending",
|
||||
DefaultSort: "due,priority",
|
||||
DefaultReport: "list",
|
||||
ColorOutput: true,
|
||||
WeekStartDay: "monday",
|
||||
DefaultDueTime: "",
|
||||
NextLimit: 5,
|
||||
SyncEnabled: false,
|
||||
SyncStrategy: "last-write-wins",
|
||||
SyncQueueOffline: true,
|
||||
UrgencyDue: 12.0,
|
||||
UrgencyPriorityH: 6.0,
|
||||
UrgencyPriorityM: 3.9,
|
||||
UrgencyPriorityD: 1.8,
|
||||
UrgencyPriorityL: 0.0,
|
||||
UrgencyActive: 4.0,
|
||||
UrgencyAge: 2.0,
|
||||
UrgencyAgeMax: 365,
|
||||
UrgencyTags: 1.0,
|
||||
UrgencyProject: 1.0,
|
||||
UrgencyWaiting: -3.0,
|
||||
UrgencyUrgentTag: "next",
|
||||
UrgencyUrgentCoeff: 15.0,
|
||||
}
|
||||
|
||||
// Create directories
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
wizard.PrintError(fmt.Sprintf("Failed to create config directory: %v", err))
|
||||
@@ -158,8 +131,8 @@ func runQuickSetup() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Save config
|
||||
if err := engine.SaveConfig(&cfg); err != nil {
|
||||
// Save default config
|
||||
if err := engine.SaveConfig(engine.DefaultConfig()); err != nil {
|
||||
wizard.PrintError(fmt.Sprintf("Failed to save config: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -304,34 +277,15 @@ func runInteractiveSetup() {
|
||||
return
|
||||
}
|
||||
|
||||
// Create configuration
|
||||
cfg := &engine.Config{
|
||||
DefaultFilter: defaultFilter,
|
||||
DefaultSort: "due,priority",
|
||||
DefaultReport: reportNames[defaultReport],
|
||||
ColorOutput: colorOutput,
|
||||
WeekStartDay: weekStartDay,
|
||||
DefaultDueTime: "",
|
||||
NextLimit: 5,
|
||||
SyncEnabled: syncEnabled,
|
||||
SyncURL: syncURL,
|
||||
SyncAPIKey: syncAPIKey,
|
||||
SyncStrategy: "last-write-wins",
|
||||
SyncQueueOffline: true,
|
||||
UrgencyDue: 12.0,
|
||||
UrgencyPriorityH: 6.0,
|
||||
UrgencyPriorityM: 3.9,
|
||||
UrgencyPriorityD: 1.8,
|
||||
UrgencyPriorityL: 0.0,
|
||||
UrgencyActive: 4.0,
|
||||
UrgencyAge: 2.0,
|
||||
UrgencyAgeMax: 365,
|
||||
UrgencyTags: 1.0,
|
||||
UrgencyProject: 1.0,
|
||||
UrgencyWaiting: -3.0,
|
||||
UrgencyUrgentTag: "next",
|
||||
UrgencyUrgentCoeff: 15.0,
|
||||
}
|
||||
// Create configuration from defaults, then apply user choices
|
||||
cfg := engine.DefaultConfig()
|
||||
cfg.DefaultFilter = defaultFilter
|
||||
cfg.DefaultReport = reportNames[defaultReport]
|
||||
cfg.ColorOutput = colorOutput
|
||||
cfg.WeekStartDay = weekStartDay
|
||||
cfg.SyncEnabled = syncEnabled
|
||||
cfg.SyncURL = syncURL
|
||||
cfg.SyncAPIKey = syncAPIKey
|
||||
|
||||
// Set directory overrides
|
||||
engine.SetConfigDirOverride(configDir)
|
||||
|
||||
Reference in New Issue
Block a user