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
|
- [x] T4.7 — Confirm app.go is gone; smoke test both platforms
|
||||||
|
|
||||||
### Phase 5 — Hardening & docs
|
### 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.2 — Introduce `autostart.Manager` interface
|
||||||
- [ ] T5.3 — Fill test gaps (folder filtering, cleanup, migration, concurrency)
|
- [ ] T5.3 — Fill test gaps (folder filtering, cleanup, migration, concurrency)
|
||||||
- [ ] T5.4 — Run `go test -race ./...` clean on both platforms
|
- [ ] T5.4 — Run `go test -race ./...` clean on both platforms
|
||||||
|
|||||||
@@ -41,9 +41,18 @@ type SchedulerStateChanged struct {
|
|||||||
Paused bool
|
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 (JobChanged) isEvent() {}
|
||||||
func (RunRecorded) isEvent() {}
|
func (RunRecorded) isEvent() {}
|
||||||
func (SchedulerStateChanged) isEvent() {}
|
func (SchedulerStateChanged) isEvent() {}
|
||||||
|
func (ErrorOccurred) isEvent() {}
|
||||||
|
|
||||||
// Observer receives events emitted by the Service. OnEvent is the single
|
// Observer receives events emitted by the Service. OnEvent is the single
|
||||||
// reaction point; the UI implements it and marshals any widget work onto the
|
// 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) {
|
func (s *Service) RunDue(now time.Time) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
var startedID int
|
var startedID int
|
||||||
|
var startErr error
|
||||||
if !s.paused {
|
if !s.paused {
|
||||||
for index := range s.jobs {
|
for index := range s.jobs {
|
||||||
job := &s.jobs[index]
|
job := &s.jobs[index]
|
||||||
@@ -221,15 +222,16 @@ func (s *Service) RunDue(now time.Time) {
|
|||||||
if runtime.LastState == "Running" {
|
if runtime.LastState == "Running" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Async save errors cannot be returned to a caller here; surfacing them
|
startErr = s.startRunLocked(job, runtime, "Schedule")
|
||||||
// is deferred to T5.1 with the rest of the swallowed saves.
|
|
||||||
_ = s.startRunLocked(job, runtime, "Schedule")
|
|
||||||
startedID = job.ID
|
startedID = job.ID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if startErr != nil {
|
||||||
|
s.emit(ErrorOccurred{Err: fmt.Errorf("save jobs before scheduled run: %w", startErr)})
|
||||||
|
}
|
||||||
if startedID != 0 {
|
if startedID != 0 {
|
||||||
s.emit(JobChanged{JobID: startedID})
|
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)
|
record := s.runJob(ctx, &jobCopy, trigger, s.store.Paths.LogsDir)
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
var cleanupErr, saveErr error
|
||||||
if current := s.findByIDLocked(jobCopy.ID); current != nil {
|
if current := s.findByIDLocked(jobCopy.ID); current != nil {
|
||||||
runtime := s.runtimeForLocked(current)
|
runtime := s.runtimeForLocked(current)
|
||||||
runtime.LastRun = record.Time
|
runtime.LastRun = record.Time
|
||||||
@@ -293,13 +296,17 @@ func (s *Service) executeRun(ctx context.Context, jobCopy domain.Job, trigger st
|
|||||||
runtime.Output = record.Output
|
runtime.Output = record.Output
|
||||||
prependLog(runtime, record)
|
prependLog(runtime, record)
|
||||||
s.refreshNextRunLocked(current, runtime)
|
s.refreshNextRunLocked(current, runtime)
|
||||||
// Async save errors cannot be returned to a caller; surfacing them is
|
cleanupErr = runner.CleanupLogs(s.store.Paths.LogsDir, s.store.Config.MaxLogFiles, s.store.Config.MaxLogAgeDays)
|
||||||
// deferred to T5.1 along with the rest of the swallowed saves.
|
saveErr = s.store.SaveJobs(s.jobs)
|
||||||
_ = runner.CleanupLogs(s.store.Paths.LogsDir, s.store.Config.MaxLogFiles, s.store.Config.MaxLogAgeDays)
|
|
||||||
_ = s.store.SaveJobs(s.jobs)
|
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
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(RunRecorded{Record: record})
|
||||||
s.emit(JobChanged{JobID: jobCopy.ID})
|
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.)
|
// the UI thread. This is the sole place events touch widgets. (Resolves #4.)
|
||||||
svc.Subscribe(app.ObserverFunc(func(ev app.Event) {
|
svc.Subscribe(app.ObserverFunc(func(ev app.Event) {
|
||||||
recorded, isRecorded := ev.(app.RunRecorded)
|
recorded, isRecorded := ev.(app.RunRecorded)
|
||||||
|
errOccurred, isError := ev.(app.ErrorOccurred)
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
if isRecorded {
|
if isRecorded {
|
||||||
events = append(events, recorded.Record)
|
events = append(events, recorded.Record)
|
||||||
}
|
}
|
||||||
|
if isError {
|
||||||
|
events = append(events, newEvent(0, "Service", "Error", errOccurred.Err.Error()))
|
||||||
|
}
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user