Skip to content

UI upgrades notifications and manual check - #3

Merged
B87 merged 4 commits into
mainfrom
ui-uprages-notifications
Jan 7, 2026
Merged

B87 merged 4 commits into
mainfrom
ui-uprages-notifications

Conversation

@B87

@B87 B87 commented Jan 6, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Automatic background upgrade checks with configurable interval and manual check option
    • In-app upgrade notifications with dismiss, view release notes, and open release/download actions
    • New Upgrade settings tab showing current version, last check, and controls for auto-check and interval
    • Template-based workflow for creating Pub/Sub resources with preconfigured templates

✏️ Tip: You can customize this high-level summary in your review settings.

B87 added 2 commits January 7, 2026 00:19
- Added periodic upgrade checking mechanism in the App struct, including methods for checking updates and dismissing notifications.
- Integrated version management with the new version package, allowing the application to fetch and compare versions from GitHub.
- Updated application configuration to include fields for upgrade check settings, such as auto-check and last upgrade check timestamp.
- Enhanced frontend with UpgradeNotification component and settings for managing upgrade preferences.
- Updated Go modules to include necessary dependencies for version handling.

This commit lays the groundwork for notifying users about available updates and managing upgrade settings effectively.
@coderabbitai

coderabbitai Bot commented Jan 6, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Implements an automatic upgrade-check feature spanning backend (Go) and frontend (TypeScript/React): new version package, GitHub release fetching, semantic version comparison, periodic/background checks, event-driven notifications, Wails bindings, UI components for notifications and settings, build-time version injection, and unit tests.

Changes

