Skip to content

feat: macOS GUI Phase 10+11 — diagnostics, panic, install/uninstall, VPN config panel#16

Merged
Behnam-RK merged 10 commits into
mainfrom
feat/macos-gui-phase-9-10
Jul 8, 2026
Merged

feat: macOS GUI Phase 10+11 — diagnostics, panic, install/uninstall, VPN config panel#16
Behnam-RK merged 10 commits into
mainfrom
feat/macos-gui-phase-9-10

Conversation

@Behnam-RK

@Behnam-RK Behnam-RK commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the CLI↔GUI gap identified after Phase 8: the menubar app previously only exposed Start/Stop/Block/Unblock, leaving several essential CLI features reachable only from the terminal.

Numbering note: these GUI phases were originally drafted as 9+10, but phase 9 was taken by the VPN-profiles/switch-window work (PR #17) which merged first. They are renumbered to 10 & 11 to sit after it; the doc files and in-code references were updated to match during the rebase onto main.

Phase 10 — diagnostics, output capture, safety controls (docs/plans/phase-10-gui-diagnostics.md):

  • runPrivileged now captures and returns real stdout/stderr instead of a bare Bool — failures show what actually happened instead of silence
  • Panic (confirmation-gated), Install/Uninstall service (gated on status --json), an About panel, and a View-logs submenu scoped to the unified log (process == "dezhban")
  • One shared OutputPanel window reused by every diagnostic/action

Phase 11 — VPN guard config panel (docs/plans/phase-11-gui-vpn-config.md):

  • Closes the Phase 8 scope note that deferred in-app VPN-mode toggling
  • Drives the CLI's existing config get/set/validate per dotted key — no new Go command, no second config schema mirrored in Swift
  • Orders field writes carefully (other fields first, vpn.enabled last when enabling; reversed when disabling), since config set validates the entire config on every write, not just the touched field
  • Discloses the stop/start restart's fail-open window explicitly via a warning modal rather than hiding it, and polls status --json after restart instead of assuming success

Zero Go changesgo build/go vet/go test (93 tests, 11 packages) all pass unmodified. swift build, swift build -c release, and make gui-macos all succeed and produce dist/Dezhban.app.

This was planned via an independent draft + adversarial review pass before implementation (see the two phase docs for full design rationale, rejected alternatives, and the risks/open-questions sections).

Test plan

Automated (done):

  • go build ./... && go vet ./... && go test ./...
  • swift build and swift build -c release
  • make gui-macos produces dist/Dezhban.app

Manual (needs a real macOS host with root — not exercisable in CI/sandbox, see each phase doc's ## Acceptance / verification section):

  • Panic tears down rules; confirm/cancel behavior
  • Install/Uninstall flips the menu and status --json's registered flag correctly
  • Diagnostics output matches hand-run doctor/validate/print-rules/monitor --once
  • VPN panel: seeded values match dezhban config show; enabling with valid endpoints round-trips through restart (icon ⚪ during the gap, then 🟢/🔴); a deliberately-bad endpoint is refused before any restart; a simulated start failure is reported as failure, not false success
  • Log show/stream output matches hand-run log commands; no orphaned log stream process after closing the window
  • Auth caching still prompts once per ~5 min burst of privileged actions

🤖 Generated with Claude Code

https://claude.ai/code/session_017o9YaEjft1DgS62KQN4UaS

Closes the CLI↔GUI gap identified after Phase 8: the menubar app only
exposed Start/Stop/Block/Unblock, leaving Panic, install/uninstall,
diagnostics, and VPN-guard config editing CLI-only.

Phase 9 (diagnostics, output capture, safety controls):
- runPrivileged now returns captured stdout/stderr instead of a bare
  Bool, so failures show real output instead of silence
- Panic (confirmation-gated), Install/Uninstall service, About panel,
  and a View logs submenu filtered to the unified log
- One shared OutputPanel window reused by every diagnostic/action

Phase 10 (VPN guard config panel):
- Closes the Phase 8 scope note deferring in-app VPN-mode toggling
- Drives the CLI's existing config get/set/validate per dotted key —
  no new Go command, no second config schema mirrored in Swift
- Orders field writes (other fields before enabling, vpn.enabled
  first when disabling) since config set validates the whole config
  on every write, not just the touched field
- Discloses the stop/start restart's fail-open window explicitly via
  a warning modal rather than hiding it; polls status --json after
  restart instead of assuming success

Zero Go changes; go build/vet/test and swift build (debug + release)
all pass. Planned via independent draft + adversarial review before
implementation (see docs/plans/phase-9-gui-diagnostics.md and
phase-10-gui-vpn-config.md for full design rationale and the
still-open manual/root verification checklist).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017o9YaEjft1DgS62KQN4UaS
@Behnam-RK Behnam-RK force-pushed the feat/macos-gui-phase-9-10 branch from d4f9115 to 5b1e5c8 Compare July 8, 2026 15:53
@Behnam-RK Behnam-RK requested a review from Copilot July 8, 2026 15:55
@Behnam-RK Behnam-RK changed the title feat: macOS GUI Phase 9+10 — diagnostics, panic, install/uninstall, VPN config panel feat: macOS GUI Phase 10+11 — diagnostics, panic, install/uninstall, VPN config panel Jul 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends the macOS menubar GUI to cover additional CLI functionality (diagnostics, panic, install/uninstall, about, scoped logs) and adds an in-app VPN guard configuration panel that reads/writes the existing dotted vpn.* config keys via the CLI, including an explicit restart warning and bounded post-restart status polling.

Changes:

  • Added a shared reusable output window (OutputPanel) and streaming process support for log stream.
  • Enhanced CLI invocation plumbing (DezhbanCLI) to return captured output and added log helpers + service-installed detection.
  • Added a Phase 11 VPN config panel that seeds from config get and applies changes via ordered privileged config set + validate + restart flow.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift New VPN guard config UI that seeds via config get and applies via ordered config set + validate + restart + status polling.
macos-gui/Sources/DezhbanMenu/OutputPanel.swift New shared output window for run-to-completion output and streaming logs with a Stop button.
macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift Introduces CommandResult, improves privileged output capture, adds unprivileged run, service detection, and log command helpers + streaming process.
macos-gui/Sources/DezhbanMenu/AppDelegate.swift Wires new menu items (diagnostics/panic/install/uninstall/about/logs) and opens the VPN config panel; improves failure alerts with captured output.
docs/plans/readme.md Updates phase index/status text and adds Phase 10/11 entries.
docs/plans/phase-10-gui-diagnostics.md Adds the Phase 10 plan document describing diagnostics/output capture/logs/etc.
docs/plans/phase-11-gui-vpn-config.md Adds the Phase 11 plan document describing the VPN config panel approach and restart-window disclosure.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/OutputPanel.swift
Comment thread docs/plans/readme.md Outdated
Comment thread docs/plans/readme.md Outdated
- runPrivileged: fold stderr into stdout via 2>&1 so a command that
  writes to stderr while exiting 0 still surfaces its output
- OutputPanel.append: append into textStorage instead of `string +=`
  (O(n^2) over long log streams); carry the view font to keep styling
- VPNConfigPanel.seedFields: capture each `config get` result,
  short-circuit on failure, keep Apply disabled, surface the error
  instead of seeding error text back into the config
- docs/plans/readme.md: mark phases 10-11 as complete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/AppDelegate.swift
Comment thread docs/plans/phase-11-gui-vpn-config.md Outdated
- exec: drain stdout/stderr concurrently via a DispatchGroup, so a child
  that fills its stderr pipe buffer can't deadlock the GUI background thread
- VPNConfigPanel apply: resolve the config path once and pass --config to
  every `config set` (matching the final `validate`), so all writes and the
  validation provably target the same file
- AppDelegate log menu: stop gating "Show last hour"/"Stream live…" on the
  dezhban binary — they run /usr/bin/log and stay useful when the CLI is
  uninstalled/mislocated
- phase-11 doc: correct the seeding description to per-key `config get`
  (not `config show`), matching the implementation

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/AppDelegate.swift
Comment thread macos-gui/Sources/DezhbanMenu/AppDelegate.swift
- VPNConfigPanel.seedFields: read via the same resolved --config path Apply
  writes/validates with, so a nonstandard path can't seed one file and apply
  to another
- exec: drop the two-writer concurrent drain for a single background stderr
  reader ordered by a semaphore — no shared mutable state, still deadlock-free
- AppDelegate: cache serviceInstalled() and refresh it off the main thread
  (launch, post install/uninstall, each menu open) so opening the menu never
  blocks on `status --json`
- streamLogs: on start failure, revert the panel to non-streaming so the Stop
  button and onStop handler don't linger with no process

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/AppDelegate.swift Outdated
Comment thread docs/plans/readme.md Outdated
Comment thread docs/plans/phase-11-gui-vpn-config.md Outdated
- VPNConfigPanel: keep Apply disabled through the restart (only re-enabled
  on cancel or when the restart finishes), so a second config-write/restart
  can't run concurrently; re-enable Apply in the restart-failure and
  completion paths so the panel is never left stuck
- About panel: split the misleading "Service status" (which showed the
  enforcement posture) into distinct "Posture" and "Service" (installed?)
  lines
- docs/plans/readme.md: stop claiming phases 10-11 are merged to main —
  they're complete on this branch, pending merge
- phase-11 doc: reference AppDelegate.openConfig / "Open config file…" by
  name instead of a fragile line number

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift Outdated
- Add DezhbanCLI.reportedPosture(): parse `status --json` from stdout only
  (via exec, like serviceInstalled) instead of run's combined stdout+stderr,
  so a warning on stderr with a 0 exit can't corrupt the JSON parse. Use it
  in the post-restart poll.
- performRestart: document why stop/start take no --config — they act on the
  already-installed service unit whose config path is baked at install time
  (cmdService reads --config only for `install`); the service manager ignores
  --config on start/stop, and the GUI installs with resolvedConfigPath(), the
  same path the panel writes to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
- resolvedConfigPath: stop splitting `config path` output on spaces, which
  truncated valid paths containing spaces (e.g. ~/Library/Application
  Support/…). Take the single line and strip only the optional trailing
  " (not present …)" parenthetical note.
- VPNConfigPanel.seedFields: reuse the already-resolved cfgPath in the
  "Seeded from …" status text instead of calling resolvedConfigPath() a
  second time (extra subprocess; could display a path different from the
  one actually seeded).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
- runPrivileged: fix a regression from the earlier `2>&1` change. `do shell
  script` reports a non-zero exit as an AppleScript error whose message is the
  command's stderr, so folding stderr into stdout erased failure diagnostics
  (a bare "error code N" for `config set` validation failures). Now capture
  combined output in a shell var and print it to stdout on success / stderr +
  re-exit non-zero on failure, so both outcomes surface real output.
- StreamingProcess.start: decode with String(decoding:as:) (never nil) instead
  of String(data:encoding:), which dropped whole chunks when a multi-byte UTF-8
  sequence split across availableData reads; clear the readabilityHandler on EOF
  so it doesn't dangle.
- VPNConfigPanel apply transcript: quote value/cfgPath via String(reflecting:)
  so the log stays unambiguous and copy/paste-runnable when they contain spaces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread macos-gui/Sources/DezhbanMenu/DezhbanCLI.swift Outdated
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
@Behnam-RK Behnam-RK added the run-ci Run CI on this PR (gates .github/workflows/ci.yml) label Jul 8, 2026
- resolvedConfigPath: strip only the exact fixed Go note suffix
  " (not present — using built-in defaults)" instead of cutting at the last
  " (", so a real config path that itself ends in ")" (a directory with
  parentheses) is no longer truncated.
- looksLikeGoDuration: make the superficial pre-check permissive enough to
  accept everything time.ParseDuration does — optional sign and the bare "0"
  were previously rejected (e.g. "0", "-1s", "+5m"). config set stays the
  authority; the pre-check only needs to avoid rejecting valid input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
Comment thread macos-gui/Sources/DezhbanMenu/VPNConfigPanel.swift
- looksLikeGoDuration: require a numeric prefix per chunk (a digit before or
  after the dot) so bare units like "s"/"ms" are rejected again, while still
  accepting every valid form (signed, bare "0", ".5s", "5.s"). Balances the
  previous round's over-permissive fix without regressing valid input.
- VPNConfigPanel.seedFields: clear all controls before loading, so a failed
  seed can't leave stale values from a previous successful open on screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F6ZjJH6UqiJQAYre5tNSuC
@Behnam-RK Behnam-RK merged commit bd692b9 into main Jul 8, 2026
5 checks passed
@Behnam-RK Behnam-RK deleted the feat/macos-gui-phase-9-10 branch July 8, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci Run CI on this PR (gates .github/workflows/ci.yml)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants