Skip to content

Implement native macOS GUI foundation with structured API helper#129

Open
jamesyc wants to merge 8 commits into
mainfrom
gui
Open

Implement native macOS GUI foundation with structured API helper#129
jamesyc wants to merge 8 commits into
mainfrom
gui

Conversation

@jamesyc
Copy link
Copy Markdown
Owner

@jamesyc jamesyc commented May 20, 2026

Implement native macOS GUI foundation with structured API helper

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eec1bcb6ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/timecapsulesmb/app/service.py Outdated
Comment thread macos/TimeCapsuleSMB/Sources/TimeCapsuleSMBApp/BackendClient.swift Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a structured Python backend and a macOS SwiftUI frontend to provide a graphical interface for TimeCapsuleSMB operations. The backend adds a JSON-based API mode for tasks such as device discovery, deployment, and maintenance, while the frontend manages the helper process and event streaming. Review feedback identifies several critical improvements, including resolving a potential deadlock when reading process output, ensuring configuration files are merged rather than overwritten, capturing stderr for better logging, and enhancing error diagnostics with tracebacks.

Comment thread macos/TimeCapsuleSMB/Sources/TimeCapsuleSMBApp/BackendClient.swift Outdated
Comment thread src/timecapsulesmb/app/service.py Outdated
Comment thread macos/TimeCapsuleSMB/Sources/TimeCapsuleSMBApp/BackendClient.swift Outdated
Comment thread src/timecapsulesmb/app/service.py Outdated
Comment thread src/timecapsulesmb/app/service.py Outdated
@jamesyc
Copy link
Copy Markdown
Owner Author

jamesyc commented May 20, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a macOS GUI for TimeCapsuleSMB, featuring a SwiftUI application and a new Python-based structured API. The implementation includes a subprocess runner to execute the Python helper and a refactored backend to support event streaming and configuration management. Review feedback highlights the need for more idiomatic Swift Concurrency patterns in the subprocess runner to avoid polling and thread blocking. Additionally, the reviewer identified several locations in the Python operations where unvalidated type conversions of GUI parameters could cause crashes, and recommended updating a deprecated SwiftUI alert API.

Comment thread macos/TimeCapsuleSMB/Sources/TimeCapsuleSMBApp/HelperRunner.swift Outdated
Comment thread macos/TimeCapsuleSMB/Sources/TimeCapsuleSMBApp/HelperRunner.swift Outdated
Comment thread src/timecapsulesmb/app/operations.py Outdated
Comment thread src/timecapsulesmb/app/operations.py Outdated
Comment thread macos/TimeCapsuleSMB/Sources/TimeCapsuleSMBApp/ContentView.swift Outdated
@jamesyc
Copy link
Copy Markdown
Owner Author

jamesyc commented May 20, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a structured app backend and a macOS GUI integration for TimeCapsuleSMB. The changes include a new Python-based API helper for structured communication, a Swift-based macOS application, and comprehensive unit tests for both. Feedback provided includes recommendations for localizing user-facing strings in the SwiftUI views, improving UTF-8 decoding safety in the Swift helper runner, implementing input size limits on stdin to prevent potential Denial of Service, and enhancing the jsonable utility to support Enum types.