Cohort / File(s) Summary
Version package (backend)
internal/version/version.go, internal/version/models.go, internal/version/github.go, internal/version/checker.go, internal/version/*.go tests
New package exposing SetVersion, GetVersion, GetReleasesURL, types GitHubRelease and UpdateInfo, FetchLatestRelease() (HTTP GitHub fetch), CheckForUpdates() (normalization, semantic compare via hashicorp/go-version), and unit tests for normalization, parsing, and comparison.
App integration & lifecycle (backend)
app.go, main.go, app_test.go
Adds public App methods GetCurrentVersion, CheckForUpdates, StartPeriodicUpgradeCheck, DismissUpgradeNotification, OpenReleasesPage; periodic scheduler, dismissal persistence, last-check updates, event emission upgrade:available, startup hook, and shutdown cleanup; unit tests covering version and upgrade-check state.
Config & models (backend)
internal/models/connection.go
Adds AppConfig fields: AutoCheckUpgrades, UpgradeCheckInterval, LastUpgradeCheck, DismissedUpgradeVersion and default values in NewDefaultConfig.
Build/workflow changes
.goreleaser.yaml, .github/workflows/release.yml, go.mod
Injects version into build via ldflags (including internal/version.Version), adds dependency github.com/hashicorp/go-version.
Frontend types & Wails bindings
frontend/src/types/upgrade.ts, frontend/wailsjs/go/main/App.d.ts, frontend/wailsjs/go/main/App.js, frontend/wailsjs/go/models.ts, frontend/src/wailsjs
New TS interfaces UpdateInfo, UpgradeSettings; generated Wails JS/TS bindings exposing new App methods to frontend and model wrappers.
Frontend components & UI integration
frontend/src/components/UpgradeNotification.tsx, frontend/src/components/Settings/UpgradeTab.tsx, frontend/src/App.tsx, frontend/src/components/SettingsDialog.tsx
New UpgradeNotification component listens for upgrade:available and offers Download/Dismiss/Check Again; UpgradeTab in settings manages auto-check toggle, interval, manual checks; App.tsx renders notification; SettingsDialog adds 'upgrade' tab.
Documentation & guidelines
.cursor/plans/upgrade_check_feature_implementation_7e439e70.plan.md, .cursor/rules/react-tailwind.mdc, .cursor/rules/wails.mdc, .cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md
Adds implementation plan for upgrade feature, updates React/Tailwind error-handling guidance, documents concurrency-safe access to App.config, and adds a separate template-system design doc.
Tests
internal/version/*_test.go, app_test.go
Adds unit tests for version behavior, GitHub response parsing, dev-build skipping, and app upgrade-check state; replaces heavier integration tests with lighter unit tests.

Sequence Diagram(s)

sequenceDiagram
    participant App as Go App
    participant Scheduler as Scheduler/Ticker
    participant Version as Version pkg
    participant GitHub as GitHub API
    participant Config as Config Manager
    participant Event as Runtime/Event
    participant Frontend as Frontend

    App->>Scheduler: StartPeriodicUpgradeCheck()
    Scheduler->>App: Tick -> trigger check
    App->>Version: CheckForUpdates()
    Version->>Version: GetVersion() / normalize
    Version->>GitHub: FetchLatestRelease()
    GitHub-->>Version: GitHubRelease JSON
    Version->>Version: parse & compare semver
    Version-->>App: UpdateInfo
    App->>Config: Read DismissedUpgradeVersion
    alt update available and not dismissed
        App->>Config: Persist LastUpgradeCheck
        App->>Event: Emit "upgrade:available" (payload UpdateInfo)
        Event->>Frontend: Notify
        Frontend->>Frontend: Show UpgradeNotification
    else no update or dismissed
        App->>App: No event
    end
Loading
sequenceDiagram
    participant User as User
    participant UI as Frontend UI
    participant Wails as Wails Bridge
    participant App as Go App
    participant Browser as Browser Runtime

    User->>UI: Open Settings -> Upgrade tab
    UI->>Wails: GetCurrentVersion()
    Wails->>App: GetCurrentVersion()
    App-->>Wails: "1.0.0"
    Wails-->>UI: "1.0.0"
    User->>UI: Click "Check for Updates"
    UI->>Wails: CheckForUpdates()
    Wails->>App: CheckForUpdates()
    App-->>Wails: UpdateInfo
    Wails-->>UI: UpdateInfo
    alt update available
        UI->>User: Show "Download" and "Dismiss"
        User->>UI: Click "Download"
        UI->>Wails: OpenReleasesPage(url)
        Wails->>App: OpenReleasesPage(url)
        App->>Browser: Open URL
    else dismiss
        User->>UI: Click "Dismiss"
        UI->>Wails: DismissUpgradeNotification(version)
        Wails->>App: DismissUpgradeNotification(version)
        App->>Config: Save dismissed version
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🐰
I hop through tags and tests so bright,
Fetch the latest release by moonlight,
Compare the versions, shout "there's new!"
A click, a dismiss—then back to chew,
Happy code, tidy home, upgrade in sight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'UI upgrades notifications and manual check' accurately reflects the primary changes: implementing upgrade notifications in the UI and adding manual upgrade check functionality.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🤖 Fix all issues with AI Agents
In @.cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md:
- Around line 1299-1351: CreateFromTemplate dereferences req.Overrides without
nil checks causing panics; guard uses of req.Overrides (e.g., reading
DisableDeadLetter and passing overrides into createTopic/createSubscription) by
first assigning a local var like overrides := req.Overrides and checking if
overrides != nil before accessing fields (e.g., if overrides != nil &&
overrides.DisableDeadLetter) or pass nil through to helper methods; update the
dead-letter creation branch and the subscription creation loop to use these
nil-safe checks so no field access occurs on a nil Overrides pointer while still
passing overrides to createTopic/createSubscription unchanged.

In @app.go:
- Around line 235-268: The scheduled upgrade-check goroutine can leak because
the ticker loop in scheduleNextUpgradeCheck (for range a.upgradeCheckTicker.C)
never exits and the time.AfterFunc timer isn’t stored so pending callbacks can’t
be cancelled; to fix, add fields to App (e.g., upgradeCheckDone chan struct{}
and upgradeCheckTimer *time.Timer), update scheduleNextUpgradeCheck to create a
fresh done channel under a.upgradeCheckMu, store the result of time.AfterFunc in
a.upgradeCheckTimer (so you can Stop() it if restarting), and run the periodic
goroutine with a select that exits when a.upgradeCheckDone is closed or when
ticker.C fires; update Disconnect (and any restart path) to stop and nil the
ticker, Stop() the timer if set, and close the done channel safely (only once)
while holding upgradeCheckMu so the goroutine can exit cleanly and no duplicate
callbacks remain.

In @frontend/src/components/Settings/UpgradeTab.tsx:
- Around line 88-95: The async handler handleAutoCheckChange lacks error
handling for GetConfigFileContent and SaveConfigFileContent; wrap the async
operations in a try/catch, keep calling setAutoCheckUpgrades immediately, and on
failure revert state or show feedback (e.g., call an existing notify/error
handler) and log the error; specifically update handleAutoCheckChange to catch
exceptions from GetConfigFileContent and SaveConfigFileContent, call the
appropriate UI error notification, and ensure state consistency if saving fails.
- Around line 97-104: handleIntervalChange performs multiple async steps
(GetConfigFileContent, JSON.parse, SaveConfigFileContent) without error
handling; wrap the body of handleIntervalChange in a try/catch, keep calling
setUpgradeCheckInterval on success, and in the catch log or surface the error
(e.g., use your existing logger or show a user-facing error) and optionally
revert state if needed; ensure errors from GetConfigFileContent, JSON.parse, and
SaveConfigFileContent are caught and handled to avoid unhandled promise
rejections.
- Line 112: The code is masking a type mismatch by using setCheckResult(result
as any); instead of properly typing the value returned by CheckForUpdates(); fix
by aligning the state type with the Wails-generated type: either change the
checkResult state to use useState<version.UpdateInfo | null>(null) and keep
using the Wails result, or import the Wails-generated UpdateInfo type/class and
use that for the checkResult state and any related local typings; update any
references to checkResult and setCheckResult accordingly so no "as any"
assertions remain and the types match CheckForUpdates()'s
Promise<version.UpdateInfo>.

In @frontend/src/components/UpgradeNotification.tsx:
- Around line 3-11: The import list in UpgradeNotification.tsx includes
GetCurrentVersion which is unused; remove GetCurrentVersion from the named
import list that currently imports GetCurrentVersion, CheckForUpdates,
DismissUpgradeNotification, OpenReleasesPage from '../wailsjs/go/main/App' so
only the used functions remain (e.g., CheckForUpdates,
DismissUpgradeNotification, OpenReleasesPage), update the import statement
accordingly to eliminate the unused symbol and clean up lint warnings.
- Around line 13-14: The component UpgradeNotification destructures theme from
useTheme() but never uses it; remove the unused variable by deleting "const {
theme } = useTheme();" and, if useTheme is no longer used elsewhere in the file,
also remove the corresponding import (useTheme) to avoid dead code and lint
warnings.

In @internal/version/checker_test.go:
- Around line 93-96: The test "dev with suffix" uses input "123-dev" but
CheckForUpdates() in internal/version/checker.go detects dev builds only via
currentVersion == "dev" || strings.HasPrefix(strings.ToLower(currentVersion),
"dev"), so the test is invalid; either remove or update the test case to use a
matching dev form (e.g., "dev" or "dev-..."), or change the detection logic in
CheckForUpdates() to also treat versions with a trailing "-dev" (e.g., use
strings.Contains(strings.ToLower(currentVersion), "dev") or check for suffix
"-dev") so "123-dev" is recognized as a dev build; update tests accordingly
(adjust the case named "dev with suffix" or the CheckForUpdates() dev-detection
branch).
🧹 Nitpick comments (17)
.cursor/rules/wails.mdc (1)

262-315: Document mutex strategy: clarify whether config access uses dedicated or shared mutex.

The new CRITICAL section on config field access in goroutines is helpful, but introduces upgradeCheckMu as a dedicated mutex for upgrade-check config fields. However, the existing codebase pattern uses shared mutexes (resourceMu for resources, etc.).

Consider documenting whether:

  1. Option A (preferred for consistency): All config field access uses a single configMu mutex
  2. Option B (current pattern): Different subsystems get dedicated mutexes (upgradeCheckMu, monitorsMu, etc.)

If Option B is intentional, add a note explaining the rationale.

🔎 Consider consolidating to single configMu for consistency
- a.upgradeCheckMu.Lock()
- dismissedVersion := ""
- if a.config != nil {
-     dismissedVersion = a.config.DismissedUpgradeVersion
- }
- a.upgradeCheckMu.Unlock()
+ a.configMu.Lock()
+ dismissedVersion := ""
+ if a.config != nil {
+     dismissedVersion = a.config.DismissedUpgradeVersion
+ }
+ a.configMu.Unlock()

This aligns with the stated pattern in the wails.mdc: "Use sync.RWMutex for read-heavy shared state."

.cursor/plans/upgrade_check_feature_implementation_7e439e70.plan.md (2)

1-168: Clarify task status semantics in planning document.

The YAML frontmatter marks tasks as "completed" but this is a planning document. Consider using statuses like "planned" or "ready-to-implement" to distinguish planning completeness from actual implementation completion.

This is a documentation improvement that would reduce confusion when referencing the plan later.


204-437: Fix markdown list indentation for consistency.

Static analysis flags MD007 (unordered list indentation) and MD005 (inconsistent indentation) throughout the document. The lists should use consistent 2-space indentation per markdownlint rules.

While this doesn't affect functionality, fixing indentation improves document consistency:

- **File**: `internal/version/version.go`
+ - **File**: `internal/version/version.go`
-    - Define version constants (GitHubOwner: "B87", GitHubRepo: "pubsub-gui")
+   - Define version constants (GitHubOwner: "B87", GitHubRepo: "pubsub-gui")

Use mdformat or similar tool to auto-fix list indentation.

.cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md (2)

442-456: Data models are comprehensive but one field name needs clarification.

The template data models cover all major Pub/Sub settings appropriately. However, DeadLetterTemplateConfig.MessageRetentionDuration2 (line 454) is confusing.

Suggest renaming for clarity:

- MessageRetentionDuration2 string `json:"messageRetentionDuration2"`
+ DeadLetterSubMessageRetention string `json:"deadLetterSubMessageRetention"`

This makes it clear this is the message retention for the dead-letter subscription, not the topic.


141-141: Add language specifiers to fenced code blocks for markdown consistency.

Static analysis flags MD040 (missing language specifier) on several code blocks. While this doesn't affect functionality, it improves readability and consistency:

- ```
+ ```plaintext
 delay = min(min_backoff * 2^attempt, max_backoff)
- ```
+ ```

- ```
+ ```plaintext
 attributes.region = "us-west1"
- ```
+ ```

This is purely cosmetic but helps syntax highlighters and markdown processors handle the content correctly.

Also applies to: 187-187, 224-224, 310-310, 341-341, 1820-1820

internal/version/github_test.go (3)

79-99: Empty test body does not validate actual behavior.

TestFetchLatestRelease_SkipDraft and TestFetchLatestRelease_SkipPrerelease (lines 79-121) contain empty if blocks that don't assert anything meaningful. These tests only verify that the struct fields are set correctly, not that FetchLatestRelease actually skips drafts/prereleases. This is already covered more comprehensively by TestFetchLatestRelease_DraftPrereleaseSkipping.

Consider removing these redundant tests or refactoring to actually call FetchLatestRelease with a mock server returning draft/prerelease releases to verify the error is returned.


307-361: Test does not verify actual FetchLatestRelease User-Agent behavior.

This test manually sets the User-Agent header (line 350) and verifies the server receives it, but it doesn't actually call FetchLatestRelease(). The test simulates what the function does rather than testing the function itself.

Additionally, line 311's defer SetVersion("dev") won't reset the version to empty because SetVersion ignores empty strings but accepts "dev" as a valid value - this is fine but the comment implies it's restoring a default state.

Since FetchLatestRelease uses a hardcoded URL from GetReleasesURL(), consider refactoring to accept an optional URL parameter or HTTP client for better testability.


205-248: Error handling test doesn't actually test FetchLatestRelease.

The test simulates error scenarios that FetchLatestRelease would encounter but doesn't call the actual function. The comment on lines 213-215 acknowledges this limitation.

Also, line 209 ignores the return value of w.Write().

🔎 Minor fix for unchecked write
-			w.Write([]byte(tt.responseBody))
+			_, _ = w.Write([]byte(tt.responseBody))
internal/version/github.go (1)

49-52: Consider using a sentinel error for draft/prerelease detection.

The current error message is descriptive but upstream callers cannot easily distinguish this "expected" scenario (no stable release available) from actual failures. A sentinel error would allow callers to handle this case differently.

🔎 Suggested improvement
// At package level:
var ErrNoStableRelease = errors.New("latest release is draft or prerelease")

// In function:
if release.Draft || release.Prerelease {
    return nil, ErrNoStableRelease
}
frontend/src/components/UpgradeNotification.tsx (1)

24-32: Extract duplicate UpdateInfo conversion logic.

The conversion from version.UpdateInfo to UpdateInfo is duplicated in both the event handler (lines 24-32) and handleCheckNow (lines 80-88).

🔎 Proposed refactor
// Add helper function at the top of the component or as a utility
const mapToUpdateInfo = (data: version.UpdateInfo): UpdateInfo => ({
  currentVersion: data.currentVersion,
  latestVersion: data.latestVersion,
  releaseNotes: data.releaseNotes,
  releaseUrl: data.releaseUrl,
  publishedAt: data.publishedAt,
  isUpdateAvailable: data.isUpdateAvailable,
});

// Then use in both places:
setUpdateInfo(mapToUpdateInfo(data));

Also applies to: 80-88

frontend/src/components/SettingsDialog.tsx (1)

499-512: Consider adding footer handling for the 'upgrade' tab.

The footer conditionally shows buttons for 'appearance', 'connections', and 'advanced' tabs, but the new 'upgrade' tab is not included. This means no footer buttons appear when on the upgrade tab.

Looking at the UpgradeTab implementation (from relevant snippets), it handles its own "Check for Updates Now" button internally. If this is intentional, consider adding a simple "Close" button in the footer for consistency, or explicitly document that the upgrade tab manages its own actions.

🔎 Suggested addition
-            {(activeTab === 'appearance' || activeTab === 'connections') && (
+            {(activeTab === 'appearance' || activeTab === 'connections' || activeTab === 'upgrade') && (
frontend/src/types/upgrade.ts (1)

1-17: Consider exporting from types/index.ts per coding guidelines.

Per coding guidelines: "Define all TypeScript types and interfaces in types/index.ts. Export them for use across components."

The types are correctly defined and align with the backend structs, but they should ideally be exported from types/index.ts for consistency with the project structure.

Additionally, note that frontend/wailsjs/go/models.ts (lines 253-274 in relevant snippets) already generates an UpdateInfo class from the Go struct. Consider whether you need this separate interface or can use the Wails-generated type directly.

app_test.go (3)

51-52: Inconsistent config initialization pattern.

This test initializes app.config with an empty AppConfig{}, while TestCheckForUpdates_UpdatesLastUpgradeCheck (line 83) uses models.NewDefaultConfig(). The empty initialization might miss required fields with non-zero defaults defined in NewDefaultConfig().

🔎 Proposed fix
-	// Initialize minimal config for test
-	app.config = &models.AppConfig{}
+	// Initialize config with defaults
+	app.config = models.NewDefaultConfig()

91-93: Confusing double-negative logic.

The condition !initialTime.IsZero() && !initialTime.Before(time.Now()) is logically correct but hard to parse due to double negatives. Consider a more readable assertion.

🔎 Clearer alternative
-	if !initialTime.IsZero() && !initialTime.Before(time.Now()) {
-		t.Error("initial LastUpgradeCheck should be zero or in the past")
+	if !initialTime.IsZero() && initialTime.After(time.Now()) {
+		t.Error("initial LastUpgradeCheck should be zero or in the past")
 	}

104-121: Unclear test validation logic.

The test comment states "the actual version check will fail" (line 113), but the assertion logic (lines 117-120) accepts both error and success cases. This makes it unclear what behavior is actually being validated beyond "doesn't panic."

Consider clarifying the test's purpose: Is it testing graceful nil configManager handling, or dev-build behavior, or network failure resilience?

frontend/src/components/Settings/UpgradeTab.tsx (1)

69-86: Dead code: saveSettings function is never called.

The saveSettings function is defined but unused. Settings are saved directly in handleAutoCheckChange and handleIntervalChange instead. Either remove this function or refactor the handlers to use it.

🔎 Proposed refactor

Remove the unused function:

-  const saveSettings = async () => {
-    setError('');
-    try {
-      const content = await GetConfigFileContent();
-      const config = JSON.parse(content);
-
-      config.autoCheckUpgrades = autoCheckUpgrades;
-      config.upgradeCheckInterval = upgradeCheckInterval;
-
-      const updatedContent = JSON.stringify(config, null, 2);
-      await SaveConfigFileContent(updatedContent);
-
-      // Reload to get updated last check time
-      await loadSettings();
-    } catch (e: any) {
-      setError('Failed to save settings: ' + e.toString());
-    }
-  };
-

Or refactor handlers to use it (after adding error handling to those handlers).

app.go (1)

320-328: Consider adding URL scheme validation.

The URL is expected to come from GitHub API responses, which should be trustworthy. However, as defense-in-depth, consider validating that the URL uses an expected scheme (https://).

🔎 Optional: Add URL scheme validation
+import "strings"

 func (a *App) OpenReleasesPage(url string) error {
 	if url == "" {
 		return fmt.Errorf("URL cannot be empty")
 	}
+	if !strings.HasPrefix(url, "https://") {
+		return fmt.Errorf("invalid URL scheme: only HTTPS URLs are allowed")
+	}
 	runtime.BrowserOpenURL(a.ctx, url)
 	return nil
 }

Comment thread .cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md
Comment thread app.go
Comment thread frontend/src/components/Settings/UpgradeTab.tsx
Comment thread frontend/src/components/Settings/UpgradeTab.tsx
setCheckResult(null);
try {
const result = await CheckForUpdates();
setCheckResult(result as any);

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check UpdateInfo type definition in bindings vs local types
echo "=== Wails binding type for CheckForUpdates ==="
rg -A 20 "CheckForUpdates" frontend/wailsjs/go/main/App.d.ts | head -40

echo -e "\n=== Local UpdateInfo type ==="
rg -A 10 "interface UpdateInfo" frontend/src/types/

Repository: B87/pubsub-gui

Length of output: 1454


🏁 Script executed:

#!/bin/bash
# Find version.UpdateInfo definition in Wails bindings
echo "=== Searching for version.UpdateInfo in Wails bindings ==="
find frontend/wailsjs -name "*.d.ts" -o -name "*.ts" | xargs rg -l "version" | head -5

echo -e "\n=== Looking for UpdateInfo in version namespace ==="
rg "UpdateInfo" frontend/wailsjs/ | head -20

echo -e "\n=== Context around line 112 in UpgradeTab.tsx ==="
sed -n '100,125p' frontend/src/components/Settings/UpgradeTab.tsx

Repository: B87/pubsub-gui

Length of output: 1309


🏁 Script executed:

#!/bin/bash
echo "=== version.UpdateInfo class definition ==="
rg -A 30 "export class UpdateInfo" frontend/wailsjs/go/models.ts

Repository: B87/pubsub-gui

Length of output: 839


🏁 Script executed:

#!/bin/bash
echo "=== Finding checkResult state declaration ==="
sed -n '1,100p' frontend/src/components/Settings/UpgradeTab.tsx | rg -A 2 "checkResult"

Repository: B87/pubsub-gui

Length of output: 268


Remove as any type assertion and properly type the result.

The CheckForUpdates() method returns Promise<version.UpdateInfo> (from Wails bindings), but checkResult state expects UpdateInfo interface. While both have identical field structures, using as any masks a real type mismatch between the generated class and the local interface.

Change to either:

  • Update state type to useState<version.UpdateInfo | null>(null), or
  • Import and use the Wails-generated UpdateInfo class instead of the local interface

This ensures type safety and aligns with the coding guideline to use proper type conversion for Wails objects rather than bypassing TypeScript checks.

🤖 Prompt for AI Agents
In @frontend/src/components/Settings/UpgradeTab.tsx at line 112, The code is
masking a type mismatch by using setCheckResult(result as any); instead of
properly typing the value returned by CheckForUpdates(); fix by aligning the
state type with the Wails-generated type: either change the checkResult state to
use useState<version.UpdateInfo | null>(null) and keep using the Wails result,
or import the Wails-generated UpdateInfo type/class and use that for the
checkResult state and any related local typings; update any references to
checkResult and setCheckResult accordingly so no "as any" assertions remain and
the types match CheckForUpdates()'s Promise<version.UpdateInfo>.

Comment thread frontend/src/components/UpgradeNotification.tsx Outdated
Comment thread frontend/src/components/UpgradeNotification.tsx Outdated
Comment thread internal/version/checker_test.go
- Introduced a timer and a done channel in the App struct to manage upgrade checks more effectively, allowing for clean exits of goroutines.
- Updated the scheduleNextUpgradeCheck method to stop existing timers and handle channel closures safely.
- Improved error handling in the UpgradeTab component for auto-check and interval settings, ensuring UI state reverts on failure.
- Refactored the UpgradeNotification component by removing unnecessary theme imports.

These changes improve the robustness of the upgrade checking process and enhance user experience in managing upgrade settings.
@greptile-apps

greptile-apps Bot commented Jan 7, 2026

Copy link
Copy Markdown

Greetile Summary

  • Implements comprehensive upgrade notification system with automatic background checking, in-app notifications, and manual check functionality
  • Adds new Upgrade settings tab allowing users to configure automatic check intervals and view current version information
  • Includes extensive backend infrastructure with GitHub API integration, version comparison logic, and proper concurrent access patterns

Important Files Changed

Filename Overview
frontend/src/wailsjs New symbolic link to wailsjs directory - conflicts with Wails auto-generation patterns and could interfere with TypeScript module resolution
app.go Major enhancement adding upgrade check infrastructure with automatic background checking, notification management, and proper concurrency handling
internal/version/checker.go New core version checking logic with GitHub API integration and semantic version comparison using hashicorp/go-version library
frontend/src/components/UpgradeNotification.tsx New notification component implementing fixed-position upgrade alerts with theme compliance and proper event handling
frontend/src/components/Settings/UpgradeTab.tsx New settings tab for upgrade configuration with auto-check toggles and manual update checking functionality

Confidence score: 2/5

  • This PR introduces a substantial upgrade notification system with proper architecture and implementation
  • Critical issue: symbolic link frontend/src/wailsjs conflicts with Wails auto-generation patterns and could cause build/import problems
  • Pay close attention to the symlink which breaks established Wails conventions and requires immediate removal

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant Version
    participant GitHub
    participant Frontend
    participant Browser

    User->>App: "Start Application"
    App->>App: "startup()"
    App->>App: "StartPeriodicUpgradeCheck()"
    
    alt config.AutoCheckUpgrades == true
        App->>App: "scheduleNextUpgradeCheck()"
        App->>App: "performUpgradeCheck()"
        App->>Version: "CheckForUpdates()"
        
        alt current version != "dev"
            Version->>GitHub: "FetchLatestRelease()"
            GitHub-->>Version: "GitHubRelease"
            Version->>Version: "Compare versions"
            
            alt update available
                Version-->>App: "UpdateInfo{isUpdateAvailable: true}"
                
                alt not dismissed
                    App->>Frontend: "EventsEmit('upgrade:available', updateInfo)"
                    Frontend->>Frontend: "Show UpgradeNotification"
                    
                    User->>Frontend: "Click Download Update"
                    Frontend->>App: "OpenReleasesPage(url)"
                    App->>Browser: "runtime.BrowserOpenURL()"
                    
                    User->>Frontend: "Click Dismiss"
                    Frontend->>App: "DismissUpgradeNotification(version)"
                    App->>App: "Save dismissed version to config"
                end
            end
        end
    end
Loading

@greptile-apps greptile-apps Bot 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.

30 files reviewed, 12 comments

Edit Code Review Agent Settings | Greptile

Comment thread frontend/src/wailsjs
@@ -0,0 +1 @@
../wailsjs No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: This symlink should be removed. Wails auto-generates bindings in frontend/wailsjs/ and components should import directly using ../wailsjs/go/main/App rather than creating symlinks that could interfere with the generation process.

Context Used: Context from dashboard - Wails v2 Application Development Guide (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/wailsjs
Line: 1:1

Comment:
**logic:** This symlink should be removed. Wails auto-generates bindings in `frontend/wailsjs/` and components should import directly using `../wailsjs/go/main/App` rather than creating symlinks that could interfere with the generation process.

**Context Used:** Context from `dashboard` - Wails v2 Application Development Guide ([source](https://app.greptile.com/review/custom-context?memory=2a6d6324-ea6d-4761-9f38-5ee98a08bfd4))

How can I resolve this? If you propose a fix, please make it concise.

Comment thread internal/models/connection.go Outdated
Templates []MessageTemplate `json:"templates"` // Message templates
AutoCheckUpgrades bool `json:"autoCheckUpgrades"`
UpgradeCheckInterval int `json:"upgradeCheckInterval"` // hours
LastUpgradeCheck time.Time `json:"lastUpgradeCheck,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style: Consider using time.Duration instead of int for UpgradeCheckInterval to make the units explicit and type-safe

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/models/connection.go
Line: 35:35

Comment:
**style:** Consider using `time.Duration` instead of `int` for UpgradeCheckInterval to make the units explicit and type-safe

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

import { useTheme } from '../hooks/useTheme';

export default function UpgradeNotification() {
const { theme } = useTheme();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style: The theme variable is imported from useTheme() but never used in the component logic.

Suggested change
const { theme } = useTheme();
const {} = useTheme();

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/UpgradeNotification.tsx
Line: 14:14

Comment:
**style:** The `theme` variable is imported from `useTheme()` but never used in the component logic.

```suggestion
  const {} = useTheme();
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

setError('');
try {
await OpenReleasesPage(updateInfo.releaseUrl);
setError('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style: Redundant error clearing - setError('') is already called on line 65.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/UpgradeNotification.tsx
Line: 68:68

Comment:
**style:** Redundant error clearing - `setError('')` is already called on line 65.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

};
setUpdateInfo(updateInfo);
setIsVisible(true);
setError('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style: Redundant error clearing - setError('') is already called on line 76.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/UpgradeNotification.tsx
Line: 91:91

Comment:
**style:** Redundant error clearing - `setError('')` is already called on line 76.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +117 to +120
if mockPrerelease.Draft || mockPrerelease.Prerelease {
// This is the condition that causes FetchLatestRelease to return an error
// We're testing the logic, not the actual HTTP call
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style: Same issue as above - empty conditional block. The comment explains the intent but no actual validation occurs.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/version/github_test.go
Line: 117:120

Comment:
**style:** Same issue as above - empty conditional block. The comment explains the intent but no actual validation occurs.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Prerelease: false,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(release)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Missing error check for json.NewEncoder(w).Encode(release) - should handle potential encoding errors in test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/version/github_test.go
Line: 335:335

Comment:
**logic:** Missing error check for json.NewEncoder(w).Encode(release) - should handle potential encoding errors in test.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread app_test.go
Comment on lines +1 to 139
package main

import (
"context"
"os"
"path/filepath"
"testing"
"time"

"pubsub-gui/test"
"pubsub-gui/internal/config"
"pubsub-gui/internal/models"
)

// setupIntegrationTestApp creates an App instance configured for integration testing with emulator
func setupIntegrationTestApp(t *testing.T) (*App, func()) {
t.Helper()

// Start emulator
_, cleanupEmulator := test.StartEmulator(t)

// Setup test config directory
_, cleanupConfig := test.SetupTestConfigDir(t)

// Create app
func TestGetCurrentVersion(t *testing.T) {
app := NewApp()
// Use context with longer timeout for integration tests (60 seconds for emulator operations)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
t.Cleanup(cancel)
app.startup(ctx)

cleanup := func() {
app.Disconnect()
cleanupConfig()
cleanupEmulator()
tests := []struct {
name string
version string
want string
}{
{
name: "set version v1.0.0",
version: "v1.0.0",
want: "v1.0.0",
},
{
name: "set version 1.2.3",
version: "1.2.3",
want: "1.2.3",
},
{
name: "empty version returns dev",
version: "",
want: "dev",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app.SetVersion(tt.version)
got := app.GetCurrentVersion()
if got != tt.want {
t.Errorf("GetCurrentVersion() = %q, want %q", got, tt.want)
}
})
}

return app, cleanup
}

// TestConnectWithADC_Integration tests connecting to emulator using ADC
func TestConnectWithADC_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()
func TestCheckForUpdates_DevBuild(t *testing.T) {
app := NewApp()
app.SetVersion("dev")

projectID := "test-project"
// Initialize minimal config for test
app.config = &models.AppConfig{}

err := app.ConnectWithADC(projectID)
got, err := app.CheckForUpdates()
if err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
t.Errorf("CheckForUpdates() error = %v, want nil", err)
return
}

// Verify connection status
status := app.GetConnectionStatus()
if !status.IsConnected {
t.Error("ConnectWithADC() IsConnected = false, want true")
}
if status.ProjectID != projectID {
t.Errorf("ConnectWithADC() ProjectID = %q, want %q", status.ProjectID, projectID)
if got == nil {
t.Fatal("CheckForUpdates() = nil, want UpdateInfo")
}
if status.EmulatorHost == "" {
t.Error("ConnectWithADC() EmulatorHost is empty, should be set when using emulator")
}
}

// TestPublishMessage_Integration tests publishing messages using emulator
func TestPublishMessage_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
if got.CurrentVersion != "dev" {
t.Errorf("CheckForUpdates() CurrentVersion = %q, want %q", got.CurrentVersion, "dev")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic
topicID := "test-topic"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

// Publish message
payload := `{"test": "data"}`
attributes := map[string]string{
"key1": "value1",
"key2": "value2",
}

got, err := app.PublishMessage(topicID, payload, attributes)
if err != nil {
t.Fatalf("PublishMessage() error = %v", err)
}

if got.MessageID == "" {
t.Error("PublishMessage() MessageID is empty")
}
if got.Timestamp == "" {
t.Error("PublishMessage() Timestamp is empty")
}

// Verify timestamp is valid RFC3339
_, err = time.Parse(time.RFC3339, got.Timestamp)
if err != nil {
t.Errorf("PublishMessage() Timestamp = %q, want valid RFC3339 format", got.Timestamp)
if got.IsUpdateAvailable {
t.Errorf("CheckForUpdates() IsUpdateAvailable = %v, want false for dev builds", got.IsUpdateAvailable)
}
}

// TestCreateTopic_Integration tests topic creation with emulator
func TestCreateTopic_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic
topicID := "integration-test-topic"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

// Verify topic exists by getting metadata
topicInfo, err := app.GetTopicMetadata(topicID)
if err != nil {
t.Fatalf("GetTopicMetadata() error = %v", err)
}

if topicInfo.Name == "" {
t.Error("GetTopicMetadata() Name is empty")
}
func TestCheckForUpdates_UpdatesLastUpgradeCheck(t *testing.T) {
app := NewApp()
app.SetVersion("v1.0.0")

// Verify topic appears in list
topics, err := app.ListTopics()
// Initialize config manager and config
configMgr, err := config.NewManager()
if err != nil {
t.Fatalf("ListTopics() error = %v", err)
}

found := false
for _, topic := range topics {
if topic.Name == topicInfo.Name {
found = true
break
}
t.Fatalf("failed to create config manager: %v", err)
}
if !found {
t.Error("CreateTopic() topic not found in ListTopics()")
}
}

// TestDeleteTopic_Integration tests topic deletion with emulator
func TestDeleteTopic_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()
app.configManager = configMgr
app.config = models.NewDefaultConfig()

projectID := "test-project"
// Mock version.CheckForUpdates to return a result without making HTTP call
// Since we can't easily mock the version package, we'll test the timestamp update logic
initialTime := app.config.LastUpgradeCheck

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
// Note: This test would require mocking version.CheckForUpdates or making it testable
// For now, we verify the field exists and can be set
if !initialTime.IsZero() && !initialTime.Before(time.Now()) {
t.Error("initial LastUpgradeCheck should be zero or in the past")
}

// Create topic
topicID := "integration-test-topic-delete"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}
// Verify the field can be updated
newTime := time.Now()
app.config.LastUpgradeCheck = newTime

// Delete topic
if err := app.DeleteTopic(topicID); err != nil {
t.Fatalf("DeleteTopic() error = %v", err)
}

// Verify topic is deleted
_, err := app.GetTopicMetadata(topicID)
if err == nil {
t.Error("DeleteTopic() topic still exists after deletion")
if app.config.LastUpgradeCheck != newTime {
t.Errorf("LastUpgradeCheck = %v, want %v", app.config.LastUpgradeCheck, newTime)
}
}

// TestCreateSubscription_Integration tests subscription creation with emulator
func TestCreateSubscription_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic
topicID := "integration-test-topic-sub"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

// Create subscription
subID := "integration-test-sub"
ttlSeconds := int64(3600) // 1 hour
if err := app.CreateSubscription(topicID, subID, ttlSeconds); err != nil {
t.Fatalf("CreateSubscription() error = %v", err)
}

// Verify subscription exists
subInfo, err := app.GetSubscriptionMetadata(subID)
if err != nil {
t.Fatalf("GetSubscriptionMetadata() error = %v", err)
}

if subInfo.Name == "" {
t.Error("GetSubscriptionMetadata() Name is empty")
}

// Verify subscription appears in list
subscriptions, err := app.ListSubscriptions()
if err != nil {
t.Fatalf("ListSubscriptions() error = %v", err)
}

found := false
for _, sub := range subscriptions {
if sub.Name == subInfo.Name {
found = true
break
}
}
if !found {
t.Error("CreateSubscription() subscription not found in ListSubscriptions()")
}
}

// TestDeleteSubscription_Integration tests subscription deletion with emulator
func TestDeleteSubscription_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic and subscription
topicID := "integration-test-topic-sub-delete"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

subID := "integration-test-sub-delete"
if err := app.CreateSubscription(topicID, subID, 3600); err != nil {
t.Fatalf("CreateSubscription() error = %v", err)
}
func TestCheckForUpdates_ErrorHandling(t *testing.T) {
app := NewApp()
app.SetVersion("v1.0.0")
app.config = models.NewDefaultConfig()

// Delete subscription
if err := app.DeleteSubscription(subID); err != nil {
t.Fatalf("DeleteSubscription() error = %v", err)
}
// Test with nil config manager (should not panic, just log warning)
app.configManager = nil

// Verify subscription is deleted
_, err := app.GetSubscriptionMetadata(subID)
// This should handle the nil config manager gracefully
// The actual version check will fail, but we test error handling
_, err := app.CheckForUpdates()
// Error is expected since we can't actually reach GitHub API in unit test
// We're just verifying it doesn't panic
if err == nil {
t.Error("DeleteSubscription() subscription still exists after deletion")
}
}

// TestStartMonitor_Integration tests message monitoring with emulator
func TestStartMonitor_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic and subscription
topicID := "integration-test-topic-monitor"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

subID := "integration-test-sub-monitor"
if err := app.CreateSubscription(topicID, subID, 3600); err != nil {
t.Fatalf("CreateSubscription() error = %v", err)
}

// Start monitoring
if err := app.StartMonitor(subID); err != nil {
t.Fatalf("StartMonitor() error = %v", err)
}

// Publish a message
payload := `{"test": "monitor"}`
if _, err := app.PublishMessage(topicID, payload, nil); err != nil {
t.Fatalf("PublishMessage() error = %v", err)
}

// Wait a bit for message to be received
time.Sleep(1 * time.Second)

// Get buffered messages
messages, err := app.GetBufferedMessages(subID)
if err != nil {
t.Fatalf("GetBufferedMessages() error = %v", err)
}

if len(messages) == 0 {
t.Error("StartMonitor() no messages received")
} else {
// Verify message content
msg := messages[0]
if msg.ID == "" {
t.Error("StartMonitor() message ID is empty")
}
if string(msg.Data) != payload {
t.Errorf("StartMonitor() message Data = %q, want %q", string(msg.Data), payload)
}
}

// Stop monitoring
if err := app.StopMonitor(subID); err != nil {
t.Fatalf("StopMonitor() error = %v", err)
}
}

// TestStartTopicMonitor_Integration tests topic monitoring with auto-created subscription
func TestStartTopicMonitor_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic
topicID := "integration-test-topic-monitor-auto"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

// Start topic monitoring (auto-creates subscription)
if err := app.StartTopicMonitor(topicID, ""); err != nil {
t.Fatalf("StartTopicMonitor() error = %v", err)
}

// Publish a message
payload := `{"test": "topic-monitor"}`
if _, err := app.PublishMessage(topicID, payload, nil); err != nil {
t.Fatalf("PublishMessage() error = %v", err)
}

// Wait a bit for message to be received
time.Sleep(1 * time.Second)

// Verify subscription was created by listing subscriptions
subscriptions, err := app.ListSubscriptions()
if err != nil {
t.Fatalf("ListSubscriptions() error = %v", err)
}

// Verify at least one subscription exists (the auto-created one)
if len(subscriptions) == 0 {
t.Error("StartTopicMonitor() no subscription created")
}

// Stop topic monitoring (should clean up subscription)
if err := app.StopTopicMonitor(topicID); err != nil {
t.Fatalf("StopTopicMonitor() error = %v", err)
// If no error, that's fine - dev build or network succeeded
// We just want to ensure no panic
}
}

// TestDisconnect_Integration tests disconnect with active monitors
func TestDisconnect_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic and subscription
topicID := "integration-test-topic-disconnect"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

subID := "integration-test-sub-disconnect"
if err := app.CreateSubscription(topicID, subID, 3600); err != nil {
t.Fatalf("CreateSubscription() error = %v", err)
}

// Start monitoring
if err := app.StartMonitor(subID); err != nil {
t.Fatalf("StartMonitor() error = %v", err)
}

// Start topic monitoring
if err := app.StartTopicMonitor(topicID, ""); err != nil {
t.Fatalf("StartTopicMonitor() error = %v", err)
}

// Disconnect
if err := app.Disconnect(); err != nil {
t.Fatalf("Disconnect() error = %v", err)
}

// Verify connection is closed
status := app.GetConnectionStatus()
if status.IsConnected {
t.Error("Disconnect() IsConnected = true, want false")
}

// Verify resource cache is cleared
topics, err := app.ListTopics()
if err != nil {
t.Fatalf("ListTopics() error = %v", err)
}
if len(topics) != 0 {
t.Errorf("Disconnect() topics length = %d, want 0 (cache cleared)", len(topics))
}
}

// TestStartup_Integration tests full startup sequence with emulator
func TestStartup_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

// Start emulator
_, cleanupEmulator := test.StartEmulator(t)
defer cleanupEmulator()

// Setup test config with active profile
configDir, cleanupConfig := test.SetupTestConfigDir(t)
defer cleanupConfig()

configPath := filepath.Join(configDir, ".pubsub-gui", "config.json")
configJSON := `{
"profiles": [{
"id": "test-profile",
"name": "Test Profile",
"projectId": "test-project",
"authMethod": "ADC",
"isDefault": true,
"createdAt": "2024-01-01T00:00:00Z"
}],
"activeProfileId": "test-profile",
"messageBufferSize": 500,
"autoAck": true,
"theme": "auto",
"fontSize": "medium",
"templates": []
}`

if err := os.WriteFile(configPath, []byte(configJSON), 0644); err != nil {
t.Fatalf("failed to write config: %v", err)
}

// Create app and startup
func TestApp_UpgradeCheckFields(t *testing.T) {
app := NewApp()
ctx := context.Background()
app.startup(ctx)

// Verify handlers are initialized
if app.resources == nil {
t.Error("startup() resources handler is nil")
}
if app.connection == nil {
t.Error("startup() connection handler is nil")
}
if app.templates == nil {
t.Error("startup() templates handler is nil")
}
if app.monitoring == nil {
t.Error("startup() monitoring handler is nil")
}
if app.configH == nil {
t.Error("startup() config handler is nil")
}

// Verify config is loaded
if app.config == nil {
t.Error("startup() config is nil")
// Verify upgrade check fields are initialized
if app.upgradeCheckTicker != nil {
t.Error("upgradeCheckTicker should be nil initially")
}

// Verify auto-connect attempted (may succeed or fail, but should not crash)
status := app.GetConnectionStatus()
// Status may be connected or not, but should not panic
_ = status
// Verify mutex can be used
app.upgradeCheckMu.Lock()
app.lastUpgradeCheck = time.Now()
app.upgradeCheckMu.Unlock()

// Cleanup
app.Disconnect()
if app.lastUpgradeCheck.IsZero() {
t.Error("lastUpgradeCheck should be set after lock/unlock")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Complete replacement of integration tests with unit tests removes important test coverage. Integration tests validated real Pub/Sub operations (connection, topic/subscription CRUD, monitoring) which are core app functionality that should be preserved. Were the integration tests moved to another file, or are they permanently removed? The original tests covered critical Pub/Sub functionality.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app_test.go
Line: 1:139

Comment:
**logic:** Complete replacement of integration tests with unit tests removes important test coverage. Integration tests validated real Pub/Sub operations (connection, topic/subscription CRUD, monitoring) which are core app functionality that should be preserved. Were the integration tests moved to another file, or are they permanently removed? The original tests covered critical Pub/Sub functionality.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread app_test.go
Comment on lines +85 to +90
// Mock version.CheckForUpdates to return a result without making HTTP call
// Since we can't easily mock the version package, we'll test the timestamp update logic
initialTime := app.config.LastUpgradeCheck

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
// Note: This test would require mocking version.CheckForUpdates or making it testable
// For now, we verify the field exists and can be set

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

style: Comments indicate the test is incomplete due to inability to mock the version package. Consider using dependency injection or interfaces to make this testable without external HTTP calls.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: app_test.go
Line: 85:90

Comment:
**style:** Comments indicate the test is incomplete due to inability to mock the version package. Consider using dependency injection or interfaces to make this testable without external HTTP calls.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment thread app_test.go
Comment on lines +110 to 120
app.configManager = nil

// Verify subscription is deleted
_, err := app.GetSubscriptionMetadata(subID)
// This should handle the nil config manager gracefully
// The actual version check will fail, but we test error handling
_, err := app.CheckForUpdates()
// Error is expected since we can't actually reach GitHub API in unit test
// We're just verifying it doesn't panic
if err == nil {
t.Error("DeleteSubscription() subscription still exists after deletion")
}
}

// TestStartMonitor_Integration tests message monitoring with emulator
func TestStartMonitor_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic and subscription
topicID := "integration-test-topic-monitor"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

subID := "integration-test-sub-monitor"
if err := app.CreateSubscription(topicID, subID, 3600); err != nil {
t.Fatalf("CreateSubscription() error = %v", err)
}

// Start monitoring
if err := app.StartMonitor(subID); err != nil {
t.Fatalf("StartMonitor() error = %v", err)
}

// Publish a message
payload := `{"test": "monitor"}`
if _, err := app.PublishMessage(topicID, payload, nil); err != nil {
t.Fatalf("PublishMessage() error = %v", err)
}

// Wait a bit for message to be received
time.Sleep(1 * time.Second)

// Get buffered messages
messages, err := app.GetBufferedMessages(subID)
if err != nil {
t.Fatalf("GetBufferedMessages() error = %v", err)
}

if len(messages) == 0 {
t.Error("StartMonitor() no messages received")
} else {
// Verify message content
msg := messages[0]
if msg.ID == "" {
t.Error("StartMonitor() message ID is empty")
}
if string(msg.Data) != payload {
t.Errorf("StartMonitor() message Data = %q, want %q", string(msg.Data), payload)
}
}

// Stop monitoring
if err := app.StopMonitor(subID); err != nil {
t.Fatalf("StopMonitor() error = %v", err)
}
}

// TestStartTopicMonitor_Integration tests topic monitoring with auto-created subscription
func TestStartTopicMonitor_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

app, cleanup := setupIntegrationTestApp(t)
defer cleanup()

projectID := "test-project"

// Connect to emulator
if err := app.ConnectWithADC(projectID); err != nil {
t.Fatalf("ConnectWithADC() error = %v", err)
}

// Create topic
topicID := "integration-test-topic-monitor-auto"
if err := app.CreateTopic(topicID, ""); err != nil {
t.Fatalf("CreateTopic() error = %v", err)
}

// Start topic monitoring (auto-creates subscription)
if err := app.StartTopicMonitor(topicID, ""); err != nil {
t.Fatalf("StartTopicMonitor() error = %v", err)
}

// Publish a message
payload := `{"test": "topic-monitor"}`
if _, err := app.PublishMessage(topicID, payload, nil); err != nil {
t.Fatalf("PublishMessage() error = %v", err)
}

// Wait a bit for message to be received
time.Sleep(1 * time.Second)

// Verify subscription was created by listing subscriptions
subscriptions, err := app.ListSubscriptions()
if err != nil {
t.Fatalf("ListSubscriptions() error = %v", err)
}

// Verify at least one subscription exists (the auto-created one)
if len(subscriptions) == 0 {
t.Error("StartTopicMonitor() no subscription created")
}

// Stop topic monitoring (should clean up subscription)
if err := app.StopTopicMonitor(topicID); err != nil {
t.Fatalf("StopTopicMonitor() error = %v", err)
// If no error, that's fine - dev build or network succeeded
// We just want to ensure no panic
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

logic: Error handling test allows both success and error cases, making it unclear what's actually being validated. The test comment suggests it's just checking for no panic, but the assertion logic is confusing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app_test.go
Line: 110:120

Comment:
**logic:** Error handling test allows both success and error cases, making it unclear what's actually being validated. The test comment suggests it's just checking for no panic, but the assertion logic is confusing.

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 0

🧹 Nitpick comments (5)
internal/version/checker.go (1)

13-18: Minor logic redundancy in isDevBuild.

Line 15 checks version == "dev" but this is already covered by strings.HasPrefix(lower, "dev") on line 16. The explicit check is redundant.

🔎 Suggested simplification
 func isDevBuild(version string) bool {
 	lower := strings.ToLower(version)
-	return version == "dev" ||
-		strings.HasPrefix(lower, "dev") ||
+	return strings.HasPrefix(lower, "dev") ||
 		strings.HasSuffix(lower, "-dev")
 }
.cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md (1)

141-143: Add language specifiers to fenced code blocks.

Several code blocks are missing language specifiers, which affects syntax highlighting and documentation quality. Consider adding appropriate language identifiers (e.g., text, plaintext, or the actual language).

Static analysis flagged lines 141, 187, 224, 310, 341, and 1823 as missing language specifiers.

Also applies to: 187-192, 224-234, 310-337, 341-353, 1823-1855

frontend/src/components/Settings/UpgradeTab.tsx (1)

258-293: Add aria-label to the check button for better accessibility.

The button contains dynamic content (spinner vs icon) and could benefit from an explicit aria-label for screen readers.

🔎 Suggested improvement
 <button
   onClick={handleCheckNow}
   disabled={checking || saving}
+  aria-label={checking ? 'Checking for updates' : 'Check for updates now'}
   style={{
     backgroundColor: 'var(--color-accent-primary)',

Based on learnings, interactive components should have aria-label for accessibility.

app.go (2)

169-198: Consider extracting version synchronization to a single location.

CheckForUpdates sets the version in the version package (versionpkg.SetVersion(a.GetVersion())) before calling versionpkg.CheckForUpdates(). This creates a coupling where the version must be set before each check. Consider setting the version once during startup (in main.go or startup()) to avoid this repeated synchronization.

 func (a *App) CheckForUpdates() (*versionpkg.UpdateInfo, error) {
-	// Set version in version package so CheckForUpdates can use it
-	versionpkg.SetVersion(a.GetVersion())
-
 	// Call version.CheckForUpdates()
 	updateInfo, err := versionpkg.CheckForUpdates()

Then in startup() or main.go:

versionpkg.SetVersion(app.GetVersion())

366-374: Consider validating URL format in OpenReleasesPage.

The function validates that URL is not empty, but doesn't validate that it's a proper URL or specifically a GitHub releases URL. Consider adding validation to prevent potential security issues from arbitrary URL opening.

🔎 Suggested improvement
+import (
+    "net/url"
+    "strings"
+)

 func (a *App) OpenReleasesPage(url string) error {
 	if url == "" {
 		return fmt.Errorf("URL cannot be empty")
 	}
+	
+	// Validate URL format
+	parsedURL, err := url.Parse(url)
+	if err != nil {
+		return fmt.Errorf("invalid URL: %w", err)
+	}
+	
+	// Optionally restrict to GitHub releases URLs for security
+	if !strings.HasPrefix(parsedURL.Host, "github.com") {
+		return fmt.Errorf("URL must be a GitHub releases URL")
+	}
+	
 	runtime.BrowserOpenURL(a.ctx, url)
 	return nil
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d341ed and 5621822.

📒 Files selected for processing (6)
  • .cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md
  • app.go
  • frontend/src/components/Settings/UpgradeTab.tsx
  • frontend/src/components/UpgradeNotification.tsx
  • internal/models/connection.go
  • internal/version/checker.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/UpgradeNotification.tsx
🧰 Additional context used
📓 Path-based instructions (8)
**/*.go

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.go: Run go fmt ./... on all Go code before committing
Use log.Printf() for debugging Go code
Use the race detector with go test -race ./... to debug race conditions
Initialize resource slices to empty arrays instead of nil to prevent race conditions
Always lock shared state with a.resourceMu.RLock() and a.resourceMu.RUnlock() when accessing resources
Use goroutines with proper cleanup via context cancellation (ctx.Done()) and channels for message streaming
Emit events to frontend using runtime.EventsEmit(a.ctx, "event:name", payload) for real-time updates

