Improve probo-agent tray icon and enrollment menu

Replace the placeholder orange-circle tray icon with the monochrome
Probo logo used in auditor-mode. On macOS, show the icon only (no
menu bar title); keep the title on Windows.

Restructure the context menu: status rows with colored indicators,
an Enroll via… submenu for region selection, and a clearer About
label. Surface enrollment errors in native dialogs instead of stderr.

Fix self-hosted enrollment on macOS by reading the hostname from
osascript's returned value; the default dialog output order broke
URL parsing and silently skipped opening the browser.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
Ludovic Vielle
2026-07-15 19:10:44 +02:00
parent ae769f52a1
commit 4a56be2e3e
13 changed files with 108 additions and 12 deletions

View File

@@ -39,3 +39,16 @@ func showAbout(version string) {
)
_ = exec.Command(path, "-e", script).Run()
}
func showEnrollmentError(message string) {
path, ok := osascriptPath()
if !ok {
return
}
script := fmt.Sprintf(
`display alert "Probo Device Posture Agent" message %q buttons {"OK"} default button "OK"`,
message,
)
_ = exec.Command(path, "-e", script).Run()
}

View File

@@ -33,3 +33,7 @@ func showAbout(version string) {
)
nativeMessageBox("Probo Device Posture Agent", message, mbOK|mbIconInformation)
}
func showEnrollmentError(message string) {
nativeMessageBox("Probo Device Posture Agent", message, mbOK|mbIconError)
}

View File

@@ -24,7 +24,6 @@ package tray
import (
"fmt"
"os"
"go.probo.inc/probo/pkg/deviceagent"
)
@@ -32,12 +31,12 @@ import (
func openConsoleEnroll(serverURL string) {
enrollURL, err := deviceagent.ConsoleEnrollURL(serverURL)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot open enrollment page: %v\n", err)
showEnrollmentError(fmt.Sprintf("Cannot open enrollment page: %v", err))
return
}
if err := openBrowser(enrollURL); err != nil {
fmt.Fprintf(os.Stderr, "cannot open browser: %v\n", err)
showEnrollmentError(fmt.Sprintf("Cannot open browser: %v", err))
}
}
@@ -49,7 +48,7 @@ func openSelfHostedEnroll() {
normalized, err := deviceagent.NormalizeServerURL(hostname)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid hostname: %v\n", err)
showEnrollmentError(fmt.Sprintf("Invalid hostname: %v", err))
return
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 851 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 B

After

Width:  |  Height:  |  Size: 851 B

View File

@@ -29,3 +29,12 @@ var iconData []byte
//go:embed iconTemplate.png
var iconTemplateData []byte
//go:embed status_connected.png
var statusConnectedIconData []byte
//go:embed status_unavailable.png
var statusUnavailableIconData []byte
//go:embed status_enrollment.png
var statusEnrollmentIconData []byte

View File

@@ -31,6 +31,7 @@ import (
const (
mbOK = 0x00000000
mbIconInformation = 0x00000040
mbIconError = 0x00000010
)
var (

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

View File

@@ -3,7 +3,7 @@
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// to use, copy, modify, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
@@ -23,7 +23,6 @@
package tray
import (
"fmt"
"os"
"os/signal"
"sync"
@@ -87,25 +86,35 @@ func (m enrollmentMenu) hide() {
func onReady(opts Options, done <-chan struct{}) {
setTrayIcons()
systray.SetTitle("Probo")
enrollMenu := setupEnrollmentMenu(opts, done)
setTrayTitle()
connectedItem := systray.AddMenuItem("Connected", "Device is enrolled and reporting")
connectedItem.SetIcon(statusConnectedIconData)
connectedItem.Disable()
connectedItem.Hide()
enrollmentRequiredItem := systray.AddMenuItem(
"Enrollment required",
"Device is not enrolled yet",
)
enrollmentRequiredItem.SetIcon(statusEnrollmentIconData)
enrollmentRequiredItem.Disable()
enrollmentRequiredItem.Hide()
statusUnavailableItem := systray.AddMenuItem(
"Status unavailable",
"Cannot read enrollment status",
)
statusUnavailableItem.SetIcon(statusUnavailableIconData)
statusUnavailableItem.Disable()
statusUnavailableItem.Hide()
systray.AddSeparator()
enrollMenu := setupEnrollmentMenu(opts, done)
aboutItem := systray.AddMenuItem(
fmt.Sprintf("About probo-agent %s", opts.Version),
"About Probo Device Posture Agent…",
"Probo device posture agent",
)
@@ -114,6 +123,7 @@ func onReady(opts Options, done <-chan struct{}) {
if err != nil {
enrollMenu.hide()
connectedItem.Hide()
enrollmentRequiredItem.Hide()
statusUnavailableItem.Show()
systray.SetTooltip("Probo Device Posture Agent — Status unavailable")
@@ -124,11 +134,13 @@ func onReady(opts Options, done <-chan struct{}) {
if enrolled {
enrollMenu.hide()
enrollmentRequiredItem.Hide()
connectedItem.Show()
systray.SetTooltip("Probo Device Posture Agent — Connected")
} else {
enrollMenu.show()
connectedItem.Hide()
enrollmentRequiredItem.Show()
systray.SetTooltip("Probo Device Posture Agent — Enrollment required")
}
}
@@ -173,7 +185,7 @@ func setupEnrollmentMenu(opts Options, done <-chan struct{}) enrollmentMenu {
return enrollmentMenu{items: []*systray.MenuItem{enrollItem}}
}
enrollRoot := systray.AddMenuItem("Enroll in browser", enrollTooltip)
enrollRoot := systray.AddMenuItem("Enroll via…", enrollTooltip)
usItem := enrollRoot.AddSubMenuItem(
"United States (us.probo.com)",

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//go:build darwin && cgo
package tray
import "fyne.io/systray"
func setTrayTitle() {
systray.SetTitle("")
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//go:build windows
package tray
import "fyne.io/systray"
func setTrayTitle() {
systray.SetTitle("Probo")
}