Comment on lines +3 to +263
public struct ContentView: View {
@StateObject private var backend = BackendClient()
@State private var selection: Screen = .readiness
@State private var host = "root@192.168.x.x"
@State private var password = ""
@State private var repairPath = ""
@State private var volume = ""
@State private var nbnsEnabled = true
@State private var noReboot = false
@State private var dryRun = true
@State private var pendingConfirmation: PendingConfirmation?

public init() {}

public var body: some View {
NavigationSplitView {
List(Screen.allCases, selection: $selection) { screen in
Label(screen.title, systemImage: screen.icon)
.tag(screen)
}
.navigationTitle("TimeCapsuleSMB")
} detail: {
VStack(spacing: 0) {
form
Divider()
EventList(events: backend.events)
}
.toolbar {
ToolbarItemGroup {
Button {
backend.clear()
} label: {
Label("Clear", systemImage: "trash")
}
.disabled(backend.isRunning)
Button {
backend.cancel()
} label: {
Label("Cancel", systemImage: "xmark.circle")
}
.disabled(!backend.isRunning)
}
}
}
.frame(minWidth: 980, minHeight: 680)
.alert(
pendingConfirmation?.title ?? "",
isPresented: confirmationPresented,
presenting: pendingConfirmation
) { confirmation in
Button(confirmation.actionTitle, role: .destructive) {
backend.run(operation: confirmation.operation, params: confirmation.params)
pendingConfirmation = nil
}
Button("Cancel", role: .cancel) {
pendingConfirmation = nil
}
} message: { confirmation in
Text(confirmation.message)
}
}

private var confirmationPresented: Binding<Bool> {
Binding(
get: { pendingConfirmation != nil },
set: { isPresented in
if !isPresented {
pendingConfirmation = nil
}
}
)
}

@ViewBuilder
private var form: some View {
switch selection {
case .readiness:
CommandPanel(title: "Readiness") {
TextField("Helper", text: $backend.helperPath)
HStack {
runButton("Paths", icon: "folder", operation: "paths")
runButton("Validate", icon: "checkmark.seal", operation: "validate-install")
}
}
case .connect:
CommandPanel(title: "Discover And Connect") {
TextField("Host", text: $host)
SecureField("Password", text: $password)
HStack {
runButton("Discover", icon: "network", operation: "discover")
Button {
backend.run(operation: "configure", params: [
"host": .string(host),
"password": .string(password)
])
} label: {
Label("Configure", systemImage: "lock.open")
}
.disabled(backend.isRunning || password.isEmpty)
}
}
case .deploy:
CommandPanel(title: "Deploy") {
Toggle("Enable NBNS", isOn: $nbnsEnabled)
Toggle("No Reboot", isOn: $noReboot)
Toggle("Dry Run", isOn: $dryRun)
Button {
if dryRun {
backend.run(operation: "deploy", params: [
"dry_run": .bool(true),
"no_reboot": .bool(noReboot),
"nbns_enabled": .bool(nbnsEnabled)
])
} else {
pendingConfirmation = .deploy(noReboot: noReboot, nbnsEnabled: nbnsEnabled)
}
} label: {
Label(dryRun ? "Plan Deploy" : "Deploy", systemImage: dryRun ? "doc.text.magnifyingglass" : "square.and.arrow.up")
}
.disabled(backend.isRunning)
}
case .doctor:
CommandPanel(title: "Doctor") {
runButton("Run Doctor", icon: "stethoscope", operation: "doctor")
}
case .maintenance:
CommandPanel(title: "Maintenance") {
TextField("Repair xattrs path", text: $repairPath)
TextField("fsck volume, optional", text: $volume)
Toggle("No Reboot", isOn: $noReboot)
HStack {
Button {
pendingConfirmation = .activate()
} label: {
Label("Activate", systemImage: "power")
}
.disabled(backend.isRunning)
runButton("Uninstall Plan", icon: "xmark.bin", operation: "uninstall", params: ["dry_run": .bool(true)])
Button {
pendingConfirmation = .uninstall(noReboot: noReboot)
} label: {
Label("Uninstall", systemImage: "xmark.bin.fill")
}
.disabled(backend.isRunning)
}
HStack {
Button {
pendingConfirmation = .fsck(volume: volume, noReboot: noReboot)
} label: {
Label("Run fsck", systemImage: "externaldrive.badge.checkmark")
}
.disabled(backend.isRunning)
Button {
backend.run(operation: "repair-xattrs", params: [
"path": .string(repairPath),
"dry_run": .bool(true)
])
} label: {
Label("Scan xattrs", systemImage: "wand.and.stars")
}
.disabled(backend.isRunning || repairPath.isEmpty)
Button {
pendingConfirmation = .repairXattrs(path: repairPath)
} label: {
Label("Repair xattrs", systemImage: "wand.and.stars.inverse")
}
.disabled(backend.isRunning || repairPath.isEmpty)
}
}
case .advanced:
CommandPanel(title: "Advanced") {
Text("Flash backup, patch, and restore remain CLI-only in this version.")
.foregroundStyle(.secondary)
Text("Use `.venv/bin/tcapsule flash --help` for firmware operations.")
.font(.system(.body, design: .monospaced))
}
}
}

private func runButton(
_ title: String,
icon: String,
operation: String,
params: [String: JSONValue] = [:]
) -> some View {
Button {
backend.run(operation: operation, params: params)
} label: {
Label(title, systemImage: icon)
}
.disabled(backend.isRunning)
}
}

private enum Screen: String, CaseIterable, Identifiable {
case readiness
case connect
case deploy
case doctor
case maintenance
case advanced

var id: String { rawValue }

var title: String {
switch self {
case .readiness: return "Readiness"
case .connect: return "Connect"
case .deploy: return "Deploy"
case .doctor: return "Doctor"
case .maintenance: return "Maintenance"
case .advanced: return "Advanced"
}
}

var icon: String {
switch self {
case .readiness: return "checklist"
case .connect: return "network"
case .deploy: return "square.and.arrow.up"
case .doctor: return "stethoscope"
case .maintenance: return "wrench.and.screwdriver"
case .advanced: return "exclamationmark.triangle"
}
}
}

private struct CommandPanel<Content: View>: View {
let title: String
@ViewBuilder var content: Content

var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.title2.weight(.semibold))
content
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}