**/*.go: Use PascalCase for exported Go methods (e.g., GetConnectionStatus, PublishMessage) to follow Go conventions and ensure proper method exposure
Use camelCase for private Go helper methods (e.g., connectWithProfile, findExistingMonitoringSubscription)
Always return errors from Go methods, even when they may be optional in some contexts
Use custom errors from internal/models/errors.go for domain errors and wrap errors with context using fmt.Errorf
All return types from exposed methods must be JSON-serializable with proper json tags using camelCase for field names, omitempty for optional fields, and json:"-" to exclude fields
Always use the stored context (a.ctx) for API calls instead of creating new contexts with context.Background()
Emit events to notify frontend of state changes using runtime.EventsEmit(a.ctx, eventName, payload) with resource:action format and kebab-case event names
Use sync.RWMutex for read-heavy shared state, RLock() for read operations, Lock() for write operations, and minimize lock duration by unlocking before expensive operations
Normalize resource IDs when needed by backend methods before calling admin functions that construct full paths internally - extract short ID from full resource name if provided
Trigger background resource synchronization after mutations using go a.syncResources() and emit resources:updated event whe...

Files:

  • internal/models/connection.go
  • internal/version/checker.go
  • app.go
internal/models/connection.go

📄 CodeRabbit inference engine (.cursor/rules/theme-system.mdc)

internal/models/connection.go: AppConfig struct must include Theme field with values: 'light', 'dark', 'auto', 'dracula', 'monokai' and FontSize field with values: 'small', 'medium', 'large'
Default theme configuration must be Theme: 'auto' and FontSize: 'medium' as defined in the AppConfig struct

Files:

  • internal/models/connection.go
internal/models/**/*.go

