Files
gosentry/docs/ROADMAP.md
T
mix 1e3d14bef2 Log failure-notification timing and plan faster Windows toasts.
Append app-side delays to logs/notify-timing.log, add a PowerShell baseline
script (~773 ms), and track native WinRT toasts in ROADMAP.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 23:15:44 +03:00

206 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roadmap
This file tracks planned GoSentry work that is larger than a single bug fix.
Completed work is recorded in [CHANGELOG.md](CHANGELOG.md), not here.
## Open Items
### Faster Windows failure notifications
Fyne `SendNotification` on Windows does not call WinRT directly. Each toast
writes a short script to `%TEMP%` and runs it through a **new PowerShell
process** (`app/app_windows.go`), which typically adds **13 seconds** of cold
start before the toast appears. GoSentry's own path from run completion through
`SendNotification` is much smaller and is logged separately.
**Baseline (2026-08-05, `scripts/measure-windows-toast.ps1`, 3 runs on dev
machine):** average **773 ms** per toast (695874 ms), dominated by PowerShell
cold start. Re-run the script when comparing after a native toast implementation.
**App-side timing:** each failure notification appends one line to
`logs/notify-timing.log` (`ms_after_run`, `ms_fyne_do`, `ms_send`,
`ms_app_total`). These columns end when Fyne returns from `SendNotification`; OS
toast latency is not included.
**Direction:** add `src/platform/notify/` with a native Windows toast (WinRT or
a maintained Go wrapper), used for failure notifications on Windows. Keep Fyne
`SendNotification` on Linux (DBus / xdg-desktop-portal) unless profiling shows it
needs the same treatment.
### Dynamic tray icon toggle
Fyne exposes `SetSystemTrayIcon` and related APIs only at application startup.
There is no supported way to register or remove the notification-area icon
after the process is running.
GoSentry now honours `KeepRunningInTray` from config: close behaviour and the
autostart entry update immediately when the user saves Settings; the tray icon
follows the saved value on the next launch. Settings shows a restart hint when
the tray checkbox changes.
Revisit when Fyne adds a documented API for mid-session tray registration, or
when a stable cross-platform approach exists without reaching into driver
internals. Until then, removing the restart hint and applying the icon on save
is blocked.
### Update check from GitHub releases
Releases are published as GitHub Releases (tags like `v0.12.0`, built by
`.github/workflows/release.yml`), but the app never tells the user a newer
version exists — they have to check the releases page by hand.
Add an update check that queries the GitHub Releases API
(`GET /repos/mixeme/gosentry/releases/latest`) for the latest published tag,
strips the leading `v`, and compares it against `app.Version`. When a newer
version is available, surface it non-intrusively — an "Update available"
line in Settings (next to the existing version/build info) with a hyperlink
to the release page, not a modal on launch.
Design notes / open questions:
- *Opt-in and offline-safe.* The check makes a network request, so it must be
off by default (or clearly consented) and never block startup. Failures
(offline, rate-limited, API change) should be silent — no error dialogs for a
best-effort convenience feature.
- *Version comparison.* Compare semantic versions, not strings, so `0.12.0`
reads as newer than `0.9.0`. A tiny semver comparator in `app` (or a small
dependency) avoids lexical bugs.
- *Where the check lives.* Keep it in the `app` layer behind the Service so the
UI only renders the result, and cache the last check so opening Settings
repeatedly does not spam the API (unauthenticated GitHub allows 60 req/h).
- *Repo coordinates.* The primary remote is Gitea; the GitHub repo used for
releases is [`mixeme/gosentry`](https://github.com/mixeme/gosentry) and must
be wired in explicitly (constant or build-time value) rather than derived from
`origin`.
- *No auto-download.* Scope is detection and notification only; installing the
update stays a manual click-through to the release page.
### Import/export jobs as a cron table
Jobs can only be moved between machines by copying `jobs.json` by hand. Add
"Import" / "Export" actions (Settings tab, file dialogs) that read and write a
crontab-style text file, so a job list can be shared, version-controlled, or
seeded from an existing Unix crontab.
Export writes one line per job — schedule fields, then command and arguments —
and import parses the same format back into `domain.Job` values.
Design notes / open questions:
- *The job model is wider than a crontab line.* `Name`, `Folder`, `StartOnly`,
`OverlapPolicy`, `TimeoutSeconds`, and `Enabled` have no cron equivalent.
Either accept a lossy export (schedule + command only) or carry the extra
fields in a structured comment above each line (`# gosentry: name=… folder=…
timeout=…`), which keeps the file readable by real cron while making the
round-trip lossless. The comment form is preferred; decide the exact key set
before implementing.
- *Disabled jobs.* `Enabled: false` maps naturally to a commented-out line, but
then a disabled job is indistinguishable from a user's own comment unless the
`# gosentry:` marker is present. Pick one representation and document it.
- *`@every` is not crontab.* GoSentry accepts `@every 10s` (see
[`domain.Parse`](../src/domain/schedule.go)), which no cron implementation
understands. Exporting it produces a file that is not a valid crontab;
exporting it as an approximation would silently change the schedule. Keep the
raw string and flag the file as GoSentry-flavoured, rather than converting.
- *Command vs arguments.* Crontab has a single command string; GoSentry splits
`Command` and `Arguments`. Import must split the line the same way the runner
would (see `runner/invocation*.go`, which differs per OS), and export must
join them back without changing quoting.
- *What to skip on import.* Environment assignments (`SHELL=`, `PATH=`,
`MAILTO=`), six-field (seconds) crontabs, and `@reboot` are outside what
`domain.Parse` accepts. Skip them, and report which lines were skipped and
why — a partial import that silently drops rows is worse than a failed one.
- *Merge semantics.* Import must decide between replacing the job list and
appending to it, and must assign fresh IDs rather than trusting the file.
Appending with a confirmation dialog is the safer default; replacing needs an
explicit "this deletes N jobs" confirmation.
- *Where it lives.* Encoding/decoding is pure text handling and belongs in
`domain` (or a small `storage` codec) with unit tests over round-trips; the
Service exposes import/export operations; the UI only picks the file and
shows the outcome.
### Split the files that are over the size guideline
[ARCHITECTURE.md](ARCHITECTURE.md) sets a ~250-line guideline per source file
and records the `jobs_view.go` and `settings_view.go` splits as the worked
examples. Six non-test files are over it at 1.0.0, including both files that
were already split once:
| File | Lines |
|------|-------|
| `src/app/operations.go` | 490 |
| `src/ui/jobs_view.go` | 355 |
| `src/app/run.go` | 287 |
| `src/ui/history_view.go` | 282 |
| `src/ui/settings_view.go` | 277 |
| `src/storage/store.go` | 265 |
This is deliberately deferred to the next whole-project review rather than done
piecemeal: [REVIEW.md](REVIEW.md) already asks item 2 to look for exactly this,
a split touches every reader of the file, and doing all six in one pass keeps
the seams consistent instead of settling them six different ways. Splitting is
also the kind of change that reads as pure movement while quietly dropping a
function, so it wants one careful pass, not six hurried ones.
Seams visible today, as a starting point rather than a decision:
- **`operations.go`** — the worst overage and the clearest split: the public
mutating operations (`CreateJob``UpdateSettings`), the `…Locked` state
helpers that only they call, and the pure validators and normalizers
(`normalizeJob`, `validateJob`, `hasFileName`, `validateConfig`) are three
distinct jobs already sitting in three consecutive blocks.
- **`history_view.go`** — the column-measuring helpers (`textWidth` through
`historyColumnWidths`) are pure, already unit-tested, and independent of the
table they size.
- **`jobs_view.go`** — nearly all of it is one `newJobsView` constructor, so the
split has to break that function up (list template, toolbar handlers,
assembly) rather than move whole functions. Larger judgement call than the
others.
- **`run.go`**, **`settings_view.go`**, **`store.go`** — barely over. Worth
re-measuring at the time; if a pass elsewhere has shrunk them, leave them
alone rather than splitting for the sake of the number.
Scope note: the guideline is about source files. Test files are much larger and
that is fine — a table-driven test file grows with the cases it covers.
### Window size persistence *(frozen)*
Window size is currently **not** saved on quit or close. Saving was disabled
because `w.Canvas().Size()` returns the maximized dimensions when the window is
maximized, which would corrupt the stored size on the next launch.
Re-enabling requires a cross-platform way to detect the maximized state before
saving. Fyne v2.x has no API for this; it needs per-OS native calls:
`IsZoomed` (Windows), `_NET_WM_STATE` (X11/Linux), `NSWindow.isZoomed`
(macOS). Unfreeze once that detection is in place.
**Disadvantages of a platform-specific approach:**
- *Three separate implementations.* Windows, macOS, and Linux each need their
own file guarded by a build tag. Each adds CGO bindings or raw syscall
wrappers that must be kept in sync as OS APIs evolve.
- *Linux is not one target.* X11 and Wayland have completely different window
state models. `_NET_WM_STATE` is X11-only; under Wayland the compositor
controls window decorations and there is no stable client-side API to query
the maximized state. A single `linux` build tag cannot cover both correctly.
- *Native window handle is not exposed.* Fyne does not surface the underlying
`HWND` / `NSWindow` / `XID` through its public API. Obtaining it requires
either enumerating OS-level windows by PID (fragile, finds wrong windows when
dialogs are open) or reaching into Fyne/GLFW internals (breaks on Fyne
upgrades).
- *Thread-safety constraints.* Win32 and GLFW both require their calls to be
made from the OS main thread. Tray-menu callbacks run on a separate goroutine,
so any native call must be marshalled back to the main thread, adding
synchronisation complexity.
- *Test coverage gap.* Maximized-state detection cannot be exercised by Fyne's
headless test driver; it requires a real display and manual or screen-capture
automation per platform.
### History tab — column filters (Trigger / Job / State)
Add dropdown filters above the History table so the user can narrow rows by
trigger source, job name, or run state. Blocked on Fyne native support: the
current `widget.Table` has no built-in filter API, and a filter bar built from
`widget.Select` widgets above the table feels visually out-of-place. Revisit
when Fyne adds first-class column filtering or a composable data-grid widget.