Refactoring complete: v0.4.0 architectural milestone (#1)

## Summary

Completed Phase 5 refactoring and reached the target architecture.

**Architectural milestone achieved:**
- Service layer owns all state and is the sole writer
- UI is a thin Fyne view, all widget updates marshaled via `fyne.Do`
- Core engines are stateless and injectable
- Domain types are pure (no `yaml:"-"` fields)
- Full module builds and `go vet ./...` clean

## Changes

- Bump version: 0.3.6 → 0.4.0
- Update CHANGELOG with Phase 5 summary
- Add ROADMAP "Refactoring Follow-Ups" section

## Known follow-up work

1. **Linux test build broken** — `runner_test.go` needs `//go:build windows` tag
2. **File-size limits exceeded** — `operations.go` (486 lines), `jobs_view.go` (415 lines)

See ROADMAP.md for details.

---------

Co-authored-by: mixeme <mix.public@ya.ru>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-06-22 08:05:10 +03:00
parent d24211cab2
commit 01fd572a89
74 changed files with 5424 additions and 2712 deletions
+55
View File
@@ -0,0 +1,55 @@
package runner
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"unicode"
"gitea.mixdep.ru/mix/gosentry/src/domain"
)
func writeRunLog(logsDir string, job domain.Job, trigger string, state string, detail string, output string, started time.Time) string {
if strings.TrimSpace(logsDir) == "" {
return ""
}
if err := os.MkdirAll(logsDir, 0o755); err != nil {
return ""
}
// The timestamp comes first so a plain directory listing is naturally sorted
// by run time. The job name is included for human scanning, but sanitized to
// avoid characters that are invalid on Windows or awkward on shells.
fileName := started.Format("20060102-150405") + "_" + sanitizeFileName(job.Name) + ".log"
path := filepath.Join(logsDir, fileName)
content := fmt.Sprintf("time: %s\njob_id: %d\njob_name: %s\ntrigger: %s\nstate: %s\ndetail: %s\ncommand: %s\narguments: %s\nsuccess_exit_codes: %s\nstart_only: %t\n\n%s\n",
started.Format("2006-01-02 15:04:05"), job.ID, job.Name, trigger, state, detail, job.Command, logArguments(job.Arguments), successExitCodesText(job), job.StartOnly, output)
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
return ""
}
return path
}
func sanitizeFileName(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return "job"
}
var builder strings.Builder
for _, r := range name {
switch {
case unicode.IsLetter(r), unicode.IsDigit(r):
builder.WriteRune(r)
case r == '-', r == '_':
builder.WriteRune(r)
default:
builder.WriteRune('_')
}
}
result := strings.Trim(builder.String(), "_")
if result == "" {
return "job"
}
return result
}