Refactoring complete: v0.4.0 architectural milestone #1
+1
-1
@@ -277,7 +277,7 @@ Track progress here. Mark tasks complete as they land and pass review.
|
||||
- [x] T4.7 — Confirm app.go is gone; smoke test both platforms
|
||||
|
||||
### Phase 5 — Hardening & docs
|
||||
- [ ] T5.1 — Surface errors from service + storage
|
||||
- [x] T5.1 — Surface errors from service + storage
|
||||
- [ ] T5.2 — Introduce `autostart.Manager` interface
|
||||
- [ ] T5.3 — Fill test gaps (folder filtering, cleanup, migration, concurrency)
|
||||
- [ ] T5.4 — Run `go test -race ./...` clean on both platforms
|
||||
|
||||
@@ -41,9 +41,18 @@ type SchedulerStateChanged struct {
|
||||
Paused bool
|
||||
}
|
||||
|
||||
// ErrorOccurred signals a background error that could not be returned to a
|
||||
// caller — typically a failed save or cleanup after an async run. The UI
|
||||
// surfaces it in the History tab so the user is not silently left with
|
||||
// un-persisted state.
|
||||
type ErrorOccurred struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (JobChanged) isEvent() {}
|
||||
func (RunRecorded) isEvent() {}
|
||||
func (SchedulerStateChanged) isEvent() {}
|
||||
func (ErrorOccurred) isEvent() {}
|
||||
|
||||
// Observer receives events emitted by the Service. OnEvent is the single
|
||||
// reaction point; the UI implements it and marshals any widget work onto the
|
||||
|
||||
+14
-7
@@ -211,6 +211,7 @@ func (s *Service) RunNow(id int) error {
|
||||
func (s *Service) RunDue(now time.Time) {
|
||||
s.mu.Lock()
|
||||
var startedID int
|
||||
var startErr error
|
||||
if !s.paused {
|
||||
for index := range s.jobs {
|
||||
job := &s.jobs[index]
|
||||
@@ -221,15 +222,16 @@ func (s *Service) RunDue(now time.Time) {
|
||||
if runtime.LastState == "Running" {
|
||||
continue
|
||||
}
|
||||
// Async save errors cannot be returned to a caller here; surfacing them
|
||||
// is deferred to T5.1 with the rest of the swallowed saves.
|
||||
_ = s.startRunLocked(job, runtime, "Schedule")
|
||||
startErr = s.startRunLocked(job, runtime, "Schedule")
|
||||
startedID = job.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if startErr != nil {
|
||||
s.emit(ErrorOccurred{Err: fmt.Errorf("save jobs before scheduled run: %w", startErr)})
|
||||
}
|
||||
if startedID != 0 {
|
||||
s.emit(JobChanged{JobID: startedID})
|
||||
}
|
||||
@@ -286,6 +288,7 @@ func (s *Service) executeRun(ctx context.Context, jobCopy domain.Job, trigger st
|
||||
record := s.runJob(ctx, &jobCopy, trigger, s.store.Paths.LogsDir)
|
||||
|
||||
s.mu.Lock()
|
||||
var cleanupErr, saveErr error
|
||||
if current := s.findByIDLocked(jobCopy.ID); current != nil {
|
||||
runtime := s.runtimeForLocked(current)
|
||||
runtime.LastRun = record.Time
|
||||
@@ -293,13 +296,17 @@ func (s *Service) executeRun(ctx context.Context, jobCopy domain.Job, trigger st
|
||||
runtime.Output = record.Output
|
||||
prependLog(runtime, record)
|
||||
s.refreshNextRunLocked(current, runtime)
|
||||
// Async save errors cannot be returned to a caller; surfacing them is
|
||||
// deferred to T5.1 along with the rest of the swallowed saves.
|
||||
_ = runner.CleanupLogs(s.store.Paths.LogsDir, s.store.Config.MaxLogFiles, s.store.Config.MaxLogAgeDays)
|
||||
_ = s.store.SaveJobs(s.jobs)
|
||||
cleanupErr = runner.CleanupLogs(s.store.Paths.LogsDir, s.store.Config.MaxLogFiles, s.store.Config.MaxLogAgeDays)
|
||||
saveErr = s.store.SaveJobs(s.jobs)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if cleanupErr != nil {
|
||||
s.emit(ErrorOccurred{Err: fmt.Errorf("log cleanup after run %q: %w", jobCopy.Name, cleanupErr)})
|
||||
}
|
||||
if saveErr != nil {
|
||||
s.emit(ErrorOccurred{Err: fmt.Errorf("save jobs after run %q: %w", jobCopy.Name, saveErr)})
|
||||
}
|
||||
s.emit(RunRecorded{Record: record})
|
||||
s.emit(JobChanged{JobID: jobCopy.ID})
|
||||
}
|
||||
|
||||
@@ -68,10 +68,14 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
||||
// the UI thread. This is the sole place events touch widgets. (Resolves #4.)
|
||||
svc.Subscribe(app.ObserverFunc(func(ev app.Event) {
|
||||
recorded, isRecorded := ev.(app.RunRecorded)
|
||||
errOccurred, isError := ev.(app.ErrorOccurred)
|
||||
fyne.Do(func() {
|
||||
if isRecorded {
|
||||
events = append(events, recorded.Record)
|
||||
}
|
||||
if isError {
|
||||
events = append(events, newEvent(0, "Service", "Error", errOccurred.Err.Error()))
|
||||
}
|
||||
refresh()
|
||||
})
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user