📄 CodeRabbit inference engine (.cursor/rules/wails.mdc)

Define custom domain error types and reusable error variables in internal/models/errors.go for consistent error handling across the application

Files:

  • internal/models/connection.go
app.go

📄 CodeRabbit inference engine (CLAUDE.md)

Define new Go methods that need to be exposed to the frontend on the App struct in app.go

app.go: Store context.Context as a field (set in startup()), use handler structs to organize methods by domain, initialize managers and handlers in startup() lifecycle hook, and clean up resources in Disconnect() and OnShutdown
All public methods on App struct are automatically exposed to frontend via Wails bindings

Files:

  • app.go
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.tsx: Never use hardcoded colors (hex, rgb values) in React components; use semantic CSS variables like var(--color-bg-primary) instead
Test React components with all 5 themes (Auto, Dark, Light, Dracula, Monokai) before committing
Add aria-label or visible text to all interactive elements for accessibility
Use useMemo for expensive computations to optimize React component performance
Use @tanstack/react-virtual for lists with 100+ items to implement virtual scrolling
Use useTheme() hook to access theme and fontSize from ThemeContext in React components
Ensure keyboard navigation works (Tab, Enter, Escape) and focus states are visible in interactive components

**/*.tsx: Import Go methods from auto-generated Wails bindings in wailsjs/go/main/App and call them as async functions
Set up backend event listeners using EventsOn() in useEffect with empty dependency array, store unsubscribe functions, and call them in cleanup to prevent memory leaks
Use try/catch blocks for all async Wails method calls, clear state on error to prevent stale data, and display user-friendly error messages
Use useRef to access latest state in event handlers without triggering unnecessary re-renders
Use useMemo to memoize filtered results from full resource lists to avoid unnecessary recalculations and for instant local filtering without API calls
Pass full resource lists as props to child components and filter locally using useMemo instead of making backend API calls for relationship queries
Listen for resources:updated events to receive synchronized resource lists and update state directly without calling manual load functions

Files:

  • frontend/src/components/Settings/UpgradeTab.tsx
frontend/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use EventsOn/EventsOff from wailsjs/runtime/runtime to listen to backend events; unsubscribe in useEffect cleanup

Files:

  • frontend/src/components/Settings/UpgradeTab.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import Go methods from ../wailsjs/go/main/App after running wails dev to generate bindings

**/*.{ts,tsx}: Use PascalCase for component names and file names (e.g., TopicDetails.tsx, ConnectionDialog.tsx)
Use ComponentNameProps interface naming convention for component props (e.g., TopicDetailsProps)
Always import types from types/index.ts and use TypeScript interfaces for component props
CRITICAL: Always use semantic CSS variables (var(--color-*)) for all color styling via inline styles or custom CSS classes - never use hardcoded colors
Use Tailwind CSS classes only as fallback for layout, spacing, and sizing when semantic styling isn't practical
CRITICAL: All user-facing errors must be displayed in the UI - never rely solely on console.error. Convert technical errors to user-friendly messages
CRITICAL: Always await all async operations that update state. Never call async functions without awaiting them
When switching connections or profiles: immediately clear dependent state (resources, selections) before any async operations, then verify connection, then load resources
Prefer manual user actions (buttons) over automatic actions for operations that consume resources, create/modify resources, or start background processes
CRITICAL: Test all components visually with all 5 themes (Dark, Light, Dracula, Monokai, Auto) to ensure theme variables are working correctly
Use useMemo for expensive computations and useCallback for stable function references to optimize performance
Always use semantic HTML and add ARIA labels where needed to ensure accessibility
Unsubscribe from Wails events in useEffect cleanup functions using the unsubscribe callback returned by EventsOn
Import Wails methods using: import { MethodName } from '../wailsjs/go/main/App' and call with await
When passing complex objects to Wails methods, convert plain TypeScript objects using main.TypeName.createFrom() from Wails-generated classes
Use useRef to track state values in ev...

