Compare commits

...

2 Commits

Author SHA1 Message Date
mixeme 554ce2b93a ui/app: per-job overlap policy in job dialog and details panel
- job_dialog.go: add overlap-policy widget.Select with "(Use global
  default)" → empty, "skip", and "queue" options; pre-selects the job's
  current value; on save maps the inherit label back to empty string.
- app/format.go: add DisplayOverlapPolicy(job, globalPolicy) — returns
  the policy name when overridden, else "skip/queue (global default)".
- ui/jobs_view.go: add "Overlap policy" detail row updated in
  updateDetails using DisplayOverlapPolicy.
- settings_view.go: rename "Overlap policy" label to "Default overlap
  policy" to clarify it is the global default jobs can override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 21:05:50 +03:00
mixeme 3f3b977fb0 app/run.go: resolve effective overlap policy per job in RunDue
RunDue now resolves each job's effective overlap policy via
effectiveOverlapPolicy (job value when set, else Config.OverlapPolicy)
instead of reading the global policy once per tick. An empty
Job.OverlapPolicy inherits the global default, so normalizeJobs continues
to leave the field untouched rather than backfilling it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:58:51 +03:00
5 changed files with 47 additions and 4 deletions
+10
View File
@@ -92,6 +92,16 @@ func DisplayStats(rt *domain.JobRuntime) string {
rt.RunCount, rt.FailCount, rt.LastDurationMS, rt.AvgDurationMS, rt.MaxDurationMS)
}
// DisplayOverlapPolicy formats a job's effective overlap policy for the details
// panel. When the job has its own policy it is shown as-is; when empty (inherit
// global), the global default is shown with "(global default)" appended.
func DisplayOverlapPolicy(job domain.Job, globalPolicy domain.OverlapPolicy) string {
if p := domain.OverlapPolicy(strings.TrimSpace(job.OverlapPolicy)); p != "" {
return string(p)
}
return string(globalPolicy) + " (global default)"
}
// DisplayIndex returns the position of jobIndex in the given slice of indexes,
// or 0 if not found.
func DisplayIndex(indexes []int, jobIndex int) int {
+14 -3
View File
@@ -67,7 +67,6 @@ func (s *Service) RunDue(now time.Time) {
var startErr error
if !s.paused {
sequential := s.store.Config.ExecutionMode == domain.ExecutionModeSequential
queue := s.store.Config.OverlapPolicy == domain.OverlapPolicyQueue
running := s.anyRunningLocked()
for index := range s.jobs {
job := &s.jobs[index]
@@ -77,8 +76,9 @@ func (s *Service) RunDue(now time.Time) {
}
if runtime.LastState == "Running" {
// The job came due again while its own run is still in flight.
// Apply the overlap policy and step past this occurrence.
if queue {
// Apply the effective overlap policy and step past this
// occurrence.
if s.effectiveOverlapPolicy(job) == domain.OverlapPolicyQueue {
runtime.Pending = true
}
s.advanceNextDueLocked(job, runtime, now)
@@ -165,6 +165,17 @@ func (s *Service) executeRun(ctx context.Context, jobCopy domain.Job, trigger st
s.emit(JobChanged{JobID: jobCopy.ID})
}
// effectiveOverlapPolicy resolves the overlap policy that actually governs a
// job: the job's own value when set, otherwise the global Config default. An
// empty Job.OverlapPolicy means "inherit the global default", which is why
// normalizeJobs leaves it empty rather than backfilling the configured value.
func (s *Service) effectiveOverlapPolicy(job *domain.Job) domain.OverlapPolicy {
if policy := domain.OverlapPolicy(strings.TrimSpace(job.OverlapPolicy)); policy != "" {
return policy
}
return s.store.Config.OverlapPolicy
}
// anyRunningLocked reports whether any loaded job is currently in the "Running"
// state. It backs the sequential-mode guards in RunNow and RunDue. The caller
// must hold mu.
+18
View File
@@ -13,6 +13,10 @@ import (
"fyne.io/fyne/v2/widget"
)
// overlapPolicyInherit is the display label used when a job should inherit the
// global overlap policy. It maps to an empty Job.OverlapPolicy on save.
const overlapPolicyInherit = "(Use global default)"
// showJobDialog opens a create/edit form for a single job. onSave is called
// with the populated job only when the user clicks Save and all fields pass
// validation.
@@ -40,6 +44,15 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
startOnly.SetChecked(current.StartOnly)
enabled := widget.NewCheck("Enabled", nil)
enabled.SetChecked(current.Enabled)
overlapSelect := widget.NewSelect(
[]string{overlapPolicyInherit, string(domain.OverlapPolicySkip), string(domain.OverlapPolicyQueue)},
nil,
)
overlapSelected := overlapPolicyInherit
if current.OverlapPolicy != "" {
overlapSelected = current.OverlapPolicy
}
overlapSelect.SetSelected(overlapSelected)
form := dialog.NewForm(
title,
@@ -52,6 +65,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
widget.NewFormItem("Command", commandRow),
widget.NewFormItem("Arguments", argumentsEntry),
widget.NewFormItem("", startOnly),
widget.NewFormItem("Overlap policy", overlapSelect),
widget.NewFormItem("", enabled),
},
func(saved bool) {
@@ -75,6 +89,10 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
current.Arguments = strings.TrimSpace(argumentsEntry.Text)
current.StartOnly = startOnly.Checked
current.Enabled = enabled.Checked
current.OverlapPolicy = overlapSelect.Selected
if current.OverlapPolicy == overlapPolicyInherit {
current.OverlapPolicy = ""
}
// The dialog only edits durable configuration. Runtime status is
// initialized (new jobs) or updated (edits) by the caller against the
// runtime map, keyed by job ID.
+4
View File
@@ -76,6 +76,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
nextRunLabel := newJobDetailLabel(selectedRuntime.NextRun)
stateLabel := newJobDetailLabel(selectedRuntime.LastState)
statsLabel := newJobDetailLabel(app.DisplayStats(selectedRuntime))
overlapPolicyLabel := newJobDetailLabel(app.DisplayOverlapPolicy(jobs[selected], svc.Store().Config.OverlapPolicy))
schedulerState := widget.NewLabel("Scheduler running")
commandOutput := widget.NewTextGrid()
commandOutput.SetText(selectedRuntime.Output)
@@ -112,6 +113,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
nextRunLabel.SetText("")
stateLabel.SetText("")
statsLabel.SetText("")
overlapPolicyLabel.SetText("")
commandOutput.SetText("")
selectedLogs = nil
return
@@ -125,6 +127,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
commandLabel.SetText(current.Command)
argumentsLabel.SetText(app.DisplayArguments(current.Arguments))
runModeLabel.SetText(app.DisplayRunMode(current))
overlapPolicyLabel.SetText(app.DisplayOverlapPolicy(current, svc.Store().Config.OverlapPolicy))
lastRunLabel.SetText(rt.LastRun)
nextRunLabel.SetText(rt.NextRun)
stateLabel.SetText(rt.LastState)
@@ -356,6 +359,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
detailRow("Command", commandLabel),
detailRow("Arguments", argumentsLabel),
detailRow("Run mode", runModeLabel),
detailRow("Overlap policy", overlapPolicyLabel),
detailRow("Last run", lastRunLabel),
detailRow("Next run", nextRunLabel),
detailRow("State", stateLabel),
+1 -1
View File
@@ -126,7 +126,7 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
widget.NewSeparator(),
widget.NewLabelWithStyle("Queue", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
settingsRow("Execution mode", container.New(minWidthLayout{width: settingsControlWidth}, executionModeSelect)),
settingsRow("Overlap policy", container.New(minWidthLayout{width: settingsControlWidth}, overlapPolicySelect)),
settingsRow("Default overlap policy", container.New(minWidthLayout{width: settingsControlWidth}, overlapPolicySelect)),
widget.NewSeparator(),
widget.NewLabelWithStyle("Storage", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
settingsRow("Config YAML", widget.NewLabel(store.Paths.ConfigPath)),