T6.1: split jobs_view.go into three files — jobs_view_helpers.go (pure helpers) and jobs_view_details.go (detailsPanel struct with widget creation, update, clear, and container methods) — bringing jobs_view.go from 459 to ~200 lines. T6.2: remove stale YAML upgrade note from README; drop *.yaml from .dockerignore. T6.3: delete YAML shadow structs (yamlConfig/yamlJob/yamlJobsFile), importYAMLConfig/importYAMLJobs, legacy path constants, and all YAML-import tests; run go mod tidy to remove go.yaml.in/yaml/v4. T6.4: refresh ARCHITECTURE.md — JSON storage references, new Key Domain Concepts section (per-job overlap policy, run-time statistics + log seeding, persisted pause flag, jobs_view split). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.2 KiB
GoSentry Architecture
This document shows the current component interaction model. GoSentry is a single desktop process: the GUI, application service, scheduler, storage, and command runner live in one application. They communicate through typed events and well-defined interfaces rather than shared mutable state.
Package Map
cmd/gosentry entry point — starts the UI
src/
domain/ pure value types: Job, Config, RunRecord, Schedule, JobRuntime
app/ Service — sole owner of job/runtime state; emits typed Events
scheduler/ pure timing loop; calls app.Service.RunDue on every tick
runner/ shell command execution + log file writing + cleanup
storage/ JSON persistence (gosentry.json, jobs.json)
platform/
autostart/ Manager interface + Windows (shortcut) and Linux (XDG) impls
desktop/ display-scale helper (Linux only)
winproc/ hidden-window startup flags (Windows only)
ui/ Fyne windows, tabs, and dialogs; reads service via Events
Component Diagram
flowchart LR
user["Desktop user"]
ui["src/ui\nFyne windows, tabs, dialogs"]
svc["src/app Service\nsole owner of job + runtime state"]
store["src/storage Store\nJSON config and jobs"]
sched["src/scheduler Scheduler\npure timing loop"]
runner["src/runner\nshell command execution"]
autostart["src/platform/autostart Manager\nWindows shortcut / Linux XDG"]
config["gosentry.json\napplication settings"]
jobs["jobs.json\njob definitions"]
logs["logs_dir\nper-run command output logs"]
shell["Platform shell\ncmd.exe /C or sh -c"]
user -->|"edits jobs, settings, runs commands"| ui
ui -->|"CreateJob, UpdateJob, DeleteJob, RunNow, UpdateSettings, …"| svc
svc -->|"SaveJobs, SaveConfig, LoadJobs, LoadConfig"| store
store -->|"read/write"| config
store -->|"read/write"| jobs
svc -->|"Start(RunDue)"| sched
sched -->|"RunDue(now)"| svc
svc -->|"RunJob"| runner
runner -->|"execute command"| shell
runner -->|"write stdout/stderr log"| logs
runner -->|"RunRecord"| svc
svc -->|"emit JobChanged / RunRecorded / ErrorOccurred"| ui
ui -->|"display jobs, history, status"| user
ui -->|"SetAutostart, AutostartStatus"| autostart
svc -->|"Set / Status via Manager"| autostart
Main Flows
-
Startup:
cmd/gosentrycallsui.Run, which creates anapp.Service, opens the store, loadsgosentry.jsonandjobs.json, subscribes the UI to service events, builds the main window, and callsService.Startto begin the scheduler loop. On first launch the service seeds per-job run-time statistics from existing log files so the details panel reflects accumulated history immediately (see §Statistics below). -
Editing settings or jobs: The UI calls mutating methods on
app.Service(e.g.CreateJob,UpdateJob,UpdateSettings). The Service validates the request, updates its in-memory state, persists throughstorage.Store, and emits a typedEvent. The UI's observer receives the event and refreshes the relevant widget on the main thread viafyne.Do. -
Scheduled run:
scheduler.Schedulerfires a tick every second. On each tick it callsService.RunDue(now). The Service checks which enabled, non-paused jobs are due, marks each as running, and launchesrunner.RunJobin a goroutine. -
Manual run:
Run nowin the UI callsService.RunNow. The Service checks that the job exists, is not already running, and that the scheduler is not globally paused, then executesrunner.RunJobwith theManualtrigger. -
Command execution:
runner.RunJobbuilds the platform-specific invocation, executes the command through the platform shell, captures stdout and stderr, writes one timestamped.logfile, and returns adomain.RunRecordcontainingDurationMS(wall-clock milliseconds from start to finish; 0 forStartOnlyfire-and-forget jobs). -
History update: When a run goroutine completes,
Serviceupdates the job's runtime (including the statistics aggregate), saves JSON, triggers log cleanup, and emitsRunRecorded. The UI observer appends the record to the History tab. -
Autostart:
UpdateSettingsin the Service callsautostart.Manager.Set. The Manager interface has two implementations: Windows writes a.lnkshortcut to the user Startup folder; Linux writes an XDG Autostart.desktopfile. Both entries pass--start-in-tray. -
Error surfacing: Background errors (failed JSON saves, cleanup errors) are emitted as
ErrorOccurredevents and displayed in the UI status area, rather than being silently discarded.
Key Domain Concepts
Per-job overlap policy
domain.Job carries an OverlapPolicy field (json:"overlap_policy,omitempty").
When non-empty it overrides the global Config.OverlapPolicy for that job alone.
Empty means inherit the global default. app.Service.RunDue resolves the
effective policy per job: it uses job.OverlapPolicy when set, otherwise falls
back to store.Config.OverlapPolicy. normalizeJob in app/operations.go leaves
the field empty on new jobs so the inherit semantics are preserved.
Run-time statistics
domain.JobRuntime holds a rolling aggregate updated after each run:
| Field | Meaning |
|---|---|
RunCount |
total runs recorded |
FailCount |
runs that exited non-zero |
LastDurationMS |
wall-clock time of the most recent run |
AvgDurationMS |
mean over all runs with a recorded duration |
MaxDurationMS |
longest recorded run |
runner.RunJob measures the wall-clock start→finish and sets DurationMS on
the returned RunRecord. runner/logfile.go writes a duration line into the
log file header alongside the existing state line.
On startup, runner.SeedStats scans each job's log files (matched by the
_<sanitized name>.log suffix, bounded by Config.MaxLogFiles) and folds the
parsed state/duration headers into a runner.StatSeed map. NewService
applies those seeds to the runtime map before the first scheduler tick, so the
details panel shows accumulated run history immediately after a restart.
Older log files that pre-date the duration header are tolerated: the run is
counted but the timing is skipped.
Persisted global pause
domain.Config carries a Paused bool field (json:"paused,omitempty").
app.Service.SetGlobalPause writes the new value into store.Config and calls
SaveConfig, so the paused state survives a restart. NewService initialises
s.paused from store.Config.Paused and applies the paused next-run text to
all runtimes before the first tick, ensuring the UI shows the correct state from
the moment the window opens.
jobs_view.go file structure
src/ui/jobs_view.go is split across three files to stay within the ~250-line
size guideline:
| File | Contents |
|---|---|
jobs_view.go |
newJobsView — list, toolbar, button wiring, and layout |
jobs_view_details.go |
detailsPanel struct — widget creation, update, clear, container |
jobs_view_helpers.go |
Pure helpers — filteredJobIndexes, folderOptions, filterValue, indexOfID, lastJobLogs |