Skip to content

Conversation

@joaomariolago
Copy link
Collaborator

@joaomariolago joaomariolago commented Dec 19, 2025

Not connected to Cloud

image

Connected to Cloud

image

No Files

image

Pending FIles + Last Uploaded

image image

Summary by Sourcery

Add cloud file upload tracking and status display to the Cloud tray menu, including a dedicated uploads tab and mission link.

New Features:

  • Show a badge on the cloud tray icon indicating the number of pending file uploads.
  • Introduce a tabbed Cloud tray layout separating account settings from upload status.
  • Display real-time cloud file sync activity, including active, pending, and recently completed uploads, with progress details and a link to missions in BlueOS Cloud.

Enhancements:

  • Refactor cloud settings UI into a dedicated reusable tab component.
  • Integrate Zenoh-based session management to query and subscribe to Major Tom file sync topics for upload tracking.

Summary by Sourcery

Add cloud file upload tracking and status visibility to the Cloud tray menu, including a dedicated uploads view and Zenoh-based file sync integration.

New Features:

  • Show a numeric badge on the Cloud tray icon indicating the number of pending file uploads.
  • Add an "Uploads" tab in the Cloud tray that lists uploading, pending, and recently completed cloud file transfers with progress details.
  • Expose a direct link from the uploads view to missions in BlueOS Cloud.

Enhancements:

  • Refactor the Cloud tray account/setup UI into a reusable CloudSettingsTab component and introduce a CloudSyncStatusTab for displaying sync activity.
  • Integrate a Zenoh session to query and subscribe to Major Tom file sync topics, maintaining local state for pending, uploading, and completed files.
  • Display a concise cloud sync summary message in the Cloud tray header based on current upload activity.

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 19, 2025

Reviewer's Guide

Implements cloud file upload tracking and a new tabbed Cloud tray UI, wiring Zenoh-based file sync state into reusable Account and Uploads tabs with badge + status summaries.

Sequence diagram for Zenoh-based file sync tracking in CloudTrayMenu

sequenceDiagram
  actor User
  participant CloudTrayMenu
  participant ZenohSession
  participant MajorTomFileSyncService
  participant CloudSyncStatusTab

  User->>CloudTrayMenu: Open_cloud_tray_menu
  CloudTrayMenu->>CloudTrayMenu: menu_opened=true
  CloudTrayMenu->>CloudTrayMenu: initializeFileSyncTracking()

  alt Major_Tom_ready
    CloudTrayMenu->>CloudTrayMenu: ensureFileSyncSession()
    alt No_existing_session
      CloudTrayMenu->>ZenohSession: Session.open(Config(ws_or_wss_url))
      ZenohSession-->>CloudTrayMenu: file_sync_session
    else Session_already_open
      CloudTrayMenu-->>CloudTrayMenu: Reuse_existing_session
    end

    CloudTrayMenu->>CloudTrayMenu: fetchFileSyncQueues()
    CloudTrayMenu->>ZenohSession: get(pending_topic)
    ZenohSession-->>CloudTrayMenu: pending_queue_payload
    CloudTrayMenu->>CloudTrayMenu: parseFileSyncQueue()
    CloudTrayMenu->>ZenohSession: get(completed_topic)
    ZenohSession-->>CloudTrayMenu: completed_queue_payload
    CloudTrayMenu->>CloudTrayMenu: parseFileSyncQueue()

    CloudTrayMenu->>CloudTrayMenu: ensureUploadingSubscriber()
    CloudTrayMenu->>ZenohSession: declare_subscriber(uploading_topic)
    ZenohSession-->>CloudTrayMenu: uploading_subscriber
  else Major_Tom_not_ready
    CloudTrayMenu-->>CloudTrayMenu: initializeFileSyncTracking_returns
  end

  loop For_each_uploading_event
    MajorTomFileSyncService-->>ZenohSession: Publish_uploading_sample
    ZenohSession-->>CloudTrayMenu: Sample(payload)
    CloudTrayMenu->>CloudTrayMenu: handleUploadingSample(sample)
    CloudTrayMenu->>CloudTrayMenu: normalizeUploadingEvent(event)
    CloudTrayMenu->>CloudTrayMenu: resetUploadingIdleWatchdog()
    alt Transfer_not_completed
      CloudTrayMenu->>CloudTrayMenu: Update_cloud_uploading_transfers
    else Transfer_completed
      CloudTrayMenu->>CloudTrayMenu: Remove_from_cloud_uploading_transfers
      CloudTrayMenu->>CloudTrayMenu: removeFromPendingQueue(file)
      CloudTrayMenu->>CloudTrayMenu: scheduleFileSyncRefresh(1500_ms)
    end
    CloudTrayMenu-->>CloudSyncStatusTab: Re_render_with_updated_items_and_counts
  end

  loop Scheduled_refresh
    CloudTrayMenu->>CloudTrayMenu: fetchFileSyncQueues()
    CloudTrayMenu-->>CloudSyncStatusTab: Update_items_from_pending_and_completed
  end

  User->>CloudTrayMenu: Close_cloud_tray_menu_or_Major_Tom_stops
  CloudTrayMenu->>CloudTrayMenu: teardownFileSyncTracking()
  CloudTrayMenu->>ZenohSession: undeclare(uploading_subscriber)
  CloudTrayMenu->>ZenohSession: close()
  ZenohSession-->>CloudTrayMenu: Session_closed
