# 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. ## Features User-facing functionality that is not blocked on a framework or platform gap. ### 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. ## Platform OS-specific improvements outside the Fyne abstraction. ### 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 **1–3 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 (695–874 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.tsv` (`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. The `.tsv` extension keeps it out of `runner.CleanupLogs`, which only manages `.log` files — this file is diagnostic instrumentation for this item, not job output, and should be removed (or unified with the run-log retention policy under its own knob) once the native-toast direction below lands and the timing data is no longer needed. **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. ## Fyne / UI GUI-layer gaps and trade-offs. Each item below states its **blocker**: - **Fyne API** — the missing API is the root cause. Qt, GTK, and native toolkits usually expose the same capability; a future Fyne release is the preferred fix. Some items have no viable workaround; others could be bypassed with `src/platform/` code, but that cost is not justified while upstream might still add the API. - **UX** — shippable with today's widgets; deferred until a cleaner approach exists (a nicer Fyne widget would help but is not required). ### Dynamic tray icon toggle **Blocker: Fyne API** (no viable workaround). 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. ### History tab — column filters (Trigger / Job / State) **Blocker: UX** (not a Fyne release). Add dropdown filters above the History table so the user can narrow rows by trigger source, job name, or run state. A filter bar built from `widget.Select` widgets above the table would work today — filter the row slice in app code — but `widget.Table` has no built-in filter API and the ad-hoc bar feels visually out-of-place. Revisit when Fyne adds first-class column filtering or a composable data-grid widget, or when a hand-rolled bar is acceptable. ### Window size persistence *(frozen)* **Blocker: Fyne API** (costly platform workaround possible). 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. Fyne v2.x has no API to query window state (maximized, normal, etc.) — unlike Qt, GTK, or platform-native toolkits. If Fyne added something like `Window.IsMaximized()`, persistence could be re-enabled without any platform-specific code; that upstream API is the preferred unblock. A bypass through per-OS native detection (`IsZoomed` on Windows, `_NET_WM_STATE` on X11, `NSWindow.isZoomed` on macOS) is technically possible, similar to other `src/platform/` work, but Fyne does not expose the underlying window handle, so the bypass is fragile and expensive. The item stays frozen until either Fyne ships window-state API or that platform cost is judged worth paying. **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. ## Maintenance Technical debt and one-time cleanup with no user-visible feature surface. ### Retire the config compatibility shims Two read-only shims in `storage.loadOrCreateConfig` rewrite an old file into the current shape on the next save, so each becomes dead the moment a user's config has been saved once by a build that has it: - `Config.JobsDir` (pre-0.15, superseded by `Config.JobsFile`). - `Theme == "default"` (pre-1.0.1, superseded by `ThemeSystem`). Neither has an expiry. Remove both — the field, the migration branch, and `TestLoadOrCreateConfigMigratesJobsDir` / `TestLoadOrCreateConfigMigratesLegacyThemeDefault` — once a release has shipped long enough that a config file still carrying either old shape is not a realistic upgrade path GoSentry needs to support.