721b049100
The Jobs directory row named a folder and assumed the file inside it was called jobs.json. It is now a Jobs file row: Browse opens a file picker filtered to .json, the field stays editable so a file that does not exist yet can be typed, and the job list can live under any name. Config.JobsDir/jobs_dir becomes Config.JobsFile/jobs_file, holding the whole path; Paths.JobsDir is derived from it so saves still create the folder. An older gosentry.json is migrated on load by joining its jobs_dir with jobs.json — the exact file that version used — and the retired key is dropped when the config is rewritten. The default clears before unmarshalling, or a file that omits jobs_file and a file that sets it would be indistinguishable and the migration would never run. Saving used to write the current job list over whatever was at the new path, which made switching to an existing jobs file impossible: its contents were destroyed. An existing file now wins. Its jobs are loaded, normalized, and adopted, with runtimes, schedule cache, next-run times and log-seeded statistics rebuilt around them by adoptJobsLocked — the same helper NewService now uses, so construction and adoption cannot drift. A path with no file behind it still receives the current jobs, which is how the file is renamed or relocated. The new file is read before anything is written, so an unparsable one leaves both the config and the jobs untouched. Adoption drops every runtime, and a run finishing afterwards would write its result onto whichever job inherited its ID, so the switch is refused while a job is running. Unrelated settings still save during a run. Because the replacement happens without a prompt, the Service emits JobsLoaded with the path and count, and History carries the receipt. A path that names only a folder (trailing separator, a dot, or two dots) is rejected with a validation error instead of failing later with an opaque OS error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
3.5 KiB
Markdown
64 lines
3.5 KiB
Markdown
# GoSentry — Standards
|
|
|
|
Quality rules and intentional behavior for contributors. Package contracts live
|
|
in [ARCHITECTURE.md](ARCHITECTURE.md); test conventions in [TESTS.md](TESTS.md);
|
|
what a whole-project review looks at, in [REVIEW.md](REVIEW.md).
|
|
|
|
## Code quality
|
|
|
|
- Follow package contracts in [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
- User-facing errors → `dialog.ShowError` or a History event, never a silent `return`.
|
|
- Pure helpers → unit test in the same package.
|
|
- Fixes with severity ≥ medium → regression test.
|
|
- Documented intentional behavior → section below, not a backlog bug.
|
|
- UI view constructors accept `*app.Service`; call `app.Open()` only from `run.go`.
|
|
|
|
## Config file compatibility
|
|
|
|
There is no migration step: `gosentry.json` and `jobs.json` are read as-is, are
|
|
meant to be hand-editable, and may have been written by an older version. A
|
|
change to their shape has to stay compatible on its own.
|
|
|
|
- A new `Config` field is tagged `omitempty`, and its zero value must mean the
|
|
behavior that existed before the field was added — a file written without it
|
|
keeps working unchanged. `DefaultConfig()` still sets the value explicitly.
|
|
- A zero that carries meaning is not a missing field and must not be backfilled
|
|
on load. See `DefaultTimeoutSeconds` in `storage.loadOrCreateConfig` and
|
|
`Job.TimeoutSeconds *int`, where unset and `0` are different answers.
|
|
- An unrecognised enum value reads as the default rather than an error, through
|
|
one helper that every consumer shares (`JobListView.IsCompact`, `ui.themeFor`),
|
|
and is normalized before being written back, so the file never gains a value
|
|
no reader understands.
|
|
- A renamed key keeps the old field on `Config` (tagged `omitempty`) purely so
|
|
it can still be read. `storage.loadOrCreateConfig` converts it to the new
|
|
field and clears it, so the retired key disappears on the next save. See
|
|
`Config.JobsDir` → `Config.JobsFile`. Where the new field has a non-empty
|
|
default, clear that default before unmarshalling, or "the file omits it" and
|
|
"the file sets it" become indistinguishable and the conversion never runs.
|
|
- Each of the three gets a test: the default in `storage`, the normalization in
|
|
`domain`, and a round-trip through the real config file in `app`.
|
|
|
|
## Intentional behavior (not bugs)
|
|
|
|
- `RunNow` is allowed during global pause and for disabled jobs.
|
|
- Selecting a jobs file that already exists **loads** it: its jobs replace the
|
|
in-memory list, which is the only way the user can switch between job lists. A
|
|
path with no file behind it receives the current jobs (rename/relocate). The
|
|
switch is refused while a job is running, because adoption drops every runtime
|
|
and a finishing run would then write its result onto whichever job inherited
|
|
its ID.
|
|
- Sequential mode runs jobs FIFO by order in `jobs.json`.
|
|
- Scheduler tick is 1s — sub-second `@every` intervals are not supported.
|
|
- Command timeout defaults to no timeout globally (`Config.DefaultTimeoutSeconds`
|
|
= 0) and is overridable per job (`Job.TimeoutSeconds *int`: unset = inherit the
|
|
global default, 0 = no timeout, positive = seconds). Neither zero may be
|
|
normalized away on load — 0 is a value, not a missing field.
|
|
- **History tab is session-only.** `JobRuntime.Logs` exists only in memory for the
|
|
current process. Log files on disk feed aggregate statistics via `SeedStats`
|
|
only. See [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
|
|
## Out of scope
|
|
|
|
Larger or blocked work is tracked in [ROADMAP.md](ROADMAP.md) (window size
|
|
persistence, History column filters, CI coverage gate).
|