Files:

  • frontend/src/components/Settings/UpgradeTab.tsx
frontend/src/components/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/react-tailwind.mdc)

frontend/src/components/**/*.tsx: Keep components focused and small. Extract reusable logic into custom hooks
One component per file. File name must match component name in PascalCase

frontend/src/components/**/*.tsx: React components should NOT hardcode theme-specific color values; instead use semantic CSS variables (var(--color-bg-primary), etc.) or backward-compatible Tailwind classes
When accessing current theme in React components, use effectiveTheme (resolves 'auto' to actual theme) instead of theme for conditional logic that depends on actual theme values
Do not use useTheme() hook outside of ThemeProvider wrapped components as it will throw an error

Files:

  • frontend/src/components/Settings/UpgradeTab.tsx
🧠 Learnings (19)
📓 Common learnings
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/react-tailwind.mdc:0-0
Timestamp: 2026-01-06T22:28:40.355Z
Learning: React 18 with TypeScript is the framework for this desktop application. Wails v2 handles Go backend + React frontend integration
📚 Learning: 2026-01-06T22:29:17.712Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/theme-system.mdc:0-0
Timestamp: 2026-01-06T22:29:17.712Z
Learning: Applies to internal/models/connection.go : AppConfig struct must include Theme field with values: 'light', 'dark', 'auto', 'dracula', 'monokai' and FontSize field with values: 'small', 'medium', 'large'