Loading

Updated class diagram for Cloud tray menu, tabs, and file sync types

classDiagram

  class CloudTrayMenu {
    <<VueComponent>>
    +boolean menu_opened
    +number active_tab
    +boolean setting_token
    +string token
    +Session file_sync_session
    +Promise~Session~ file_sync_session_promise
    +Subscriber uploading_subscriber
    +FileSyncEntry[] cloud_pending_uploads
    +FileSyncEntry[] cloud_completed_uploads
    +Record~string,UploadingTransfer~ cloud_uploading_transfers
    +number file_sync_refresh_timeout
    +number uploading_idle_timeout
    +boolean is_major_tom_ready
    +boolean is_tray_visible
    +boolean is_cloud_setup_complete
    +UploadingTransfer[] cloud_active_uploads
    +FileSyncEntry[] cloud_pending_queue
    +FileSyncEntry[] cloud_recent_completed
    +number activeCount
    +number pendingCount
    +string statusSummary
    +string pendingBadgeLabel
    +CloudSyncDisplayItem[] cloud_sync_items
    +string blueos_cloud_url
    +string blueos_cloud_vehicles_url
    +string missions_url
    +mounted() Promise~void~
    +beforeDestroy() void
    +setUpTrayMenu() Promise~void~
    +initializeFileSyncTracking() Promise~void~
    +ensureFileSyncSession() Promise~boolean~
    +fetchFileSyncQueues() Promise~void~
    +queryFileSyncTopic(topic) Promise~FileSyncEntry[]|
    +parseFileSyncQueue(payload) FileSyncEntry[]
    +ensureUploadingSubscriber() Promise~void~
    +handleUploadingSample(sample) void
    +normalizeUploadingEvent(event) UploadingTransfer|
    +teardownFileSyncTracking() Promise~void~
    +normalizeFilePath(path) string
    +pathsReferSameFile(pathA,pathB) boolean
    +removeFromPendingQueue(canonicalPath) void
    +scheduleFileSyncRefresh(delay) void
    +clearScheduledFileSyncRefresh() void
    +resetUploadingIdleWatchdog(timeoutSeconds) void
    +clearUploadingIdleWatchdog() void
    +getFileName(path) string
    +formatFileSize(value) string
    +formatUploadProgress(progress) string
  }

  class CloudSettingsTab {
    <<VueComponent>>
    +boolean isTokenSet
    +boolean settingToken
    +string token
    +string blueosCloudUrl
    +string blueosCloudVehiclesUrl
    +$emit(toggle-token-input, boolean)
    +$emit(update:token, string)
    +$emit(submit-token)
  }

  class CloudSyncStatusTab {
    <<VueComponent>>
    +string summaryMessage
    +CloudSyncDisplayItem[] items
    +number activeCount
    +number pendingCount
    +string missionsUrl
    +getFileName(path) string
    +formatFileSize(value) string
    +formatUploadProgress(progress) string
  }

  class FileSyncEntry {
    +string path
    +number size
  }

  class UploadingTransfer {
    +string file
    +string display_path
    +number sent
    +number total
    +number timestamp
    +number progress
    +boolean completed
  }

  class CloudSyncDisplayItem {
    +string id
    +string path
    +number size
    +CloudSyncItemStatus status
    +string display_path
    +number sent
    +number total
    +number progress
  }

  class CloudSyncItemStatus {
    <<type_alias>>
    +uploading
    +pending
    +completed
  }

  class Session {
  }

  class Subscriber {
  }

  class Sample {
  }

  %% Relationships between components and types
  CloudTrayMenu --> CloudSettingsTab : uses_in_Account_tab
  CloudTrayMenu --> CloudSyncStatusTab : uses_in_Uploads_tab

  CloudTrayMenu "*" --> FileSyncEntry : manages_queues
  CloudTrayMenu "*" --> UploadingTransfer : tracks_active_uploads
  CloudTrayMenu "*" --> CloudSyncDisplayItem : builds_display_items

  CloudSyncStatusTab "*" --> CloudSyncDisplayItem : renders

  CloudSyncDisplayItem --> CloudSyncItemStatus : has_status

  CloudTrayMenu --> Session : opens_file_sync_session
  CloudTrayMenu --> Subscriber : declares_uploading_subscriber
  CloudTrayMenu --> Sample : processes_uploading_samples