private struct EventList: View {
let events: [BackendEvent]

var body: some View {
List(events) { event in
VStack(alignment: .leading, spacing: 4) {
Text(event.summary)
.font(.body)
if let payload = event.payload, event.type == "result" {
Text(payload.displayText)
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
.lineLimit(6)
}
}
.padding(.vertical, 3)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The ContentView and its subviews contain numerous hardcoded user-facing strings (e.g., "Readiness", "Discover And Connect", "Deploy", etc.). To support internationalization (i18n) and follow macOS development best practices, these strings should be moved to a .strings or .stringsdict file and accessed using LocalizedStringKey or the String(localized:) initializer.

output.append(data.prefix(limit - output.count))
}
}
return String(data: output, encoding: .utf8) ?? ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using String(data:encoding:) with .utf8 can return nil if the Data was truncated in the middle of a multi-byte UTF-8 character sequence, which is possible here due to the limit applied in readCapped. Using String(decoding:as:) is safer as it will provide a string even if the data contains invalid or truncated sequences by using the Unicode replacement character.

Suggested change
return String(data: output, encoding: .utf8) ?? ""
return String(decoding: output, as: UTF8.self)

Comment thread src/timecapsulesmb/app/helper.py Outdated
args = parser.parse_args(argv)
sink = _sink_for_stream(sys.stdout).with_request_id(str(uuid.uuid4()))

raw = sys.stdin.read()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

Reading the entire content of sys.stdin into memory without a size limit can lead to excessive memory consumption or Denial of Service (DoS) if the input is unexpectedly large. It is safer to specify a reasonable maximum size for the expected JSON request.

    # Limit request size to 1MB to prevent memory exhaustion
    raw = sys.stdin.read(1024 * 1024)

Comment on lines +29 to +38
def jsonable(value: object) -> object:
if is_dataclass(value):
return jsonable(asdict(value))
if isinstance(value, Path):
return str(value)
if isinstance(value, dict):
return {str(key): jsonable(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set)):
return [jsonable(item) for item in value]
return value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The jsonable helper function does not handle enum.Enum types. Since many models in the project likely use Enums, and asdict() on a dataclass preserves Enum members, json.dumps() will fail when encountering them. Adding explicit Enum handling ensures the function is robust for all project models.

Suggested change
def jsonable(value: object) -> object:
if is_dataclass(value):
return jsonable(asdict(value))
if isinstance(value, Path):
return str(value)
if isinstance(value, dict):
return {str(key): jsonable(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set)):
return [jsonable(item) for item in value]
return value
def jsonable(value: object) -> object:
from enum import Enum
if is_dataclass(value):
return jsonable(asdict(value))
if isinstance(value, Enum):
return jsonable(value.value)
if isinstance(value, Path):
return str(value)
if isinstance(value, dict):
return {str(key): jsonable(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set)):
return [jsonable(item) for item in value]
return value

@jamesyc
Copy link
Copy Markdown
Owner Author

jamesyc commented May 20, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 175195804b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

def int_param(params: dict[str, object], name: str, default: int) -> int:
value = params.get(name, default)
try:
parsed = int(value)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject boolean values for integer API parameters

int_param currently accepts JSON booleans because int(True)/int(False) are valid in Python, so a request like {"mount_wait": true} is silently treated as 1 instead of being rejected. This creates hard-to-diagnose behavior (e.g., near-immediate timeouts) and is inconsistent with optional_int_param/float_param, which already reject booleans. The helper API should validate booleans as invalid integer input.

Useful? React with 👍 / 👎.

Label("Activate", systemImage: "power")
}
.disabled(backend.isRunning)
runButton("Uninstall Plan", icon: "xmark.bin", operation: "uninstall", params: ["dry_run": .bool(true)])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include no_reboot in uninstall dry-run request

The uninstall dry-run button ignores the UI's No Reboot toggle by sending only dry_run=true, so the generated plan always assumes reboot is required (no_reboot defaults to false in the backend). This means users can review a plan that does not match the operation they intend to run, which is especially risky for destructive maintenance flows.

Useful? React with 👍 / 👎.

@jamesyc
Copy link
Copy Markdown
Owner Author

jamesyc commented May 20, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a structured app backend and a macOS GUI for TimeCapsuleSMB, enabling non-interactive integration and a graphical user interface. Key additions include a Swift-based macOS application, a new Python api command for handling structured requests, and updates to existing CLI tools to support JSON output and new operational flags like --no-wait. The review feedback identifies a missing validation for the path parameter in the repair operation and points out unreachable dead code in the SSH configuration logic.

sink.stage(operation, "validate_params")
config = load_optional_env_config(env_path=config_path(params))
args = argparse.Namespace(
path=Path(str(params["path"])) if params.get("path") else None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The path parameter is not validated here. If it's missing from params, params.get("path") will be None, and args.path will be None. This will cause run_repair_structured to fail with a SystemExit when resolve_scan_root can't find a path, which is then caught as a generic operation_failed error. It's better to validate the parameter and fail with a validation_failed error.

Using require_string_param would be a good way to handle this.

Suggested change
path=Path(str(params["path"])) if params.get("path") else None,
path=Path(require_string_param(params, "path")),

Comment thread src/timecapsulesmb/cli/set_ssh.py Outdated
Comment on lines +231 to +232
command_context.fail_with_error("No set-ssh action selected.")
return 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This code appears to be unreachable. The logic for should_enable and should_disable ensures that one of them is always true when no explicit mode (--enable or --disable) is selected. The if should_enable or if should_disable blocks seem to cover all possible paths and always return a value, so this error condition will never be hit. You should consider removing this dead code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant