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>
Move all transient execution state off domain.Job into a new
domain.JobRuntime, keyed by job ID:
- domain: Job now holds only durable YAML fields; remove the yaml:"-"
fields (LastRun/NextRun/LastState/Logs/Output) and NextDue. Add
runtime.go with JobRuntime plus NewRuntime/NewRuntimes constructors,
which now own the runtime-init logic moved out of normalizeJobs.
- runner: RunJob no longer mutates the job; it is pure and returns the
RunRecord for the caller to fold into the runtime.
- scheduler: take a shared map[int]*JobRuntime and route status/next-run
bookkeeping through runtimeFor(job); prepareNextRun writes a *JobRuntime.
- storage: normalizeJobs touches only durable config.
- gui: own the runtime map (NewRuntimes), share it with the scheduler,
and read/write runtime state via runtimeFor; maintain the map by ID on
add/edit/delete.
- tests: update scheduler/storage tests to the split; tidy a pre-existing
import-order nit in scheduler.go.
This also satisfies T2.4 (storage load/save only Job, runtime init in
domain.NewRuntime, round-trip tests), since removing the fields forced it.
Runtime-map ownership remains GUI-side glue until T3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse each job's schedule once on load (resetNextRuns) and on edit
(RefreshSchedule) via the new parseJobSchedule helper, caching the
result in a map[int]domain.Schedule keyed by job ID. prepareNextRun
now looks up the cached Schedule instead of re-parsing the string on
every call. Remove the nextRunTime wrapper that did the per-call
parsing. Drop the three scheduler_test.go tests that duplicated
coverage already in domain/schedule_test.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce src/domain/schedule.go with a Schedule value object that
centralizes schedule parsing and validation: Parse, Validate, and
Next(time.Time). It owns the cron parser and @every handling, moved out
of the scheduler. The scheduler's nextRunTime is kept as a thin wrapper
delegating to domain.Parse for now (T2.2 will parse once on load/edit).
Add unit tests covering invalid specs, @every intervals, five-field cron,
cron descriptors, whitespace trimming, the zero-value Next, and String.
Mark T2.1 complete in REFACTORING.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move store.go, paths.go, and store_test.go from src/core into the new
src/storage package. Update src/scheduler and src/gui to import storage
instead of core for Store/Paths/OpenStore. Empty the moved files in core
to preserve the package declaration for the remaining core symbols.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move scheduler.go and scheduler_test.go from src/core to the new
src/scheduler package. The scheduler still takes *[]domain.Job and
*core.Store (storage moves in T1.5). Update src/gui/app.go to import
the new package; rename the local variable to sched to avoid shadowing
the scheduler package name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>