Loading

File-Level Changes

Change Details Files
Add Zenoh-based file sync tracking and status aggregation in Cloud tray menu.
  • Import Zenoh client APIs and new Cloud sync types, and define file sync topic constants and missions URL.
  • Extend CloudTrayMenu state with Zenoh session/subscriber handles, pending/completed/uploading queues, and timeouts.
  • Add computed properties to derive active/pending counts, status summary text, tray badge label, and a flattened CloudSyncDisplayItem list for the UI.
  • Initialize file sync tracking when the tray opens or Major Tom becomes ready, and tear it down when Major Tom stops or the component is destroyed.
  • Implement helpers to open/close Zenoh sessions, query pending/completed queues, subscribe to uploading events, parse/normalize payloads, and maintain queues with refresh/watchdog timers.
  • Add small formatting helpers for file names, byte sizes, and progress percentage.
core/frontend/src/components/cloud/CloudTrayMenu.vue
core/frontend/src/components/cloud/types.ts
Refactor Cloud tray layout into tabbed Account and Uploads views with icon badges and status messaging.
  • Wrap the tray activator icon in a v-badge that shows a red pending-upload count when there are queued files.
  • Replace inline token/account UI with a CloudSettingsTab component, and show it alone when Cloud is not fully set up.
  • Introduce v-tabs/v-tabs-items to switch between Account and Uploads when setup is complete, resetting the active tab when menu closes.
  • Display a compact textual status summary in the Cloud tray app bar when Cloud setup is complete.
  • Add minimal styling for the tab strip border.
core/frontend/src/components/cloud/CloudTrayMenu.vue
core/frontend/src/components/cloud/CloudSettingsTab.vue
Introduce dedicated Cloud settings and upload-status tab components for reuse and clarity.
  • Create CloudSettingsTab to encapsulate the token/setup UI, emitting events to toggle token input, update the token, and submit it.
  • Create CloudSyncStatusTab to render aggregated CloudSyncDisplayItem entries with progress bars, status icons, counts, and a CTA button to missions.
  • Define shared TypeScript interfaces for file sync entries, upload transfers, and display items used across components.