Applied to files:

  • internal/models/connection.go
📚 Learning: 2026-01-06T22:29:17.712Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/theme-system.mdc:0-0
Timestamp: 2026-01-06T22:29:17.712Z
Learning: Applies to internal/models/connection.go : Default theme configuration must be Theme: 'auto' and FontSize: 'medium' as defined in the AppConfig struct

Applied to files:

  • internal/models/connection.go
📚 Learning: 2026-01-06T22:27:51.811Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T22:27:51.811Z
Learning: Applies to app.go : Define new Go methods that need to be exposed to the frontend on the `App` struct in `app.go`

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:29:41.057Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/wails.mdc:0-0
Timestamp: 2026-01-06T22:29:41.057Z
Learning: Applies to app.go : All public methods on `App` struct are automatically exposed to frontend via Wails bindings

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:29:41.057Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/wails.mdc:0-0
Timestamp: 2026-01-06T22:29:41.057Z
Learning: Applies to app.go : Store `context.Context` as a field (set in `startup()`), use handler structs to organize methods by domain, initialize managers and handlers in `startup()` lifecycle hook, and clean up resources in `Disconnect()` and `OnShutdown`

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:27:51.811Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T22:27:51.811Z
Learning: Applies to **/*.go : Use goroutines with proper cleanup via context cancellation (`ctx.Done()`) and channels for message streaming

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:29:41.057Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/wails.mdc:0-0
Timestamp: 2026-01-06T22:29:41.057Z
Learning: Applies to main.go : Use Wails lifecycle hooks (`OnStartup`, `OnShutdown`) for initialization and cleanup, storing context in `startup()` and cleaning up in `OnShutdown`

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:27:51.811Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T22:27:51.811Z
Learning: Applies to **/*.go : Emit events to frontend using `runtime.EventsEmit(a.ctx, "event:name", payload)` for real-time updates

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:29:41.057Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/wails.mdc:0-0
Timestamp: 2026-01-06T22:29:41.057Z
Learning: Applies to **/*.go : Emit events to notify frontend of state changes using `runtime.EventsEmit(a.ctx, eventName, payload)` with `resource:action` format and kebab-case event names

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:29:17.712Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/theme-system.mdc:0-0
Timestamp: 2026-01-06T22:29:17.712Z
Learning: Applies to internal/app/**/*.go : Config file location is ~/.pubsub-gui/config.json with permissions 0600 (user read/write only), and must be auto-created with defaults if missing

