Compare commits
3 Commits
e482e3261c
...
648325a690
| Author | SHA1 | Date | |
|---|---|---|---|
| 648325a690 | |||
| 27927f3ab1 | |||
| 276539c383 |
@@ -74,7 +74,7 @@ portable application: moving the program folder also moves its configuration.
|
|||||||
"execution_mode": "parallel",
|
"execution_mode": "parallel",
|
||||||
"overlap_policy": "skip",
|
"overlap_policy": "skip",
|
||||||
"default_timeout_seconds": 0,
|
"default_timeout_seconds": 0,
|
||||||
"theme": "default",
|
"theme": "gosentry",
|
||||||
"job_list_view": "detailed"
|
"job_list_view": "detailed"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -126,22 +126,55 @@ include the run timestamp and job name:
|
|||||||
|
|
||||||
## Schedules
|
## Schedules
|
||||||
|
|
||||||
Interval schedules using Go duration syntax:
|
GoSentry accepts two schedule forms: fixed `@every` intervals and standard
|
||||||
|
5-field cron expressions.
|
||||||
|
|
||||||
|
### `@every` intervals
|
||||||
|
|
||||||
|
Write `@every` followed by a [Go duration](https://pkg.go.dev/time#ParseDuration)
|
||||||
|
— a positive number with a unit suffix. Units can be combined in one value:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@every 10s
|
@every 10s every 10 seconds
|
||||||
@every 5m
|
@every 5m every 5 minutes
|
||||||
@every 1h30m
|
@every 1h every hour
|
||||||
|
@every 1h30m every hour and a half (same as @every 90m)
|
||||||
|
@every 2h45m10s hours, minutes, and seconds combined
|
||||||
```
|
```
|
||||||
|
|
||||||
Standard 5-field cron expressions:
|
Supported units:
|
||||||
|
|
||||||
|
| Unit | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `ns` | nanoseconds |
|
||||||
|
| `us`, `µs` | microseconds |
|
||||||
|
| `ms` | milliseconds |
|
||||||
|
| `s` | seconds |
|
||||||
|
| `m` | minutes |
|
||||||
|
| `h` | hours |
|
||||||
|
|
||||||
|
`@every` does **not** support days, weeks, months, or years — those follow a
|
||||||
|
calendar, not a fixed interval. For “every day at 02:00”, “on the 1st of each
|
||||||
|
month”, or “once a year”, use a cron expression (below).
|
||||||
|
|
||||||
|
The scheduler checks due jobs once per second, so values shorter than `1s` are
|
||||||
|
accepted but will not fire faster than once a second.
|
||||||
|
|
||||||
|
### Cron expressions
|
||||||
|
|
||||||
|
Five fields: minute, hour, day-of-month, month, day-of-week.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
*/5 * * * * every five minutes
|
*/5 * * * * every five minutes
|
||||||
0 2 * * * every day at 02:00
|
0 2 * * * every day at 02:00
|
||||||
30 9 * * 1-5 weekdays at 09:30
|
30 9 * * 1-5 weekdays at 09:30
|
||||||
|
0 0 1 * * first day of every month at midnight
|
||||||
|
0 0 1 1 * every year on 1 January at midnight
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Named descriptors are also accepted: `@hourly`, `@daily`, `@weekly`,
|
||||||
|
`@monthly`, `@yearly` (and `@annually`, `@midnight`).
|
||||||
|
|
||||||
## Using The App
|
## Using The App
|
||||||
|
|
||||||
1. Start GoSentry.
|
1. Start GoSentry.
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ dragged.**
|
|||||||
|
|
||||||
**Settings:**
|
**Settings:**
|
||||||
|
|
||||||
|
- **The branded GoSentry theme is now the default.** Fresh installs, the
|
||||||
|
**Defaults** button, and configs that omit `theme` all open in the teal/amber
|
||||||
|
look; users who prefer Fyne's built-in theme can still pick **Default** in
|
||||||
|
Settings.
|
||||||
- The **Save / Cancel / Restore defaults** row sits 4 px from the left edge, as
|
- The **Save / Cancel / Restore defaults** row sits 4 px from the left edge, as
|
||||||
its layout always intended, rather than 8.
|
its layout always intended, rather than 8.
|
||||||
- The caption column is as wide as the widest caption instead of a fixed width,
|
- The caption column is as wide as the widest caption instead of a fixed width,
|
||||||
@@ -51,9 +55,16 @@ dragged.**
|
|||||||
the rows above have to give but a dropdown — which paints its box out to the
|
the rows above have to give but a dropdown — which paints its box out to the
|
||||||
row's edge — does not, so the gap collapsed to about a pixel. The Theme row
|
row's edge — does not, so the gap collapsed to about a pixel. The Theme row
|
||||||
now keeps the same gap the checkbox rows have.
|
now keeps the same gap the checkbox rows have.
|
||||||
|
- The **About** repository link points at GitHub (`mixeme/gosentry`) instead of
|
||||||
|
the private Gitea mirror.
|
||||||
|
|
||||||
**Documentation:**
|
**Documentation:**
|
||||||
|
|
||||||
|
- The **README Schedules** section now documents `@every` in full: supported Go
|
||||||
|
duration units (`ns` through `h`), combined values such as `1h30m`, the link to
|
||||||
|
`time.ParseDuration`, the fact that days/months/years belong in cron rather
|
||||||
|
than `@every`, the one-second scheduler tick floor, cron examples for monthly
|
||||||
|
and yearly runs, and the `@hourly`/`@daily`/… descriptors.
|
||||||
- The **README** describes the application that exists. Its `gosentry.json`
|
- The **README** describes the application that exists. Its `gosentry.json`
|
||||||
sample was three keys short of what the app writes on first run, which made
|
sample was three keys short of what the app writes on first run, which made
|
||||||
the one file the user is invited to hand-edit the least accurate thing in the
|
the one file the user is invited to hand-edit the least accurate thing in the
|
||||||
|
|||||||
+1
-1
@@ -514,7 +514,7 @@ Tests the branded theme and the stored theme choice.
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| `TestGoSentryThemeBrandColors` | Verifies the brand colors land on the semantically correct `ColorName`s in both the light and dark variants. |
|
| `TestGoSentryThemeBrandColors` | Verifies the brand colors land on the semantically correct `ColorName`s in both the light and dark variants. |
|
||||||
| `TestGoSentryThemeDelegatesUnbrandedColors` | Verifies unbranded color names fall through to the base theme rather than rendering transparent. |
|
| `TestGoSentryThemeDelegatesUnbrandedColors` | Verifies unbranded color names fall through to the base theme rather than rendering transparent. |
|
||||||
| `TestThemeForChoice` | Verifies the GoSentry choice yields the branded primary and every other value — including the empty legacy one — yields the default theme. |
|
| `TestThemeForChoice` | Verifies the GoSentry choice and the empty legacy value yield the branded primary; only the explicit default choice yields Fyne's built-in theme. |
|
||||||
| `TestThemeLabelRoundTrip` | Verifies the dropdown labels round-trip and that the empty value maps to the Default label rather than a blank option. |
|
| `TestThemeLabelRoundTrip` | Verifies the dropdown labels round-trip and that the empty value maps to the Default label rather than a blank option. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -481,8 +481,8 @@ func validateConfig(config domain.Config) error {
|
|||||||
if config.DefaultTimeoutSeconds < 0 {
|
if config.DefaultTimeoutSeconds < 0 {
|
||||||
return errors.New("default timeout must not be negative (0 means no timeout)")
|
return errors.New("default timeout must not be negative (0 means no timeout)")
|
||||||
}
|
}
|
||||||
// Empty Theme is accepted and normalized to the default on load, so older
|
// Empty Theme is accepted and normalized to the branded theme on load, so
|
||||||
// configs (and hand-built ones) stay valid without an explicit theme.
|
// older configs (and hand-built ones) stay valid without an explicit theme.
|
||||||
if config.Theme != "" && config.Theme != domain.ThemeDefault && config.Theme != domain.ThemeGoSentry {
|
if config.Theme != "" && config.Theme != domain.ThemeDefault && config.Theme != domain.ThemeGoSentry {
|
||||||
return errors.New("theme must be 'default' or 'gosentry'")
|
return errors.New("theme must be 'default' or 'gosentry'")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ type Config struct {
|
|||||||
// omitempty would hide a deliberate choice from the hand-editable config.
|
// omitempty would hide a deliberate choice from the hand-editable config.
|
||||||
DefaultTimeoutSeconds int `json:"default_timeout_seconds"`
|
DefaultTimeoutSeconds int `json:"default_timeout_seconds"`
|
||||||
Paused bool `json:"paused,omitempty"`
|
Paused bool `json:"paused,omitempty"`
|
||||||
// Theme selects the visual appearance. Empty is treated as ThemeDefault so
|
// Theme selects the visual appearance. Empty is treated as ThemeGoSentry so
|
||||||
// configs written before this field existed keep the original look.
|
// configs written before this field existed pick up the branded look.
|
||||||
Theme Theme `json:"theme,omitempty"`
|
Theme Theme `json:"theme,omitempty"`
|
||||||
// JobListView selects the Jobs list density. Empty is treated as
|
// JobListView selects the Jobs list density. Empty is treated as
|
||||||
// JobListViewDetailed so configs written before this field existed keep the
|
// JobListViewDetailed so configs written before this field existed keep the
|
||||||
@@ -110,7 +110,7 @@ func DefaultConfig() Config {
|
|||||||
NotifyOnFailure: true,
|
NotifyOnFailure: true,
|
||||||
ExecutionMode: ExecutionModeParallel,
|
ExecutionMode: ExecutionModeParallel,
|
||||||
OverlapPolicy: OverlapPolicySkip,
|
OverlapPolicy: OverlapPolicySkip,
|
||||||
Theme: ThemeDefault,
|
Theme: ThemeGoSentry,
|
||||||
JobListView: JobListViewDetailed,
|
JobListView: JobListViewDetailed,
|
||||||
DefaultTimeoutSeconds: 0,
|
DefaultTimeoutSeconds: 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ func loadOrCreateConfig(paths Paths) (domain.Config, error) {
|
|||||||
// the setting impossible to persist. Negative values are rejected by
|
// the setting impossible to persist. Negative values are rejected by
|
||||||
// app.validateConfig before they can be saved.
|
// app.validateConfig before they can be saved.
|
||||||
if config.Theme == "" {
|
if config.Theme == "" {
|
||||||
config.Theme = domain.ThemeDefault
|
config.Theme = domain.ThemeGoSentry
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,8 +171,8 @@ func TestLoadOrCreateConfigCreatesDefaultsOnFirstRun(t *testing.T) {
|
|||||||
if got.DefaultTimeoutSeconds != 0 {
|
if got.DefaultTimeoutSeconds != 0 {
|
||||||
t.Errorf("default DefaultTimeoutSeconds = %d, want 0 (no timeout)", got.DefaultTimeoutSeconds)
|
t.Errorf("default DefaultTimeoutSeconds = %d, want 0 (no timeout)", got.DefaultTimeoutSeconds)
|
||||||
}
|
}
|
||||||
if got.Theme != domain.ThemeDefault {
|
if got.Theme != domain.ThemeGoSentry {
|
||||||
t.Errorf("default Theme = %q, want %q", got.Theme, domain.ThemeDefault)
|
t.Errorf("default Theme = %q, want %q", got.Theme, domain.ThemeGoSentry)
|
||||||
}
|
}
|
||||||
if got.JobListView != domain.JobListViewDetailed {
|
if got.JobListView != domain.JobListViewDetailed {
|
||||||
t.Errorf("default JobListView = %q, want %q", got.JobListView, domain.JobListViewDetailed)
|
t.Errorf("default JobListView = %q, want %q", got.JobListView, domain.JobListViewDetailed)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
const projectRepositoryURL = "https://gitea.mixdep.ru/mix/gosentry"
|
const projectRepositoryURL = "https://github.com/mixeme/gosentry"
|
||||||
|
|
||||||
// settingsCaptions lists every settingsRow caption in the tab, in no
|
// settingsCaptions lists every settingsRow caption in the tab, in no
|
||||||
// particular order. settingsView measures this once with captionColumnWidth
|
// particular order. settingsView measures this once with captionColumnWidth
|
||||||
@@ -263,11 +263,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func themeLabel(choice domain.Theme) string {
|
func themeLabel(choice domain.Theme) string {
|
||||||
if choice == domain.ThemeGoSentry {
|
if choice == domain.ThemeDefault {
|
||||||
return themeLabelGoSentry
|
|
||||||
}
|
|
||||||
return themeLabelDefault
|
return themeLabelDefault
|
||||||
}
|
}
|
||||||
|
return themeLabelGoSentry
|
||||||
|
}
|
||||||
|
|
||||||
func themeFromLabel(label string) domain.Theme {
|
func themeFromLabel(label string) domain.Theme {
|
||||||
if label == themeLabelGoSentry {
|
if label == themeLabelGoSentry {
|
||||||
|
|||||||
+6
-6
@@ -103,15 +103,15 @@ func (t gosentryTheme) Font(style fyne.TextStyle) fyne.Resource { return t.base.
|
|||||||
func (t gosentryTheme) Icon(name fyne.ThemeIconName) fyne.Resource { return t.base.Icon(name) }
|
func (t gosentryTheme) Icon(name fyne.ThemeIconName) fyne.Resource { return t.base.Icon(name) }
|
||||||
func (t gosentryTheme) Size(name fyne.ThemeSizeName) float32 { return t.base.Size(name) }
|
func (t gosentryTheme) Size(name fyne.ThemeSizeName) float32 { return t.base.Size(name) }
|
||||||
|
|
||||||
// themeFor maps a stored Theme choice to a concrete fyne.Theme. Anything other
|
// themeFor maps a stored Theme choice to a concrete fyne.Theme. Only the
|
||||||
// than the explicit GoSentry choice (including the empty/legacy value) keeps
|
// explicit default choice keeps Fyne's built-in theme; everything else
|
||||||
// Fyne's built-in theme.
|
// (including the empty/legacy value) uses the branded GoSentry theme.
|
||||||
func themeFor(choice domain.Theme) fyne.Theme {
|
func themeFor(choice domain.Theme) fyne.Theme {
|
||||||
if choice == domain.ThemeGoSentry {
|
if choice == domain.ThemeDefault {
|
||||||
return newGoSentryTheme()
|
|
||||||
}
|
|
||||||
return theme.DefaultTheme()
|
return theme.DefaultTheme()
|
||||||
}
|
}
|
||||||
|
return newGoSentryTheme()
|
||||||
|
}
|
||||||
|
|
||||||
// applyTheme installs the theme for the given choice on the running app. Fyne
|
// applyTheme installs the theme for the given choice on the running app. Fyne
|
||||||
// refreshes every canvas when the theme changes, so this works both at startup
|
// refreshes every canvas when the theme changes, so this works both at startup
|
||||||
|
|||||||
+13
-13
@@ -54,24 +54,24 @@ func TestGoSentryThemeDelegatesUnbrandedColors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// themeFor maps the stored choice to the right theme: the GoSentry choice yields
|
// themeFor maps the stored choice to the right theme: the GoSentry choice and the
|
||||||
// the branded teal primary; every other value (including the empty legacy value)
|
// empty legacy value yield the branded teal primary; only the explicit default
|
||||||
// yields the default theme, whose primary is not the brand teal.
|
// choice yields Fyne's built-in theme.
|
||||||
func TestThemeForChoice(t *testing.T) {
|
func TestThemeForChoice(t *testing.T) {
|
||||||
gosentry := themeFor(domain.ThemeGoSentry)
|
for _, choice := range []domain.Theme{domain.ThemeGoSentry, ""} {
|
||||||
if got := gosentry.Color(theme.ColorNamePrimary, theme.VariantLight); got != brandTeal {
|
branded := themeFor(choice)
|
||||||
t.Errorf("themeFor(gosentry) primary = %v, want brand teal %v", got, brandTeal)
|
if got := branded.Color(theme.ColorNamePrimary, theme.VariantLight); got != brandTeal {
|
||||||
|
t.Errorf("themeFor(%q) primary = %v, want brand teal %v", choice, got, brandTeal)
|
||||||
}
|
}
|
||||||
for _, choice := range []domain.Theme{domain.ThemeDefault, ""} {
|
}
|
||||||
def := themeFor(choice)
|
def := themeFor(domain.ThemeDefault)
|
||||||
if got := def.Color(theme.ColorNamePrimary, theme.VariantLight); got == brandTeal {
|
if got := def.Color(theme.ColorNamePrimary, theme.VariantLight); got == brandTeal {
|
||||||
t.Errorf("themeFor(%q) should not use the brand teal primary", choice)
|
t.Errorf("themeFor(default) should not use the brand teal primary")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dropdown label helpers must round-trip, and the empty/legacy value must map
|
// The dropdown label helpers must round-trip, and the empty/legacy value must map
|
||||||
// to the Default label so the select never shows a blank option.
|
// to the GoSentry label so the select never shows a blank option.
|
||||||
func TestThemeLabelRoundTrip(t *testing.T) {
|
func TestThemeLabelRoundTrip(t *testing.T) {
|
||||||
if got := themeFromLabel(themeLabel(domain.ThemeGoSentry)); got != domain.ThemeGoSentry {
|
if got := themeFromLabel(themeLabel(domain.ThemeGoSentry)); got != domain.ThemeGoSentry {
|
||||||
t.Errorf("round-trip gosentry = %q", got)
|
t.Errorf("round-trip gosentry = %q", got)
|
||||||
@@ -79,7 +79,7 @@ func TestThemeLabelRoundTrip(t *testing.T) {
|
|||||||
if got := themeFromLabel(themeLabel(domain.ThemeDefault)); got != domain.ThemeDefault {
|
if got := themeFromLabel(themeLabel(domain.ThemeDefault)); got != domain.ThemeDefault {
|
||||||
t.Errorf("round-trip default = %q", got)
|
t.Errorf("round-trip default = %q", got)
|
||||||
}
|
}
|
||||||
if got := themeLabel(""); got != themeLabelDefault {
|
if got := themeLabel(""); got != themeLabelGoSentry {
|
||||||
t.Errorf("empty theme label = %q, want %q", got, themeLabelDefault)
|
t.Errorf("empty theme label = %q, want %q", got, themeLabelGoSentry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user