The scheduler no longer shares a *[]domain.Job with the GUI. It is now a
thin timing loop with an injected Clock that calls a tick callback; the
application service is the sole writer of job and runtime state.
- scheduler: add Clock interface + RealClock (clock.go); strip all job
logic from scheduler.go (NewScheduler(clock, tick)); rewrite tests to
cover the loop with a fake clock.
- app.Service: add RunDue(now) (pause + one-run-per-tick policy, records
back through the service) and Start(Clock)/Stop() owning a cancelable
run context; prime each job's first next-run at construction. Capture
the run context under the lock for executeRun.
- gui: talk only to app.Service (no shared state) — Open() the service,
keep a refreshed snapshot, route every mutation through the service,
and react to changes via a single Subscribe listener.
- Tests: add RunDue (due/not-due/paused) and Start-drives-RunDue cases.
Verified with CGO + MSYS2 UCRT64: go vet ./... clean, go test -race
./... green (GUI included), full module builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>