core/frontend/src/components/cloud/CloudSettingsTab.vue
core/frontend/src/components/cloud/CloudSyncStatusTab.vue
core/frontend/src/components/cloud/types.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@joaomariolago joaomariolago force-pushed the improve-cloud-tray branch 3 times, most recently from aba561c to b63a81b Compare December 19, 2025 08:05
@joaomariolago joaomariolago marked this pull request as ready for review December 19, 2025 08:05
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • CloudTrayMenu.vue has grown quite large and now mixes UI concerns with fairly involved Zenoh file-sync session management; consider extracting the file-tracking logic (session setup, subscribers, queue parsing, timers) into a separate composable/service to keep the component more focused and easier to maintain.
  • The file-sync timeouts and intervals (e.g. scheduleFileSyncRefresh delay, resetUploadingIdleWatchdog timeoutSeconds) are currently hardcoded inline; pulling these into named constants or config would make the behavior easier to understand and adjust over time.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- CloudTrayMenu.vue has grown quite large and now mixes UI concerns with fairly involved Zenoh file-sync session management; consider extracting the file-tracking logic (session setup, subscribers, queue parsing, timers) into a separate composable/service to keep the component more focused and easier to maintain.
- The file-sync timeouts and intervals (e.g. scheduleFileSyncRefresh delay, resetUploadingIdleWatchdog timeoutSeconds) are currently hardcoded inline; pulling these into named constants or config would make the behavior easier to understand and adjust over time.

## Individual Comments

