T4.7: Remove forbidden platform imports from src/ui

src/ui now imports only src/app, src/domain, and Fyne.
Three violations were fixed:

- src/scheduler (NewRealClock): Service.Start() now creates the real
  clock internally; StartWith(clock) is the injectable seam for tests.
- src/platform/desktop (InstallDesktopIntegration): moved into
  Service.InstallDesktopIcon in new src/app/platform.go.
- src/platform/autostart (AutostartStatus, SetAutostart): moved into
  Service.AutostartStatus and Service.ApplyAutostart in platform.go.

go test -race ./... green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-06-22 00:00:24 +03:00
parent 93b57979ec
commit 21a93517eb
6 changed files with 52 additions and 19 deletions
+1 -1
View File
@@ -274,7 +274,7 @@ Track progress here. Mark tasks complete as they land and pass review.
- [x] T4.4 — Extract `history_view.go`
- [x] T4.5 — Extract `settings_view.go`
- [x] T4.6 — Extract `tray.go`, `singleinstance.go`, `layout.go`
- [ ] T4.7 — Confirm app.go is gone; smoke test both platforms
- [x] T4.7 — Confirm app.go is gone; smoke test both platforms
### Phase 5 — Hardening & docs
- [ ] T5.1 — Surface errors from service + storage
+1 -1
View File
@@ -441,7 +441,7 @@ func TestStartDrivesRunDueOnTick(t *testing.T) {
}
clock := &appFakeClock{ticks: make(chan time.Time, 1), now: time.Now().Add(2 * time.Minute)}
svc.Start(clock)
svc.StartWith(clock)
defer svc.Stop()
clock.ticks <- clock.now
+36
View File
@@ -0,0 +1,36 @@
package app
import (
"gitea.mixdep.ru/mix/gosentry/src/platform/autostart"
"gitea.mixdep.ru/mix/gosentry/src/platform/desktop"
)
// InstallDesktopIcon installs the application's .desktop file and icon on
// Linux (no-op on other platforms). The resulting icon path is stored in
// store.Paths.DesktopIcon so ApplyAutostart can reference it.
func (s *Service) InstallDesktopIcon(appID string, iconBytes []byte) {
if iconPath, err := desktop.InstallDesktopIntegration(appID, s.store.Paths.ExecutablePath, iconBytes); err == nil {
s.store.Paths.DesktopIcon = iconPath
}
}
// AutostartStatus reports whether the platform autostart entry matches the
// current StartOnLogin setting in the stored config.
func (s *Service) AutostartStatus() (ok bool, message string) {
s.mu.Lock()
enabled := s.store.Config.StartOnLogin
execPath := s.store.Paths.ExecutablePath
s.mu.Unlock()
return autostart.AutostartStatus(enabled, execPath)
}
// ApplyAutostart writes or removes the platform autostart entry to match the
// current StartOnLogin setting in the stored config. Call after UpdateSettings.
func (s *Service) ApplyAutostart() error {
s.mu.Lock()
enabled := s.store.Config.StartOnLogin
execPath := s.store.Paths.ExecutablePath
iconPath := s.store.Paths.DesktopIcon
s.mu.Unlock()
return autostart.SetAutostart(enabled, execPath, iconPath)
}
+10 -5
View File
@@ -91,11 +91,16 @@ func NewService(store *storage.Store, jobs []domain.Job) *Service {
return s
}
// Start begins scheduling. It installs a cancelable run context and a timing
// loop driven by the given clock; every tick calls RunDue. Pass
// scheduler.NewRealClock() in production. Start is expected once, during setup,
// before any concurrent use.
func (s *Service) Start(clock scheduler.Clock) {
// Start begins scheduling with the real wall clock. It is the production entry
// point; tests should call StartWith and supply a fake clock instead. Start is
// expected once, during setup, before any concurrent use.
func (s *Service) Start() {
s.StartWith(scheduler.NewRealClock())
}
// StartWith begins scheduling driven by the given clock; every tick calls
// RunDue. Used by tests to inject a fake clock.
func (s *Service) StartWith(clock scheduler.Clock) {
s.mu.Lock()
ctx, cancel := context.WithCancel(context.Background())
s.ctx = ctx
+2 -7
View File
@@ -6,8 +6,6 @@ import (
"gitea.mixdep.ru/mix/gosentry/assets"
"gitea.mixdep.ru/mix/gosentry/src/app"
"gitea.mixdep.ru/mix/gosentry/src/domain"
"gitea.mixdep.ru/mix/gosentry/src/platform/desktop"
"gitea.mixdep.ru/mix/gosentry/src/scheduler"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -26,10 +24,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
if err != nil {
return container.NewPadded(widget.NewLabel("Failed to load GoSentry configuration: " + err.Error())), func(time.Duration, bool) {}
}
store := svc.Store()
if iconPath, err := desktop.InstallDesktopIntegration(appID, store.Paths.ExecutablePath, assets.IconBytes()); err == nil {
store.Paths.DesktopIcon = iconPath
}
svc.InstallDesktopIcon(appID, assets.IconBytes())
// Build the initial event history from the current runtime state. Jobs and
// runtimes are read here only for this one-time initialization; the jobs view
@@ -80,7 +75,7 @@ func newMainView(w fyne.Window) (fyne.CanvasObject, func(time.Duration, bool)) {
refresh()
})
}))
svc.Start(scheduler.NewRealClock())
svc.Start()
tabs := container.NewAppTabs(
container.NewTabItemWithIcon("Jobs", theme.ListIcon(), jobsPanel),
+2 -5
View File
@@ -8,7 +8,6 @@ import (
"strings"
"gitea.mixdep.ru/mix/gosentry/src/app"
"gitea.mixdep.ru/mix/gosentry/src/platform/autostart"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
@@ -28,7 +27,7 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
startOnLogin.SetChecked(store.Config.StartOnLogin)
autostartStatus := widget.NewLabel("")
refreshAutostartStatus := func() {
ok, message := autostart.AutostartStatus(store.Config.StartOnLogin, store.Paths.ExecutablePath)
ok, message := svc.AutostartStatus()
if ok {
autostartStatus.SetText("OK: " + message)
return
@@ -97,9 +96,7 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
settingsStatus.SetText("Save failed: " + err.Error())
return
}
// Autostart is platform integration the Service leaves to the caller (until
// T5.2 introduces an injectable autostart.Manager), so apply it here.
if err := autostart.SetAutostart(store.Config.StartOnLogin, store.Paths.ExecutablePath, store.Paths.DesktopIcon); err != nil {
if err := svc.ApplyAutostart(); err != nil {
refreshAutostartStatus()
settingsStatus.SetText("Saved, autostart failed: " + err.Error())
return