Applied to files:

  • app.go
📚 Learning: 2026-01-06T22:28:40.355Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/react-tailwind.mdc:0-0
Timestamp: 2026-01-06T22:28:40.355Z
Learning: Applies to **/*.{ts,tsx} : Use Radix UI components (tabs, dialog, dropdown-menu, select, label, separator) for accessible, unstyled primitives. Style them with semantic CSS variables

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:27:51.811Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-06T22:27:51.811Z
Learning: Applies to **/*.tsx : Ensure keyboard navigation works (Tab, Enter, Escape) and focus states are visible in interactive components

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:28:40.355Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/react-tailwind.mdc:0-0
Timestamp: 2026-01-06T22:28:40.355Z
Learning: Applies to **/*.{ts,tsx} : CRITICAL: Always `await` all async operations that update state. Never call async functions without awaiting them

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:29:41.057Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/wails.mdc:0-0
Timestamp: 2026-01-06T22:29:41.057Z
Learning: Applies to **/*.tsx : Use try/catch blocks for all async Wails method calls, clear state on error to prevent stale data, and display user-friendly error messages

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:29:41.057Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/wails.mdc:0-0
Timestamp: 2026-01-06T22:29:41.057Z
Learning: Applies to **/*.ts : Use try/catch blocks for all async Wails method calls, clear state on error to prevent stale data, and display user-friendly error messages

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:28:40.355Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/react-tailwind.mdc:0-0
Timestamp: 2026-01-06T22:28:40.355Z
Learning: Applies to **/*.{ts,tsx} : CRITICAL: All user-facing errors must be displayed in the UI - never rely solely on console.error. Convert technical errors to user-friendly messages

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:28:40.355Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/react-tailwind.mdc:0-0
Timestamp: 2026-01-06T22:28:40.355Z
Learning: Applies to **/*.{ts,tsx} : Prefer manual user actions (buttons) over automatic actions for operations that consume resources, create/modify resources, or start background processes

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
📚 Learning: 2026-01-06T22:28:40.355Z
Learnt from: CR
Repo: B87/pubsub-gui PR: 0
File: .cursor/rules/react-tailwind.mdc:0-0
Timestamp: 2026-01-06T22:28:40.355Z
Learning: Applies to **/*.{ts,tsx} : CRITICAL: Verify connection status before loading resources after switching connections or profiles using `GetConnectionStatus()`

