Refactoring complete: v0.4.0 architectural milestone #1
+1
-1
@@ -264,7 +264,7 @@ Track progress here. Mark tasks complete as they land and pass review.
|
|||||||
- [x] T3.2 — Add `src/app/events.go`; Event types + Observer
|
- [x] T3.2 — Add `src/app/events.go`; Event types + Observer
|
||||||
- [x] T3.3 — Add state-mutating operations to service
|
- [x] T3.3 — Add state-mutating operations to service
|
||||||
- [x] T3.4 — Convert `scheduler` to use service; inject Clock
|
- [x] T3.4 — Convert `scheduler` to use service; inject Clock
|
||||||
- [ ] T3.5 — Move display helpers to `src/app/format.go`
|
- [x] T3.5 — Move display helpers to `src/app/format.go`
|
||||||
- [ ] T3.6 — Add `src/app` unit tests (no Fyne)
|
- [ ] T3.6 — Add `src/app` unit tests (no Fyne)
|
||||||
|
|
||||||
### Phase 4 — Carve up the GUI
|
### Phase 4 — Carve up the GUI
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusText formats a job's current state for display: "Paused" if disabled,
|
||||||
|
// else its runtime LastState (Ready, Running, Success, etc).
|
||||||
|
func StatusText(j domain.Job, runtime *domain.JobRuntime) string {
|
||||||
|
if !j.Enabled {
|
||||||
|
return "Paused"
|
||||||
|
}
|
||||||
|
if runtime == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return runtime.LastState
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventText formats a run record for the History table, showing time, trigger,
|
||||||
|
// job name, outcome state, detail, and log file (if any).
|
||||||
|
func EventText(e domain.RunRecord) string {
|
||||||
|
trigger := e.Trigger
|
||||||
|
if trigger == "" {
|
||||||
|
trigger = "Unknown"
|
||||||
|
}
|
||||||
|
if e.LogFile != "" {
|
||||||
|
return fmt.Sprintf("%s %s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail, e.LogFile)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayFolder formats a job's folder for display: "(No folder)" if empty,
|
||||||
|
// else the trimmed folder name.
|
||||||
|
func DisplayFolder(folder string) string {
|
||||||
|
if strings.TrimSpace(folder) == "" {
|
||||||
|
return "(No folder)"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayArguments formats a job's arguments for display: "(none)" if empty,
|
||||||
|
// else the trimmed arguments.
|
||||||
|
func DisplayArguments(arguments string) string {
|
||||||
|
if strings.TrimSpace(arguments) == "" {
|
||||||
|
return "(none)"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplaySuccessExitCodes formats a job's success exit codes for display:
|
||||||
|
// "0" (the default) if empty, else the trimmed codes.
|
||||||
|
func DisplaySuccessExitCodes(codes string) string {
|
||||||
|
if strings.TrimSpace(codes) == "" {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(codes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayRunMode formats a job's execution mode: "Start only" or
|
||||||
|
// "Wait for completion".
|
||||||
|
func DisplayRunMode(job domain.Job) string {
|
||||||
|
if job.StartOnly {
|
||||||
|
return "Start only"
|
||||||
|
}
|
||||||
|
return "Wait for completion"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayInvocation formats a job's command and arguments for the jobs list,
|
||||||
|
// joining them with spacing and collapsing newlines in arguments to spaces.
|
||||||
|
func DisplayInvocation(job domain.Job) string {
|
||||||
|
if strings.TrimSpace(job.Arguments) == "" {
|
||||||
|
return job.Command
|
||||||
|
}
|
||||||
|
return job.Command + " " + strings.ReplaceAll(strings.TrimSpace(job.Arguments), "\n", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayIndex returns the position of jobIndex in the given slice of indexes,
|
||||||
|
// or 0 if not found.
|
||||||
|
func DisplayIndex(indexes []int, jobIndex int) int {
|
||||||
|
for display, index := range indexes {
|
||||||
|
if index == jobIndex {
|
||||||
|
return display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
+12
-76
@@ -211,8 +211,8 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
schedule := newJobDetailLabel(jobs[selected].Schedule)
|
schedule := newJobDetailLabel(jobs[selected].Schedule)
|
||||||
command := newJobDetailLabel(jobs[selected].Command)
|
command := newJobDetailLabel(jobs[selected].Command)
|
||||||
arguments := newJobDetailLabel(jobs[selected].Arguments)
|
arguments := newJobDetailLabel(jobs[selected].Arguments)
|
||||||
successExitCodes := newJobDetailLabel(displaySuccessExitCodes(jobs[selected].SuccessExitCodes))
|
successExitCodes := newJobDetailLabel(app.DisplaySuccessExitCodes(jobs[selected].SuccessExitCodes))
|
||||||
runMode := newJobDetailLabel(displayRunMode(jobs[selected]))
|
runMode := newJobDetailLabel(app.DisplayRunMode(jobs[selected]))
|
||||||
selectedRuntime := runtimeFor(selected)
|
selectedRuntime := runtimeFor(selected)
|
||||||
lastRun := newJobDetailLabel(selectedRuntime.LastRun)
|
lastRun := newJobDetailLabel(selectedRuntime.LastRun)
|
||||||
nextRun := newJobDetailLabel(selectedRuntime.NextRun)
|
nextRun := newJobDetailLabel(selectedRuntime.NextRun)
|
||||||
@@ -245,7 +245,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
},
|
},
|
||||||
func() fyne.CanvasObject { return widget.NewLabel("log") },
|
func() fyne.CanvasObject { return widget.NewLabel("log") },
|
||||||
func(id widget.ListItemID, item fyne.CanvasObject) {
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
||||||
item.(*widget.Label).SetText(eventText(selectedLogs[id]))
|
item.(*widget.Label).SetText(app.EventText(selectedLogs[id]))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -271,12 +271,12 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
current := jobs[selected]
|
current := jobs[selected]
|
||||||
runtime := runtimeFor(selected)
|
runtime := runtimeFor(selected)
|
||||||
title.SetText(current.Name)
|
title.SetText(current.Name)
|
||||||
folder.SetText(displayFolder(current.Folder))
|
folder.SetText(app.DisplayFolder(current.Folder))
|
||||||
schedule.SetText(current.Schedule)
|
schedule.SetText(current.Schedule)
|
||||||
command.SetText(current.Command)
|
command.SetText(current.Command)
|
||||||
arguments.SetText(displayArguments(current.Arguments))
|
arguments.SetText(app.DisplayArguments(current.Arguments))
|
||||||
successExitCodes.SetText(displaySuccessExitCodes(current.SuccessExitCodes))
|
successExitCodes.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes))
|
||||||
runMode.SetText(displayRunMode(current))
|
runMode.SetText(app.DisplayRunMode(current))
|
||||||
lastRun.SetText(runtime.LastRun)
|
lastRun.SetText(runtime.LastRun)
|
||||||
nextRun.SetText(runtime.NextRun)
|
nextRun.SetText(runtime.NextRun)
|
||||||
state.SetText(runtime.LastState)
|
state.SetText(runtime.LastState)
|
||||||
@@ -312,8 +312,8 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
name.SetText(current.Name)
|
name.SetText(current.Name)
|
||||||
// Keep each row compact: folder, schedule, and command are shown in one
|
// Keep each row compact: folder, schedule, and command are shown in one
|
||||||
// metadata line so the left pane stays useful even with many jobs.
|
// metadata line so the left pane stays useful even with many jobs.
|
||||||
meta.SetText(displayFolder(current.Folder) + " " + current.Schedule + " " + displayInvocation(current))
|
meta.SetText(app.DisplayFolder(current.Folder) + " " + current.Schedule + " " + app.DisplayInvocation(current))
|
||||||
status.SetText(statusText(current, runtimes[current.ID]))
|
status.SetText(app.StatusText(current, runtimes[current.ID]))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
list.OnSelected = func(id widget.ListItemID) {
|
list.OnSelected = func(id widget.ListItemID) {
|
||||||
@@ -366,7 +366,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
selected = indexOfID(jobs, created.ID)
|
selected = indexOfID(jobs, created.ID)
|
||||||
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
list.Select(displayIndex(filteredJobs, selected))
|
list.Select(app.DisplayIndex(filteredJobs, selected))
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -480,7 +480,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
|
|||||||
}
|
}
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
if selected >= 0 {
|
if selected >= 0 {
|
||||||
list.Select(displayIndex(filteredJobs, selected))
|
list.Select(app.DisplayIndex(filteredJobs, selected))
|
||||||
}
|
}
|
||||||
refresh()
|
refresh()
|
||||||
}, w)
|
}, w)
|
||||||
@@ -569,16 +569,6 @@ func (layout minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusText(j job, runtime *domain.JobRuntime) string {
|
|
||||||
if !j.Enabled {
|
|
||||||
return "Paused"
|
|
||||||
}
|
|
||||||
if runtime == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return runtime.LastState
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEvent(jobID int, jobName string, state string, detail string) event {
|
func newEvent(jobID int, jobName string, state string, detail string) event {
|
||||||
// Use the same timestamp shape as command run records so the History tab is
|
// Use the same timestamp shape as command run records so the History tab is
|
||||||
// visually consistent across startup, UI actions, manual runs, and schedules.
|
// visually consistent across startup, UI actions, manual runs, and schedules.
|
||||||
@@ -592,17 +582,6 @@ func newEvent(jobID int, jobName string, state string, detail string) event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func eventText(e event) string {
|
|
||||||
trigger := e.Trigger
|
|
||||||
if trigger == "" {
|
|
||||||
trigger = "Unknown"
|
|
||||||
}
|
|
||||||
if e.LogFile != "" {
|
|
||||||
return fmt.Sprintf("%s %s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail, e.LogFile)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s %s %s %s %s", e.Time, trigger, e.JobName, e.State, e.Detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectActivity(jobs []job, runtimes map[int]*domain.JobRuntime) []event {
|
func collectActivity(jobs []job, runtimes map[int]*domain.JobRuntime) []event {
|
||||||
var events []event
|
var events []event
|
||||||
for _, current := range jobs {
|
for _, current := range jobs {
|
||||||
@@ -689,49 +668,6 @@ func filterValue(folder string) string {
|
|||||||
return strings.TrimSpace(folder)
|
return strings.TrimSpace(folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayFolder(folder string) string {
|
|
||||||
if strings.TrimSpace(folder) == "" {
|
|
||||||
return "(" + noFolder + ")"
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayArguments(arguments string) string {
|
|
||||||
if strings.TrimSpace(arguments) == "" {
|
|
||||||
return "(none)"
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func displaySuccessExitCodes(codes string) string {
|
|
||||||
if strings.TrimSpace(codes) == "" {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(codes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayRunMode(current job) string {
|
|
||||||
if current.StartOnly {
|
|
||||||
return "Start only"
|
|
||||||
}
|
|
||||||
return "Wait for completion"
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayInvocation(current job) string {
|
|
||||||
if strings.TrimSpace(current.Arguments) == "" {
|
|
||||||
return current.Command
|
|
||||||
}
|
|
||||||
return current.Command + " " + strings.ReplaceAll(strings.TrimSpace(current.Arguments), "\n", " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func displayIndex(indexes []int, jobIndex int) int {
|
|
||||||
for display, index := range indexes {
|
|
||||||
if index == jobIndex {
|
|
||||||
return display
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
||||||
name := widget.NewEntry()
|
name := widget.NewEntry()
|
||||||
@@ -751,7 +687,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
|||||||
arguments.SetText(current.Arguments)
|
arguments.SetText(current.Arguments)
|
||||||
successExitCodes := widget.NewEntry()
|
successExitCodes := widget.NewEntry()
|
||||||
successExitCodes.SetPlaceHolder("0")
|
successExitCodes.SetPlaceHolder("0")
|
||||||
successExitCodes.SetText(displaySuccessExitCodes(current.SuccessExitCodes))
|
successExitCodes.SetText(app.DisplaySuccessExitCodes(current.SuccessExitCodes))
|
||||||
startOnly := widget.NewCheck("Start only, do not wait for exit", nil)
|
startOnly := widget.NewCheck("Start only, do not wait for exit", nil)
|
||||||
startOnly.SetChecked(current.StartOnly)
|
startOnly.SetChecked(current.StartOnly)
|
||||||
enabled := widget.NewCheck("Enabled", nil)
|
enabled := widget.NewCheck("Enabled", nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user