### Comment 1
<location> `core/frontend/src/components/cloud/CloudTrayMenu.vue:432-445` </location>
<code_context>
+        this.file_sync_session_promise = null
+      }
+    },
+    async fetchFileSyncQueues(): Promise<void> {
+      try {
+        const [pending, completed] = await Promise.all([
+          this.queryFileSyncTopic(MAJOR_TOM_FILE_SYNC_PENDING_TOPIC),
+          this.queryFileSyncTopic(MAJOR_TOM_FILE_SYNC_COMPLETED_TOPIC),
+        ])
+
+        if (pending) {
+          this.cloud_pending_uploads = pending
+        }
+
+        if (completed) {
+          this.cloud_completed_uploads = completed
+        }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Consider resetting pending/completed queues when queries fail to avoid stale UI state.

In `fetchFileSyncQueues`, if either `queryFileSyncTopic` call returns `null` (e.g., receiver void, query error, disconnect), the corresponding `cloud_*_uploads` array is never updated. A previously fetched value can then remain visible indefinitely when the backend is unavailable, implying uploads are still pending/completed. Consider clearing the arrays on failure or representing a distinct "unknown" state so the UI doesn’t display stale data when the queues can’t be queried.

```suggestion
    async fetchFileSyncQueues(): Promise<void> {
      try {
        const [pending, completed] = await Promise.all([
          this.queryFileSyncTopic(MAJOR_TOM_FILE_SYNC_PENDING_TOPIC),
          this.queryFileSyncTopic(MAJOR_TOM_FILE_SYNC_COMPLETED_TOPIC),
        ])

        // Reset queues when topics cannot be queried (null/undefined) to avoid stale UI
        this.cloud_pending_uploads = pending ?? []
        this.cloud_completed_uploads = completed ?? []
```
</issue_to_address>

### Comment 2
<location> `core/frontend/src/components/cloud/CloudTrayMenu.vue:528-536` </location>
<code_context>
+        return []
+      }
+    },
+    async ensureUploadingSubscriber(): Promise<void> {
+      if (this.uploading_subscriber || !this.file_sync_session) {
+        return
+      }
+
+      try {
+        this.uploading_subscriber = await this.file_sync_session.declare_subscriber(
+          MAJOR_TOM_FILE_SYNC_UPLOADING_TOPIC,
+          {
+            handler: async (sample: Sample) => {
+              this.handleUploadingSample(sample)
</code_context>

<issue_to_address>
**suggestion:** The subscriber handler is declared `async` but doesn't actually await anything, which adds unnecessary indirection.

The handler passed to `declare_subscriber` is currently `async (sample: Sample) => { this.handleUploadingSample(sample); return Promise.resolve() }`. Since `handleUploadingSample` is synchronous and you’re only returning `Promise.resolve()`, this should just be a synchronous function returning `void`, e.g. `handler: (sample: Sample) => { this.handleUploadingSample(sample) }`. This avoids an unnecessary promise wrapper and more accurately reflects the handler’s behavior.

```suggestion
          MAJOR_TOM_FILE_SYNC_UPLOADING_TOPIC,
          {
            handler: (sample: Sample) => {
              this.handleUploadingSample(sample)
            },
            history: true,
          },
        )
```
</issue_to_address>

### Comment 3
<location> `core/frontend/src/components/cloud/CloudTrayMenu.vue:597-606` </location>
<code_context>
+    async teardownFileSyncTracking(): Promise<void> {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** There is a potential race between teardown and reinitialization of file sync tracking.

Since `teardownFileSyncTracking` is async and invoked from watchers/beforeDestroy without `await`, it can run concurrently with `initializeFileSyncTracking` when `is_major_tom_ready` or `menu_opened` change rapidly. This can lead to reusing a session while it’s being closed or clearing queues right after `fetchFileSyncQueues` refills them. Consider adding a state flag (e.g. `file_sync_teardown_in_progress`) or serializing both methods through a shared promise so each waits for the other to finish.

Suggested implementation:

```
    async teardownFileSyncTracking(): Promise<void> {
      // If a teardown is already in progress, wait for it and return to avoid
      // concurrent teardown logic racing with initialization.
      if (this.file_sync_teardown_in_progress) {
        try {
          await this.file_sync_teardown_in_progress
        } catch {
          // Errors are already handled in the original teardown logic.
        }
        return
      }

      const teardownPromise = (async () => {
        this.clearScheduledFileSyncRefresh()
        this.clearUploadingIdleWatchdog()

        try {
          if (this.uploading_subscriber) {
            await this.uploading_subscriber.undeclare()
          }
        } catch (error) {
          console.warn('[CloudTrayMenu] Failed to undeclare uploading subscriber:', error)
        } finally {
          // NOTE: any additional teardown logic below will be part of the
          // serialized teardown operation.

```

To fully implement the race-avoidance strategy and align with the comment you left on the PR, the following additional changes are needed elsewhere in `CloudTrayMenu.vue`:

1. **Add state to track teardown in progress**
   - In the component's state definition (e.g. `data()`, `data` option, or `setup()`-equivalent reactive object), add:
   ```ts
   file_sync_teardown_in_progress: null as Promise<void> | null,
   ```
   - Initialize it to `null`.

2. **Wire the state into `teardownFileSyncTracking`**
   - Just before `const teardownPromise = (async () => { ...` in the replaced code, add:
   ```ts
   this.file_sync_teardown_in_progress = teardownPromise
   ```
   - After the `finally { ...` block inside the async IIFE, add:
   ```ts
   this.file_sync_teardown_in_progress = null
   ```
   - This ensures that `file_sync_teardown_in_progress` always points to the currently running teardown or `null`.

3. **Serialize `initializeFileSyncTracking` with teardown**
   - In `initializeFileSyncTracking` (or the equivalent initialization method for file sync tracking in this component), add at the top of the method:
   ```ts
   if (this.file_sync_teardown_in_progress) {
     try {
       await this.file_sync_teardown_in_progress
     } catch {
       // Ignore teardown errors here; they are handled in teardown.
     }
   }
   ```
   - This guarantees that initialization never overlaps with an ongoing teardown even if `teardownFileSyncTracking` is invoked without `await`.

4. **Ensure watchers respect the serialization**
   - For watchers reacting to `is_major_tom_ready`, `menu_opened`, or similar that call `initializeFileSyncTracking` or `teardownFileSyncTracking`:
     - Prefer `async` watcher handlers that `await` the relevant method.
     - If they must remain non-async, relying on the internal serialization via `file_sync_teardown_in_progress` is acceptable, as long as the state field and logic above are consistently applied.

These additional changes will ensure that file sync teardown and initialization are serialized through a shared promise and cannot run concurrently, eliminating the race condition mentioned in your review comment.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

* Add file uploading tracking to cloud tray menu
@patrickelectric patrickelectric merged commit 4ac42d3 into bluerobotics:master Dec 19, 2025
11 of 12 checks passed
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.

2 participants