Files
Andrey KizimenkoandGeorge Karr 0f9af89a93 Add Hangar: Go/Wails desktop control panel for the Fleet dev environment (#46406)
## Summary

Adds `tools/hangar` — a macOS desktop control panel for working on Fleet
locally: branch management, `fleet serve` orchestration, log tail, dev
MySQL backup/restore, `fleetctl`, GitOps, and `osquery-perf`, all in one
window.

Built with **Go + [Wails 3](https://v3alpha.wails.io)** — the backend is
plain Go (`os/exec`, `syscall`, goroutines) so Fleet engineers can
contribute to it; only the desktop shell is Wails. The `internal/`
packages are pure and unit-tested.

### History note
Hangar started as a Rust/Tauri app. It was ported to Go, and **the Go
port is now the canonical `tools/hangar`**. The original Rust/Tauri
implementation has been removed from the monorepo (preserved in a
standalone repo) — so although this branch's earlier commits add and
then replace the Rust app, the net diff is just the Go app at
`tools/hangar`. The bundle identifier is `com.fleetdm.fleet-hangar`,
matching the original app so existing settings carry over.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

> No `changes/` file: `tools/` is contributor tooling, not a
user-visible Fleet change. No DB migrations, no Fleet config settings,
no fleetd/orbit changes.

## Testing

- [x] Added/updated automated tests (Go unit tests across `internal/`,
including a path-traversal regression for backup deletion)
- [x] QA'd all new/changed functionality manually

## Test plan
- [x] `cd tools/hangar && task dev` launches the app (live-reload)
- [x] `task build` produces `bin/fleet-hangar`; `go test ./...` is green
- [x] First-run gate discovers a local Fleet clone and runs dep checks
- [x] Server tab can run the build chain and start `fleet serve`
- [x] Git tab branch search finds an older branch (e.g. a stale `qa-*`)
by name


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced Fleet Hangar, a comprehensive desktop application for Fleet
development workflows, providing unified controls for server/database
management, git operations, configuration, logging, and troubleshooting.
  * Added database backup management with metadata tracking.
* Integrated process orchestration for development services (Docker,
ngrok, Python).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: George Karr <georgekarrv@users.noreply.github.com>
2026-06-23 15:45:23 -05:00

80 lines
2.1 KiB
Go

package main
import (
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/fleetdm/fleet/tools/hangar/internal/traymenu"
)
const mainWindowName = "main"
// trayController owns the native system tray and rebuilds its menu from
// pushed state. Ported from tray.rs.
type trayController struct {
app *application.App
tray *application.SystemTray
}
func newTrayController(app *application.App, icon []byte) *trayController {
t := app.SystemTray.New()
// Template mode renders the icon as a system-colored silhouette in the
// menu bar (alpha-only), matching the rest of the macOS menu bar.
t.SetTemplateIcon(icon)
t.SetTooltip("Fleet Hangar")
c := &trayController{app: app, tray: t}
c.update(traymenu.State{})
// Left-click opens the main window; the menu shows on right-click.
t.OnClick(func() { showMainWindow(app) })
return c
}
// update rebuilds the tray menu in place from the given state.
func (c *trayController) update(s traymenu.State) {
menu := application.NewMenu()
for _, it := range traymenu.BuildItems(s) {
if it.Separator {
menu.AddSeparator()
continue
}
item := menu.Add(it.Label)
item.SetEnabled(it.Enabled)
id := it.ID
item.OnClick(func(*application.Context) { c.handleClick(id) })
}
c.tray.SetMenu(menu)
}
// handleClick routes a menu item. show/quit are handled here; everything else
// is forwarded to the frontend, which owns the start/stop orchestration.
func (c *trayController) handleClick(id string) {
switch id {
case "tray:show":
showMainWindow(c.app)
case "tray:quit":
// Surface the window so the confirm modal is visible even if the app
// was tray-only, then let the frontend drive the quit flow.
showMainWindow(c.app)
c.app.Event.Emit("app:quit-requested")
default:
if strings.HasPrefix(id, "tray:") {
c.app.Event.Emit(id)
}
}
}
// showMainWindow brings the main window back from hidden/minimized and
// focuses it.
func showMainWindow(app *application.App) {
w, ok := app.Window.GetByName(mainWindowName)
if !ok {
return
}
if ww, ok := w.(*application.WebviewWindow); ok {
ww.UnMinimise()
ww.Show()
ww.Focus()
}
}