Every item in docs/PROJECT_REVIEW_PLAN.md is now either landed or moved to
ROADMAP.md, so the working document retires the way TEST_REVIEW_PLAN.md did.
Nothing else referenced it.
Two 1.0.2 Internal entries were out of step with what shipped: the timing
diagnostic is written as notify-timing.tsv (kept out of CleanupLogs and off
the UI thread), not notify-timing.log, and the removal of the two per-run
no-op jobs.json rewrites - along with the run-start rollback and the start
error RunDue used to surface - was not recorded at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 11 of PROJECT_REVIEW_PLAN.md: the themed cleanup pass over every
low-severity finding still open (2.2-2.3, 3.4-3.6, 4.3-4.7, 6.4-6.7,
7.1-7.3, 8.2-8.3, 9.1-9.4, and the under-documented decisions in §10/§11).
Behavioral fixes:
- Reassign duplicate job IDs in a hand-edited jobs.json instead of letting
two jobs share one runtime, schedule entry, and SeedStats bucket.
- Disambiguate run-log file names that collide within the same second.
- Compute AvgDurationMS as DurationSumMS/TimedRunCount instead of an
incremental integer mean, so it always matches the seeded-from-logs
average instead of drifting from truncation error.
- Clean absolute paths in ResolveConfiguredPath so two spellings of the
same jobs file do not trigger a spurious adoption.
- Report InstallDesktopIcon failures through ErrorOccurred instead of
discarding them silently.
- Move settingsView's blocking AutostartStatus (PowerShell on Windows) off
the UI thread.
- Give notify-timing.tsv its own extension so CleanupLogs no longer
manages it as a run log.
- Replace the settingsView Save handler's second copy of validateConfig's
rules with a bare parse, letting the Service's own error surface.
Cleanups:
- Delete collectActivity, the dead yaml tags on RunRecord, and the
logArguments/LogArguments alias.
- Fold the two systemTrayRegistered/mainWindowHidden globals into one
trayState instance Run owns and threads through Settings and the
single-instance reveal path.
- Fix stale comments/docs: the frozen window-size restore claim, a
reference to a renamed recordRun, README's "Pause all" and notification
wording, the PowerShell quoting note for TESTS.md's coverage command,
and scripts/test.bat's UTF-8 checkmarks under a non-UTF-8 code page.
- Document the single-instance fallback's consequence and the
unauthenticated instance-channel port in STANDARDS.md; record the
config-shim retirement plan in ROADMAP.md.
3.5, 7.3, and 9.4 turned out to already be fixed by earlier phases; no
change needed for those three.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 10 of the whole-project review (findings 5.1 and 5.2), folded into
the ROADMAP file-split item as that plan asks.
5.2 was a real defect. `selected` was an index into a snapshot of the jobs
slice, and every path that changed the slice patched it by hand. The one
path that could not — adopting a different jobs file, where the Service
replaces the whole list and the view only hears about it through the
refresh JobsLoaded triggers — left the details pane redrawing from an
index that belonged to the previous list, describing whichever job now sat
there (or clearing when the new list was shorter) while the list highlight
stayed put. The selection is now a job ID; rows are derived from it at
render time, and refresh ends by pointing the highlight at the selected
job, so the two can no longer disagree.
5.1: newJobsView was one 330-line constructor whose dozen closures shared
seven mutable locals. It is now a jobsView struct over a jobsViewState
that owns the snapshot, the folder filter, and the selection — the
invariant that used to be maintained by hand in five places lives in one
place — split across jobs_view.go (construction, refresh, layout),
jobs_view_state.go, jobs_view_list.go, and jobs_view_toolbar.go. The
folder-option rebuild that appeared verbatim in three handlers is one
method.
Behaviour that changed beyond the fix: switching the folder filter keeps
the current selection when the new filter still shows it, instead of
always jumping to the folder's first job.
Docs: ARCHITECTURE records the new file layout and the selection-by-ID
contract; ROADMAP drops jobs_view.go from the over-guideline table and
refreshes the other five numbers (finding 2.4); TESTS documents the new
state test file and the adoption regression test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 8 (PROJECT_REVIEW_PLAN.md 8.1): 0 in MaxLogFiles/MaxLogAgeDays now
means "keep everything" end to end. runner.CleanupLogs already treated
<= 0 as disabled; validateConfig, the Settings form, and
loadOrCreateConfig's backfill were the only things making that state
unreachable.
Phase 9 (1.1, rolling up 1.2, 1.3, 7.3): added Service.Config() and
Service.Paths(), copying under mu, and converted every UI site that read
Service state through the raw *storage.Store returned by Store() (now
removed). jobs_view's pause control is now driven by refreshView reading
svc.Config().Paused on every event instead of only mirroring its own tap
handler, which makes it an actual consumer of SchedulerStateChanged.
mainwindow's event listener is a real type switch, and events.go's doc
comment no longer claims a compiler exhaustiveness check Go doesn't have.
Unexported the redundant SetAutostart/AutostartStatus package functions
in platform/autostart now that only the Manager methods are used outside
the package.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 7 of the whole-project review (findings 3.2 and 3.3).
Service.mu is the lock the Fyne main thread takes on every Jobs() and
Runtime() call, so anything blocking inside it makes a UI refresh wait on
the disk. Three things did:
- Every SaveJobs/SaveConfig was a marshal, fsync, and rename under mu.
Writes are now prepared under the lock (Store.PrepareSaveJobs /
PrepareSaveConfig snapshot the payload and target path) and run after
it is released. deferSaveLocked takes saveMu while mu is still held, so
writes still reach the file in the order their snapshots were taken and
an older snapshot can never land on top of a newer one.
- executeRun ran runner.CleanupLogs under mu after every run. It needs
only the values already snapshotted into runEnv, so it now runs after
the unlock — including when the job is gone, since the run still wrote
a log file that retention covers.
- adoptJobsLocked ran runner.SeedStats under mu, reached from
UpdateSettings on the UI thread. Seeding moved out into
applySeededStatsLocked; UpdateSettings now reads the new jobs file and
seeds its statistics before taking the lock, and re-checks the
"no jobs-file switch while running" guard once it has it.
SeedStats also opened every log file twice — once to find the job, again
to read the result. readLogSummary reads job_id, state, and duration in
one pass, so each log is opened once.
StartOnly runs were built with exec.CommandContext on the app's lifecycle
context. os/exec keeps a watcher goroutine alive until Wait returns or the
context is done, and StartOnly never calls Wait, so one goroutine leaked
per run and would then try to kill a process whose handle startJobOnly had
already released. The invocation now uses context.Background(), whose nil
Done channel means no watcher is started at all.
Regression tests: TestRunJobStartOnlyLeavesNoContextWatcher (fails with 5
leaked goroutines on the old code), TestConcurrentJobOperationsLeaveTheFileMatchingMemory,
and TestUpdateSettingsSeedsAdoptedJobsFromLogs. STANDARDS gains the
no-I/O-under-mu rule and the "a StartOnly process outlives GoSentry" entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
History was appended to on every recorded run and never trimmed, and every
event re-sorted the whole slice and re-measured the Job, Detail and Log
columns across every row. The per-run cost therefore grew with the number of
rows, in exactly the mode the app is designed for: left in the tray for days.
The session History now keeps the newest maxHistoryRows (1000) records, the
way maxJobLogs caps a job's own activity list, and drops the oldest from the
front, zeroing the tail so a dropped record's full captured output is not
kept alive by the backing array. Column widths move into a historyLog value
that folds each new record into the current maxima instead of rescanning.
Widths only grow within a theme, so a column never narrows when a record ages
out; a theme change is the one case that still rescans, because every stored
width was measured at the old text size.
Measured with a throwaway benchmark over 5000 accumulated records: one
refresh went from 15.8 ms to 0.9 ms. At the new cap the full width rescan
alone costs 1.5 ms, so both halves of the fix carry weight.
Plan item 6 of docs/PROJECT_REVIEW_PLAN.md (finding 3.1).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements items 4-5 of the whole-project review's suggested order
(docs/PROJECT_REVIEW_PLAN.md):
- Drop the three SaveJobs calls in the run lifecycle (startRunLocked,
executeRun, SetGlobalPause): none of them change a durable Job field,
everything they touch lives on JobRuntime, which is never persisted.
Retire TestStartRunLockedRollbackOnSaveFailure with the rollback it
guarded, since a run can no longer fail to start this way.
- Clear PendingRuns (the "queue" overlap policy's backlog) when a job is
disabled or the scheduler is globally paused, so resuming or
re-enabling a job no longer replays a deferred run left over from
before the pause/disable. Cap it at maxPendingRuns (10) so a job whose
runs take longer than its own interval stops accumulating an unbounded
backlog. Surface the queued count in the details pane via DisplayStats.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements items 1-3 of the whole-project review's suggested order
(docs/PROJECT_REVIEW_PLAN.md):
- Restore TestJobListViewIsCompact, accidentally dropped by 5b0e6fe;
drop the redundant TestDefaultConfigUsesDetailedJobList row from
TESTS.md and document the two other doc gaps the review found.
- Fix quoteLeadingWindowsProgramPath to find the earliest file-extension
match at a word boundary instead of the first extension in list order,
so a .bat/.cmd command whose argument ends in .exe no longer has its
whole command line mistaken for the program path.
- Write gosentry.json, jobs.json, and run log files atomically (temp
file + rename) so a crash or power loss mid-write can no longer leave
a truncated file. Wire Service.Stop() into the app shutdown path so
it actually runs, cancelling the run context for in-flight runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records the findings of a review of the whole codebase against the agenda in
REVIEW.md, with a suggested order and a model recommendation per item.
The project is in good health: the engine layering holds, the Service.mu
locking contract is obeyed, and the UI layout code observes the
measure-don't-hardcode rule with tests to match. The findings concentrate in
three places instead: paths that only surface after the app has run for a
while (unbounded History growth, PendingRuns surviving a pause), durability of
the JSON files (non-atomic writes, no shutdown path), and one Windows quoting
bug reproduced outside the build, where an argument ending in .exe hijacks
program-path detection for a .bat or .cmd job.
Also settles what happened to the two domain tests TESTS.md still documents:
5b0e6fe dropped them while adding two new rows to TESTS.md and leaving the old
rows in place, so the loss was accidental rather than the test-suite review's
doing. Only TestJobListViewIsCompact is worth restoring — the storage default
test already subsumes the other. Neither loss moved coverage; the 84.4% ->
84.0% dip is new 1.0.2 code, PeekKeepRunningInTray at 0%.
Temporary: delete once the items are done or moved to ROADMAP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fyne toasts read App.Icon without SetIcon, preserving the PE multi-size
window and taskbar icon while giving failure notifications app artwork.
Co-authored-by: Cursor <cursoragent@cursor.com>
Seed Failure notification test in defaultJobs so new installs can verify
Settings notifications via Run now without scheduler spam.
Co-authored-by: Cursor <cursoragent@cursor.com>
Explain why autostart, file manager, shell, and winproc are OS-specific, where compile-time vs runtime branching applies, and rules for new platform code.
Co-authored-by: Cursor <cursoragent@cursor.com>
Autostart entries pass --start-in-tray only when the tray is enabled; Settings warns that the notification icon needs a restart (Fyne limitation).
Co-authored-by: Cursor <cursoragent@cursor.com>
Move post-v1.0.0 changes out of the 1.0.0 changelog section into 1.0.1,
where they belong: GoSentry theme as default, System label, About GitHub link,
Disable auto inset, README @every docs, and the completed test-suite review.
Co-authored-by: Cursor <cursoragent@cursor.com>
Every item in TEST_REVIEW_PLAN.md is done or decided, so the working
document goes as its own header instructed. What outlives it moves to
where a later reviewer will actually look:
- TESTS.md gains the -coverpkg command and the 84.4% baseline (per-package
figures understate the suite), design principle 9 (redundancy is judged
by comparing coverage profiles, and identical coverage alone is not
grounds for deletion), a table of the look-alike tests that are kept
with the reason each survives, and the list of functions deliberately
at 0%.
- STANDARDS.md's "Intentional behavior" section points at both lists, so
the mechanism REVIEW.md describes still reaches them.
Dropped as spent: the per-item checklists, the suggested order, and the
model-selection table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Item 4 of the test-suite review:
- Delete TestEmitWithNoObserversIsNoop (no assertion; ranging a nil slice
cannot panic) and TestStoreReturnsWiredStore (a getter returning its
own field).
- Collapse the four TestFilteredJobIndexes* tests into one table-driven
TestFilteredJobIndexes, matching TestFilterValue above it.
- Replace the TestMainViewBuilds smoke test with
TestMainViewRecordStartupAddsHistoryRow, which calls the recordStartup
closure for both wordings run.go selects between and asserts the rows
reach the History table through its own cell callbacks. Keeps the
unique coverage the review identified and adds the !windowShown branch.
Item 5 is declined with measurements: the three RunJob tests cost 0.14 s
combined, so merging them saves ~90 ms while forcing their three
fixtures (including the only Manual trigger) into one. The runner
package's runtime is the two timeout tests, not subprocess spawns.
go vet and go test -race pass for src/app and src/ui.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Items 1-3 of the 2026-08-04 test-suite review: TestCleanupLogsKeepsFilesWithinAgeLimit,
TestRunDueEmptyOverlapInheritsGlobal, and TestSameWindowsPathHandlesSpaces had
byte-identical coverage to an existing test and no assertion the survivor lacked.
storage.defaultJobs, the one accidental 0% coverage gap the review found, is now
covered and TESTS.md corrected to match. seed_test.go's itoa is replaced with
strconv.FormatInt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The deciding factor is the slow feedback loop, not task size: the ui
package needs CGO and the MSYS2 toolchain, and a cold test run took 258s
during the review. Getting an edit right on the first pass is worth more
than generating it faster.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the findings of a review of the whole test suite: three duplicate
tests confirmed by comparing coverage profiles, two inaccuracies in
TESTS.md, a hand-rolled itoa in seed_test.go, and four tests thin enough
to need a decision.
Also lists the pairs that share a coverage profile but assert different
properties, and the functions whose zero coverage is deliberate, so a
later pass does not re-report them. Temporary: delete once the items are
done or moved to ROADMAP.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row sat flush against the tab bar while the VBox gap below was already one theme padding; matching that on top balances the spacing.
Co-authored-by: Cursor <cursoragent@cursor.com>
GitHub is a pruning push mirror of Gitea, so a tag created by
"gh release create" belongs to no upstream ref and disappears on the next
synchronisation, orphaning the release and its archives. Document the order
that survives it: push the tag to Gitea, wait for the mirror, verify the tag
on GitHub, publish with --verify-tag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The section began at the tag, so everything that has to be true before the
tag exists was either implied or nowhere: the version bump was named only
as the thing the tag must match, and the changelog entry, the test run and
the push were not mentioned at all. The push matters because the tag has
to land on a commit the forge has.
The screenshots are the reason for writing this down. README.md is
packaged into all three release archives and is what the forge renders on
the project page, and nothing fails when the shots are stale — the release
just ships pictures of an older GUI. This project changes visible UI most
releases, so the default assumption should be that they need retaking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six commits landed after the 1.0.0 section was written, and none of them
appear in it. The release was never tagged or pushed, so they belong in
that section rather than in a 1.0.1 for changes no one could have run:
version.go stays at 1.0.0. This is the same call bde9a2e made for 0.16.0.
They are worth recording rather than dropping as "docs only". README.md
ships inside every release archive, and its gosentry.json sample — the one
file the user is invited to hand-edit — was wrong until this pass. The
rest is the contributor documentation catching up with the code it
describes: TESTS.md indexing 130 of 170 tests, ARCHITECTURE drawing an
edge the UI does not have, and the file-length guideline stated as settled
while six files are over it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note said Codeberg needs the secret but not what happens without it,
so the failure mode had to be inferred from a red job. Record it: build
and packaging still succeed, the upload step fails on authentication and
takes the job with it, and the release is published with no assets.
Not "the upload step is skipped", which is what the comment in
.forgejo/workflows/release.yml claims. That step is guarded only by
`if: github.event_name == 'release'` — there is no token check and no
continue-on-error, so an absent or unscoped token makes it fail rather
than skip. Skipping is the workflow_dispatch path. The workflow comment
is still wrong and is left for a separate change.
Also name why GitHub needs no setup: softprops/action-gh-release falls
back to the built-in GITHUB_TOKEN and the workflow grants contents:write.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two package-* invocations sat next to each other with nothing but
the code-fence language to tell them apart, and the output paths were
pooled in one block that mixed Linux tarballs with the Windows zip.
Label each command Windows: / Linux: like Run From Source does, and give
each its own artifact list.
Also note that package-linux.sh emits the arm64 tarball only when
aarch64-linux-gnu-gcc is installed; the flat list implied a plain Linux
host always produces both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The document opened with dependency installation and buried "Run From
Source" between the build and release sections, so a newcomer met the
MSYS2 setup before learning what the stack was. Reorder it as stack and
tools, external libraries, run from source, build, release, CI, and add
a clickable two-level Contents block.
External libraries now list version, repository, and license type in one
table, and the package-* scripts are documented for the first time. The
Project Layout section is gone: it duplicated the package map in
ARCHITECTURE.md and had drifted out of date, missing
src/platform/filemanager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Application section stacks its rows with rowOverlap(), a negative
spacing that trades away one label's duplicated text inset. The Theme
row's value is a Select, which paints its box out to the row's edge and
has no inset to give, so the overlap closed the gap instead: 0.46 px
between the Notifications checkbox and the dropdown, against ~8 px
between the checkbox rows.
cancelRowOverlap adds that one padding back on the Theme row's top edge
only, restoring the gap to 7.5 px without touching the other rows or the
column width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ~250-line guideline is currently broken by six source files, not the
two the last commit named from the ui section it was editing:
operations.go at 490 is the worst, and both files that were already split
once are back over.
Fixing them belongs in the next whole-project review rather than in
one-off commits. REVIEW.md item 2 already asks for exactly this sweep, and
doing all six together is what keeps the seams consistent — six separate
passes would settle the same question six ways. A split also reads as pure
movement while it is the easiest change in which to silently drop a
function, which is an argument for one careful pass rather than several
hurried ones.
The item records the seams that are visible today so the pass does not
start cold: operations.go splits along the three consecutive blocks it
already has, history_view.go's column measurement is pure and separable,
and jobs_view.go is the hard one because almost all of it is a single
constructor that has to be broken up rather than moved. The three files
barely over the line are flagged as re-measure-first, not split-on-sight.
ARCHITECTURE now points at that item instead of describing the overage in
passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four documents asserted things the code contradicts.
ARCHITECTURE's component diagram had the UI calling the autostart Manager
directly. It does not, and must not: src/ui holds no reference to the
package at all — Settings reads svc.AutostartStatus(), like everything else
it reads. The edge is folded into the existing ui→Service one, so the
diagram no longer draws the exception to the project's own rule.
platform/desktop was described in both ARCHITECTURE and DEVELOPMENT as a
"display-scale helper". It installs the .desktop entry and icon under XDG
data home; there is no scale helper in it.
The ~250-line file guideline was written as though the jobs_view and
settings_view splits had settled it. Both files are over it again and
history_view.go has never been split, so the guideline is now stated as
the target it is, with the current state named rather than implied.
STANDARDS pointed at a "CI coverage gate" item that ROADMAP does not have,
while omitting the two it does.
README's gosentry.json sample was three keys short of what the app writes
on first run — default_timeout_seconds, theme and job_list_view — which
made the one file the user is invited to hand-edit the least accurate
thing in the document. The sample is now the real default (verified by
marshalling DefaultConfig), with the keys explained, including why a zero
timeout is written out and an unset one is not. The per-job overrides for
overlap policy and timeout were undocumented despite being in the job
dialog, and the feature list had not caught up with the timeout, the theme,
or the compact job list.
Version numbers in example output paths are now <version>, matching how the
CI section already wrote them, so they cannot go stale again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The document had drifted well past the point of being useful as an index:
it listed 130 tests against 170 in the tree, described four test files that
were never added to it at all (domain/config_test.go, runner/seed_test.go,
ui/layout_test.go, ui/theme_test.go), attributed runner's Windows
invocation tests to the wrong file, kept two tests that no longer exist
(TestParseRegistryRunValue, TestLinuxAutostartRemovesLegacyDesktopEntry),
and filed three activity-panel tests under jobs_view_test.go when they live
in history_view_test.go.
Most of the gap is the work of the last few releases: the per-job timeout's
three states, the persisted pause and jobs-list density, SeedStats, and the
whole GUI-geometry set the layout review produced.
Three claims were false rather than merely missing. The ui test files are
no longer "pure helpers, no Fyne widget construction" — they build views
under test.NewApp() and measure them, which is now stated as its own design
principle, because that measurement is what makes the STANDARDS rule about
theme-derived sizes enforceable. layout.go is no longer an open coverage
gap. And the coverage-gap list now names the real remaining one: Fyne's
headless driver cannot report a maximized window, which is the reason
window-size persistence is frozen.
Verified mechanically: every test function in the tree appears exactly once
in the document, under the file it actually lives in, and the document
names no test that does not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 0.16.0 release was never tagged or pushed, so what it described ships
as 1.0.0 instead: the changelog section is renamed rather than followed by
an empty one, and there is no 0.16.0 for anyone to have seen.
The last remainder of the layout review's F9 goes in with it. The value
column in captionValueLayout has no minimum of its own — it takes whatever
the container leaves after the caption — and what actually keeps it
readable is the 460 px minimum on commandOutputScroll, a constant that
exists for command output being legible. The dependency was invisible at
both ends; both now state it, so lowering that width is a decision rather
than an accident. The HSplit divider is the user's side of the same thing:
it is how the value column can be widened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 9 of the GUI layout plan: the roadmap item the review was raised
under is closed, so the plan and the findings document go with it — what
they established now lives in STANDARDS and the CHANGELOG.
STANDARDS gains the rule the review produced: a size that must follow the
theme is measured at build time, not written as a pixel constant, because
a hand-tuned number is only correct for the theme it was tuned against.
rowOverlap, captionColumnWidth, textColumnWidth, activityRowsHeight and
initialSplitOffset are the worked examples.
The CHANGELOG entry keeps to what the user can see: the window opens at
the size it asks for and drags smaller, the Jobs divider is draggable,
History columns hold their content on a scaled UI, and the Settings
button row and block spacing are as their layouts intended.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The jobs view pinned the sidebar in a Border left slot, so it rendered at
its MinSize and nothing else: the user could never trade list width for
detail width, and the details value column had no escape from the width
the metadata grid left it.
container.NewHSplit replaces the Border. Either pane can now be dragged,
and container.Split clamps both at their own content minimum, so the
divider degrades a pane rather than clipping it.
The initial offset is derived, not a literal: SetOffset takes a ratio,
but the sidebar's natural width is absolute (448 px), and a ratio that
fits 1024 px would hand the sidebar 700 px at 1600 px. initialSplitOffset
divides that width by the default window width less the divider, which
opens the sidebar at exactly its content width with the rest to details.
The divider position is not persisted; a restart reopens at the computed
default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 5: generalize logColumnWidth into textColumnWidth so History's
Trigger/Job/State/Detail/Log columns size from measured text instead of
pixel constants that clipped at larger text sizes (F6, F14).
Stage 6: captionColumnWidth replaces detailCaptionWidth and
settingsLabelWidth with one theme-derived helper; jobs_view_details.go
now builds its metadata rows and their width from a single
metadataRows() list instead of two hand-kept ones (F10); the Settings
button row drops its transparent-rectangle spacers for a
CustomPaddedLayout (F8); the remaining eight fyne.TextTruncate call
sites move to the non-deprecated Truncation field (N1).
Stage 7: settings_view.go split into settings_view.go (field
construction/save/load/validate), settings_view_layout.go (the
two-column layout and settingsSection/settingsRow), and
settings_view_helpers.go (fyneVersion, dialogs, path helpers),
mirroring the jobs_view.go split. Along the way, Queue/Storage's inline
VBox and Application/About's settingsSection collapse into one
settingsSection(title, spacing, rows...) constructor, and
chooseFile/chooseJSONFile merge into one function with a filter
argument.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stage 4 of the GUI layout plan (F11, F12).
History: the cell callback copied and sorted the whole event list on every
call, and a full-window Refresh issues one call per visible cell — 126 sorts
of a 300-element slice per redraw, measured. The sorted snapshot now lives in
`rows`, refilled by `resort()` at build time, on a sort toggle, and from
`refresh()`. The length callback moves to `len(rows)` with it: cells and the
row count have to read the same slice, which was only incidentally true while
each cell re-derived the order for itself. The column captions become a
package-level array instead of a slice reallocated per header update, and the
per-cell `TextStyle`/`Refresh()` pair goes — the template already carries the
zero style and `SetText` refreshes.
Jobs: `refreshView()` already re-reads the service snapshot and refreshes the
list, so the six `list.Refresh()` calls that preceded it, and the duplicate
`syncFromService()` in the pause handler, were redundant. The folder filter's
early-return path never reaches `refreshView()`, so its `list.Refresh()` moves
into that branch rather than being deleted.
Both changes carry regression tests, each verified to fail against the
behaviour it guards: `TestHistorySortToggleKeepsRowsInSync` for the
cache-versus-length hazard, `TestToolbarButtonRedrawsRowAndDetails` for the
handlers that now rely on `refreshView` alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part A stages 1-3 of the GUI layout cleanup plan:
- Stage 1 (F1-F3): delete settingsControlWidth's redundant wrapper (the
Border centre slot already stretches controls), truncate the config
path label, and name the default window size so it can be asserted
against. Settings no longer widens the window past what it asks for.
- Stage 2 (F4-F5): drop compactVBoxLayout for the stock
layout.NewCustomPaddedVBoxLayout and a single derived rowOverlap()
spacing, replacing three hand-tuned spacing constants.
- Stage 3 (F7): delete the inert 400px sidebar width floor; the Border
left slot already renders it at content MinSize.
See docs/PLAN-gui-layout.md.
Turns all fifteen findings from GUI-LAYOUT-REVIEW.md into nine staged commits,
each with the exact file changes, the tests it owes, and the measured result it
should produce.
Part A (stages 1-6) is the single-fix set: bring the window minimum under the
1024x660 the app asks for, retire compactVBoxLayout for the stock
layout.NewCustomPaddedVBoxLayout, delete the sidebar width floor, hoist the
History sort out of the per-cell callback, measure the History columns from
their content, and fold the caption widths into one theme-derived helper.
Part B (stages 7-8) is the roadmap-sized work: split settings_view.go and
replace the fixed Border sidebar with a draggable HSplit. Stage 9 closes the
ROADMAP item, adds the rule this review established to STANDARDS, and ships as
0.16.0.
Three decisions are settled and folded in: everything is in scope, so nothing
carries forward to ROADMAP; the split divider position is not persisted, which
keeps stage 8 inside src/ui; and the row spacing unifies on -8, the one
deliberate visual change in Part A.
Also records a recommended model per stage, split by judgement density rather
than diff size, plus the trap each stage carries - the History length callback
in stage 4 and the paired label change in stage 6 are the two that fail
silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carries out the "GUI review — custom layouts and composition" roadmap item and
records the result in docs/GUI-LAYOUT-REVIEW.md. Composition only; no code was
changed. Every number was measured with a throwaway headless Fyne probe at text
size 14 and 20, not estimated.
Headline finding: the Settings tab sets a minimum content width of 1165.5 px for
a typical install — wider than the 1024x660 window run.go asks for — so Fyne
silently widens the window on open and the user cannot drag it narrower. It also
grows with the length of the config-file path (1501 px for a 75-character one).
Two independent causes: the seven settingsControlWidth wrappers, which measure
identically to bare controls at every reachable width and only inflate MinSize,
and the Config JSON path label, the one value label in the tab without
truncation. Fixing both takes the floor to 993.3.
Also found: compactVBoxLayout is a byte-for-byte re-implementation of stock
layout.NewCustomPaddedVBoxLayout (identical geometry at every spacing tested);
minJobsSidebarWidth (400) never binds because the toolbar row already needs 448;
the negative row spacings are theme.InnerPadding() written as a magic number;
the History column widths truncate their own content on a scaled UI; and the
History table re-sorts its whole event slice once per cell — 126 sorts of a
300-element copy for one redraw.
Fifteen findings in all, each with a disposition (single fix or roadmap) and a
suggested order. fixedHeightLayout, which the roadmap singled out, is kept: no
stock layout forces an exact height, and the alternative depends on the parent
staying a Border.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Two planned items larger than a single fix:
- Import/export jobs as a cron table, with the open questions that must be
settled first: the job fields crontab has no slot for, "@every" not being
valid crontab, splitting Command/Arguments per platform, which lines to
skip on import, and merge semantics.
- A focused pass over the ui package's custom layouts and tuned constants —
negative spacings that cancel widget padding, pixel sizes that ignore theme
metrics, a layout with one call site, and settings_view.go past the size
guideline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Close the Unreleased section as 0.14.0 and fill the gaps in it: the Folder
caption moving onto the filter row, the padding around the Settings button
row, the Truncation-field refactor, the Docker build cache mount, and the
review/standards documents added since 0.13.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reading a log file meant copying the configured path out of Settings and
pasting it into a file manager. The Logs directory row now carries an Open
button beside Browse that reveals the folder directly.
The new src/platform/filemanager package holds the platform split — explorer
on Windows, xdg-open on Linux, an "unsupported" error elsewhere — and starts
the handler without waiting on it, since Explorer exits non-zero even after it
opens the window and blocking would stall the UI thread. A missing path, a
path that is a file, and a handler that will not start are all reported to the
user; the logs directory does not exist until the first run, so that case is
reachable.
The button opens whatever the field currently holds rather than the saved
config, so an edit can be checked before Save. Resolving a relative directory
against the application folder is the store's rule, so resolveConfiguredDir is
now exported as storage.ResolveConfiguredDir instead of being duplicated in
the UI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>