Applied to files:

  • frontend/src/components/Settings/UpgradeTab.tsx
🧬 Code graph analysis (2)
internal/version/checker.go (5)
frontend/wailsjs/go/main/App.js (2)
  • CheckForUpdates (5-7)
  • GetVersion (93-95)
frontend/src/types/upgrade.ts (1)
  • UpdateInfo (3-10)
internal/version/models.go (1)
  • UpdateInfo (18-25)
internal/version/version.go (1)
  • GetVersion (29-34)
internal/version/github.go (1)
  • FetchLatestRelease (14-55)
app.go (6)
frontend/wailsjs/go/main/App.js (4)
  • StartPeriodicUpgradeCheck (137-139)
  • GetVersion (93-95)
  • CheckForUpdates (5-7)
  • SetVersion (129-131)
internal/version/version.go (2)
  • GetVersion (29-34)
  • SetVersion (21-25)
internal/version/checker.go (1)
  • CheckForUpdates (23-69)
frontend/wailsjs/go/models.ts (1)
  • UpdateInfo (254-275)
internal/version/models.go (1)
  • UpdateInfo (18-25)
frontend/wailsjs/runtime/runtime.js (2)
  • EventsEmit (59-62)
  • BrowserOpenURL (180-182)
🪛 markdownlint-cli2 (0.18.1)
.cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md

141-141: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


187-187: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


224-224: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


310-310: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


341-341: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


1823-1823: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build (macos-15, darwin/arm64)
  • GitHub Check: Build (ubuntu-latest, linux/amd64)
  • GitHub Check: Build (windows-latest, windows/amd64)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (14)
internal/version/checker.go (2)

23-69: Well-structured update check implementation.

The CheckForUpdates function correctly:

  • Handles dev builds by returning early with no update available
  • Normalizes versions before comparison
  • Uses hashicorp/go-version for proper semantic version comparison
  • Provides context-rich error messages for failures
  • Returns complete UpdateInfo with all relevant fields

71-78: LGTM!

The normalizeVersion function correctly handles both 'v' and 'V' prefixes and trims whitespace.

internal/models/connection.go (1)

70-85: LGTM!

The NewDefaultConfig function correctly initializes all new upgrade-related fields with sensible defaults:

  • AutoCheckUpgrades: true enables automatic checks by default
  • UpgradeCheckInterval: 24 (hours) is a reasonable default
  • LastUpgradeCheck: time.Time{} (zero value) correctly indicates no previous check
  • DismissedUpgradeVersion: "" correctly indicates no dismissed version

Based on learnings, Theme and FontSize defaults are also correct (auto and medium).

.cursor/tasks/TOPIC_SUBSCRIPTION_TEMPLATES.md (2)

1-5: This is an implementation plan document, not production code.

This file is a detailed planning/specification document for a future template system feature. The code examples within are illustrative and not executed. Review comments focus on documentation quality.


1317-1323: Nil pointer dereference risk has been addressed.

The code now correctly checks overrides == nil || !overrides.DisableDeadLetter at line 1323, which addresses the previously flagged nil pointer dereference concern. The local variable assignment at line 1318 (overrides := req.Overrides) combined with the nil-safe check provides proper protection.

frontend/src/components/Settings/UpgradeTab.tsx (4)

27-31: Consider adding cleanup for settings load.

The useEffect calls loadSettings() and loadCurrentVersion() on mount, but these are fire-and-forget async calls. While this works, the pattern of calling async functions without tracking their promises in useEffect is fine here since they handle their own state updates and errors.


88-108: Error handling has been properly implemented.

The handleAutoCheckChange function now includes:

  • Optimistic UI update with state revert on error
  • try/catch wrapping async Wails calls
  • User-friendly error messages displayed in UI
  • Console logging for debugging

This addresses the previously flagged concern.


110-130: Error handling has been properly implemented.

The handleIntervalChange function now includes proper error handling with optimistic updates and revert on failure, addressing the previously flagged concern.


156-360: LGTM - Component follows theming guidelines.

The component correctly uses semantic CSS variables throughout (var(--color-bg-primary), var(--color-text-primary), var(--color-accent-primary), etc.) instead of hardcoded colors, which aligns with the coding guidelines. The UI provides clear feedback for all states (loading, checking, success, error).

app.go (5)

47-54: LGTM - Proper fields for upgrade check lifecycle management.

The new fields provide proper state management for the periodic upgrade check mechanism:

  • upgradeCheckMu for thread-safe access
  • upgradeCheckTicker and upgradeCheckTimer for scheduling
  • upgradeCheckDone channel for clean goroutine shutdown

140-143: LGTM - Periodic upgrade check integrated into startup.

The StartPeriodicUpgradeCheck is appropriately called during startup, after config is loaded and handlers are initialized.


237-314: Goroutine leak issue has been properly addressed.

The implementation now correctly handles goroutine lifecycle:

  1. Stops existing ticker and timer before creating new ones (lines 244-253)
  2. Uses a done channel with safe close pattern to signal goroutine exit (lines 257-264)
  3. The periodic goroutine uses select to listen for both done signal and ticker (lines 301-311)
  4. Local variable done is captured in closure to avoid race conditions (line 270)

The safe close pattern using select prevents panic if the channel is already closed.


316-343: LGTM - Silent failure is appropriate for background checks.

The performUpgradeCheck function silently logs errors instead of notifying users, which is appropriate for background periodic checks. The dismissed version check correctly prevents repeated notifications for the same version.


418-438: LGTM - Proper cleanup in Disconnect.

The cleanup logic correctly:

  1. Stops ticker and timer with nil assignment
  2. Safely closes the done channel using select to avoid panic
  3. Sets done channel to nil after closing

This ensures clean shutdown without goroutine leaks.

@B87
B87 merged commit b4d300a into main Jan 7, 2026
5 of 6 checks passed
@B87
B87 deleted the ui-uprages-notifications branch January 7, 2026 00:40
@coderabbitai coderabbitai Bot mentioned this pull request Jan 7, 2026
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