feat(ui): two-column settings/details, compact job list, manual run while paused
- Allow manual "Run now" while the scheduler is paused: pause now stops only automatic scheduled runs (RunDue), not the user's explicit action. Drop the paused guard in Service.RunNow and the UI pause dialog; update tests. - Cap the details metadata caption width via a new captionValueLayout so a wider window feeds extra space to the value column instead of the short caption. - Reorganize the Settings tab into two columns (Application+Queue / Storage+About) with Save spanning the full width; move the Autostart status onto its own line. - Condense the Jobs list rows with compactVBoxLayout to fit more jobs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-14
@@ -335,21 +335,27 @@ func TestRunNowRefusedWhileAlreadyRunning(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNowRefusedWhilePaused(t *testing.T) {
|
||||
func TestRunNowAllowedWhilePaused(t *testing.T) {
|
||||
svc := newTempService(t, []domain.Job{{ID: 1, Name: "A", Schedule: "@every 1m", Command: "echo", Enabled: true}})
|
||||
var ran bool
|
||||
done := make(chan struct{}, 1)
|
||||
svc.runJob = func(context.Context, *domain.Job, string, string) domain.RunRecord {
|
||||
ran = true
|
||||
return domain.RunRecord{}
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return domain.RunRecord{State: "Success"}
|
||||
}
|
||||
if err := svc.SetGlobalPause(true); err != nil {
|
||||
t.Fatalf("SetGlobalPause: %v", err)
|
||||
}
|
||||
if err := svc.RunNow(1); err == nil {
|
||||
t.Error("expected RunNow to be refused while paused")
|
||||
// Pause stops only scheduled runs; an explicit manual run is still allowed.
|
||||
if err := svc.RunNow(1); err != nil {
|
||||
t.Fatalf("RunNow should be allowed while paused: %v", err)
|
||||
}
|
||||
if ran {
|
||||
t.Error("runner must not be invoked while paused")
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("runner was not invoked for a manual run while paused")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,23 +592,32 @@ func TestServiceRebuiltFromPausedStoreStartsPaused(t *testing.T) {
|
||||
svc2 := NewService(svc.store, svc.Jobs())
|
||||
|
||||
var ran int32
|
||||
runStarted := make(chan struct{}, 1)
|
||||
svc2.runJob = func(context.Context, *domain.Job, string, string) domain.RunRecord {
|
||||
atomic.AddInt32(&ran, 1)
|
||||
select {
|
||||
case runStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return domain.RunRecord{}
|
||||
}
|
||||
|
||||
// RunDue must not start any job while paused.
|
||||
// RunDue must not start any job while paused: the scheduler stays paused after
|
||||
// a restart that rebuilt the service from a paused store.
|
||||
svc2.RunDue(time.Now().Add(2 * time.Minute))
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if atomic.LoadInt32(&ran) != 0 {
|
||||
t.Error("RunDue ran a job on a service rebuilt from a paused store")
|
||||
}
|
||||
|
||||
// RunNow must be refused.
|
||||
if err := svc2.RunNow(1); err == nil {
|
||||
t.Error("RunNow should be refused on a service rebuilt from a paused store")
|
||||
// A manual RunNow is still allowed while paused — pause only stops the
|
||||
// scheduler, not the user's explicit action.
|
||||
if err := svc2.RunNow(1); err != nil {
|
||||
t.Errorf("RunNow should be allowed while paused: %v", err)
|
||||
}
|
||||
if atomic.LoadInt32(&ran) != 0 {
|
||||
t.Error("runner was invoked despite global pause")
|
||||
select {
|
||||
case <-runStarted:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("manual run was not started on a service rebuilt from a paused store")
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -11,8 +11,9 @@ import (
|
||||
"gitea.mixdep.ru/mix/gosentry/src/runner"
|
||||
)
|
||||
|
||||
// RunNow starts a manual run of a job. It refuses to run while globally paused —
|
||||
// the pause is an emergency stop for all execution — and will not start a job
|
||||
// RunNow starts a manual run of a job. Global pause stops only the scheduler's
|
||||
// automatic runs (see RunDue), so a manual "Run now" is allowed even while
|
||||
// paused — it is the user's explicit, one-off action. It will not start a job
|
||||
// that is already running. In sequential execution mode it also refuses while
|
||||
// any other job is running, so a manual run never breaks the one-at-a-time
|
||||
// guarantee. The run itself happens on a background goroutine that records the
|
||||
@@ -21,10 +22,6 @@ import (
|
||||
// "Running" status), not the run's own outcome.
|
||||
func (s *Service) RunNow(id int) error {
|
||||
s.mu.Lock()
|
||||
if s.paused {
|
||||
s.mu.Unlock()
|
||||
return errors.New("scheduler is paused")
|
||||
}
|
||||
job := s.findByIDLocked(id)
|
||||
if job == nil {
|
||||
s.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user