src/ui now imports only src/app, src/domain, and Fyne.
Three violations were fixed:
- src/scheduler (NewRealClock): Service.Start() now creates the real
clock internally; StartWith(clock) is the injectable seam for tests.
- src/platform/desktop (InstallDesktopIntegration): moved into
Service.InstallDesktopIcon in new src/app/platform.go.
- src/platform/autostart (AutostartStatus, SetAutostart): moved into
Service.AutostartStatus and Service.ApplyAutostart in platform.go.
go test -race ./... green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move settingsView and all its helpers (fyneVersion, mustParseURL,
chooseFolder, settingsRow, settingsRowWithStatus) plus their constants
out of mainwindow.go into the new settings_view.go. Save/autostart
errors were already surfaced to the status label; no logic change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move newHistoryView, historyCellText, logFileName, newEvent, and
collectActivity out of mainwindow.go into src/ui/history_view.go.
Drop the now-unused "sort" import from mainwindow.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves showJobDialog out of jobs_view.go into its own file and adds a
domain.Validate call on the schedule field so malformed cron expressions
are rejected with an error dialog before onSave is invoked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the jobs list, details panel, folder filter, toolbar buttons, and
all related helpers out of mainwindow.go into the new jobs_view.go.
newMainView now calls newJobsView(w, svc) which returns a panel and a
refresh closure; the subscriber and history wiring remain in mainwindow.go.
showJobDialog lives in jobs_view.go as a temporary home until T4.3
extracts it to job_dialog.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Carve src/gui/app.go into the new src/ui package:
- run.go: process lifecycle (single instance, app/window, tray, startup
timing).
- mainwindow.go: view assembly + the app.Service event listener.
Route every widget update driven by Service events through fyne.Do so the
run goroutine (executeRun) no longer mutates Fyne widgets directly. Also
wrap serveSingleInstance's Show/RequestFocus, which runs on the Accept
goroutine. (Resolves refactoring problem #4.)
fyne.Do/DoAndWait only exist in Fyne v2.6+, so upgrade fyne.io/fyne/v2
v2.5.3 -> v2.6.3. Mark the tray Quit item IsQuit so Fyne's
addMissingQuitForMenu reuses it instead of appending a second, localized
Quit now that v2.6 ships Russian translations.
go build / go vet / go test -race all clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fill the test gaps in the app package, raising coverage from 82% to 97%:
- format_test.go: cover all display helpers moved in T3.5 (StatusText,
EventText, Display*), previously untested.
- operations_test.go: add RunNow not-found and refused-while-running
paths, DeleteJob/SetEnabled not-found errors, UpdateJob re-enable,
runtimeForLocked lazy-recreate, UpdateSettings invalid-config branches,
and prependLog activity-list capping.
All tests are Fyne-free, using a temp-dir store, a fake runner seam, and
a fake Clock. go vet and go test -race pass across the module.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract pure display/formatting functions from the GUI:
- StatusText, EventText, DisplayFolder, DisplayArguments,
DisplaySuccessExitCodes, DisplayRunMode, DisplayInvocation, DisplayIndex.
These have no Fyne dependencies and serve as a shared formatting layer for
the service to use (e.g. StatusText in the tests when mocking the GUI).
The GUI imports them as app.DisplayFolder(...) etc.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
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>
Add src/app/operations.go with the seven intents that make the Service
the sole writer of job and runtime state: CreateJob, UpdateJob,
DeleteJob, SetEnabled, RunNow, SetGlobalPause, UpdateSettings. Each
returns error, persists through the store, and announces changes via
RunRecorded/JobChanged/SchedulerStateChanged events.
Extend the Service with a parsed-schedule cache, a global paused flag,
an injectable runJob seam (defaults to runner.RunJob) for testing the
run-now path, and a lifecycle ctx. Run and next-run timing now live in
the Service (duplicating the scheduler temporarily); T3.4 converts the
scheduler to drive the Service and removes the duplication.
Autostart is left to the caller until T5.2's injectable Manager; async
save errors in the run goroutine remain deferred to T5.1. Adds 12 tests
covering create/update/delete, enable/pause, global pause, run-now with
a fake runner, and settings persistence/validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add src/app/events.go: a sealed Event interface with three concrete
types (JobChanged, RunRecorded, SchedulerStateChanged), an Observer
interface plus ObserverFunc adapter, and Subscribe/emit on the Service.
This replaces the scheduler's single onChange callback with typed
events the UI can exhaustively type-switch over.
Dispatch is serialized by a dedicated dispatchMu (separate from the
state lock): observers never run concurrently, emit must be called
without holding s.mu so observers can read Service state, and observers
must not re-enter an emitting method. emit is wired to mutating ops in
T3.3. Adds tests for ordered multi-observer delivery, empty-observer
no-op, and observer-reads-state-without-deadlock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Create src/app/service.go: the application-service layer that becomes
the single owner of the durable jobs slice and the transient runtime
map, guarded by a non-reentrant sync.Mutex. NewService wires a loaded
store; Open() is the convenience entry point. Read-only accessors
(Jobs/Runtime/Store) take the lock, and Jobs() returns a copy to keep
callers from mutating Service-owned state.
State-mutating intents and the event/observer machinery are deferred to
T3.2-T3.4. Adds no-Fyne unit tests for runtime construction, copy
isolation, and store wiring.
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 autostart_*.go and tests from src/core to src/platform/autostart.
Update src/gui/app.go to call autostart.SetAutostart / autostart.AutostartStatus.
Add quoteDesktopExec to desktop_linux.go (was co-located in autostart_linux.go).
Co-Authored-By: Claude Sonnet 4.6 <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>
Move runner.go, runner_windows.go, runner_other.go, runner_test.go from
src/core to src/runner (package runner). Split helpers into focused files:
invocation*.go, exitcodes.go, logfile.go, cleanup.go. Export LogArguments
and SuccessExitCodesText for use by scheduler's runningOutput helper.
Update src/core/scheduler.go and src/gui/app.go to import src/runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract the hidden-window logic out of src/core runner and autostart files
into a new platform/winproc package with per-OS build-tag files. All call
sites updated to use winproc.ConfigureHiddenWindow. Builds clean on both
Windows and Linux; all tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the five domain types out of src/core/model.go into a new
src/domain package (job.go, record.go, config.go). The unexported
nextDue field is promoted to NextDue so it is accessible from core.
All references across src/core, src/gui, and cmd/gosentry are updated
to use domain.TypeName. src/core/model.go is reduced to a bare package
declaration. Windows and Linux cross-compilation both pass; all tests
remain green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track the 30 tasks across 5 phases with checkboxes. Each checkbox can be
marked complete as tasks land and pass review.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Document a phased plan to restructure GoSentry into focused packages
under src/ (domain, storage, runner, scheduler, platform, app, ui) with
an application-service layer that owns state, and link it from the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>