Skip to content

feat: First-Time User Onboarding Flow, Modular Device Addition, and Personal Cohorts Integration (vertex app)#3572

Merged
Baalmart merged 85 commits into
stagingfrom
personal-devices-claim
Jun 3, 2026
Merged

feat: First-Time User Onboarding Flow, Modular Device Addition, and Personal Cohorts Integration (vertex app)#3572
Baalmart merged 85 commits into
stagingfrom
personal-devices-claim

Conversation

@Codebmk

@Codebmk Codebmk commented Jun 3, 2026

Copy link
Copy Markdown
Member

📝 Description

This PR introduces comprehensive enhancements to how users add their AirQo devices, overall onboarding experiences, session management, and cohort management.

Motivation: The primary goal of this update is to significantly enhance the onboarding and daily experience for users. To support a smoother setup process, we refactored the monolithic "add device" and "import device" modals into intuitive, modular steps and completely deprecated the old standalone claim page. We've also added an interactive onboarding checklist, dedicated support for personal user cohorts, and quality-of-life improvements like a 6-hour idle timeout and a persistent cookie banner to reduce nagging friction.

🚀 Key Features & Changes

  • Modal-Based Device Claiming: Deleted the legacy /devices/claim standalone page. All "Add Device" actions across the platform (My Devices, Org Overview, Assignment Modal, Deploy Wizard) now trigger the modular ClaimDeviceModal directly in-place.
  • Guided Device Addition & Onboarding: Introduced guided/fast modes for adding AirQo devices and a device choice dialog. Refactored the monolithic modal into distinct step components to simplify the process.
  • Onboarding Checklist: Added an interactive onboarding checklist to the Home page that preserves completion status across sessions.
  • Session & UX Improvements:
    • Increased the idle session auto-logout timeout from 30 minutes to 6 hours to accommodate longer-running operations.
    • Whitelisted the cookie consent flag so the cookie banner preference survives logouts, preventing it from repeatedly nagging users on the login screen.
  • Read-Only Onboarding UI: Polished the interface with read-only views where appropriate to prevent accidental edits during the critical first-time setup (e.g., Network Visibility card now remains visible even for read-only users).
  • Cohort Improvements: Added dedicated APIs, hooks, and UI support for personal cohorts. Enabled importing capabilities directly for cohorts.
  • Bulk Import Enhancements: Refactored the bulk import flow into modular steps, fixed bugs, and enhanced the UI to display detailed import previews and success summaries.
  • UI Refinements: Replaced jarring skeleton loaders with a refined spinner for dynamic modal imports, updated environment-aware analytics links, sorted the organization picker alphabetically, added an Administrator pill tag to the topbar, and removed obsolete setup banners/buttons.

🏷️ Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactor (code restructuring without changing external behavior)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

🧪 How Has This Been Tested?

  • Unit/Integration tests added or updated
  • Manual end-to-end verification of the new first-time user onboarding workflow
  • Verified that all legacy "Claim Device" links correctly open the in-place modal
  • Verified that cohort imports and bulk device imports successfully parse and handle errors
  • Verified onboarding checklist state correctly persists across sessions
  • Verified cookie banner preference persists after user logout

🖼️ Screenshots (if applicable)

(Add screenshots of the new guided modal for adding devices, the onboarding checklist, and the updated organization picker here)
image
image
image
image
image
image
image
image
image
image
image
image
image
image
image
image

✅ Checklist

  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • My changes generate no new warnings or errors.
  • I have updated the CHANGELOG.md file with the relevant release entry.

Summary by CodeRabbit

  • New Features

    • In-place Claim modal with guided/fast flows (single, bulk, cohort import), bulk import wizard, onboarding checklist, cohort organizations card, and unassign confirmation dialog.
  • Improvements

    • Unified “Add AirQo Device” CTAs, home/dashboard refinements (visibility, progress, admin badge), cookie banner persistence, extended inactivity timeout, font/CSS enhancements and subtle UI polish.
  • Documentation

    • Changelog updated for Version 1.23.58.

Codebmk added 30 commits May 27, 2026 15:32
Introduce support for fetching a user's personal cohorts and wire it into the dashboard. Changes include:

- Add PersonalUserCohortsResponse type to app types.
- Add cohorts.getPersonalUserCohorts API method with validation and JWT header.
- Add usePersonalUserCohorts React Query hook (with query key, enabled flag, select and a 1-minute staleTime).
- Integrate the hook into DashboardStatsCards: obtain userId from session/store, fetch personal cohort IDs for personal scope, and update device count loading logic to depend on cohort loading.
- Expand the home page Accordion defaultValue to include 'cohorts' and tidy a minor parentheses in loading computation.

These changes ensure personal-scope device counts are correctly gated on the user's cohorts and expose the cohorts panel in the dashboard UI.
Refine personal cohort fetching and adjust device-count logic. usePersonalUserCohorts now awaits API results, verifies each cohort via verifyCohortIdApi, filters out cohorts named "airqo", and returns an array of valid cohort IDs. useDeviceCount now gives explicit cohortIds priority (personal scope), treats AirQo group only when no explicit cohorts were passed, normalizes queryKey (uses a 'personal' sentinel when applicable), and tightens the query enabled logic. Also removed an unused redux import and small comment tweaks in the dashboard component.
Always render the Device Visibility accordion on the home page (removed external-org conditional and 'cohorts' default selection). Enhance NetworkVisibilityCard to handle personal user scope: import and use usePersonalUserCohorts, useSession and redux user info, derive effective cohort IDs (personal vs group), update query enabled/loading logic, and invalidate personal cohorts cache after updates. Also adjust UI copy and visuals to reflect personal vs organization scope, and apply minor formatting/cleanup changes.
Handle personal user cohort scope and refine cohort-loading logic. This change adds useSession and useAppSelector to derive the user ID, introduces usePersonalUserCohorts, and resolves target cohort IDs from either personal or group APIs depending on userScope and org membership. It adjusts enabled flags for fetching, consolidates loading flags into isResolvingIds/tableLoading, and updates empty/error display logic. Also updates table title to show "My Cohorts" for personal scope and includes minor formatting/cleanup.
Split and refactor the Claim Device modal into a multi-step flow and move bulk/step logic into separate components. The main modal was rewritten to introduce explicit FlowStep types, shared Error/Loading UI, and better state handling (including useSession and useQueryClient). Cohort verification/import, single and bulk claim handlers, QR parsing, cache invalidation for personal/group contexts, and cohort assignment flows were implemented. Several step components and utilities were added (BulkInputStep, CohortImportStep, ConfirmationSteps, ManualInputStep, MethodSelectStep, QRScanStep, SuccessStep, utils.parseQRCode) and BulkClaimResults was relocated to steps/. Overall improves modularity, error handling, and cache invalidation after claims.
Introduce a new OnboardingChecklist component (src/vertex/components/features/home/onboarding-checklist.tsx) and wire it into the authenticated Home page. The checklist replaces the previous empty-state flow for workspaces with no devices and provides step-based guidance (add device, group devices, set visibility) with persisted frontend state keyed per org/personal user in localStorage.

Also added small helpers in the page for reading/saving checklist state, a local org-specific key, and an updater callback. Claim/import device modals and navigation to cohorts are integrated with the checklist actions; the checklist auto-dismisses when all steps complete. Misc: minor formatting refactors, improved dynamic import args, and small Accordion/JSX tidy-ups.
Introduce an 'Add a device' choice dialog in the welcome page so users can pick between claiming an AirQo device or importing a different manufacturer sensor. Add state and callbacks (isAddDeviceChoiceOpen, openAddDeviceChoice, openClaimModal, openImportModal) and a handleDeviceAdded routine that updates the onboarding checklist when a device is added. Wire ClaimDeviceModal and ImportDeviceModal to call onSuccess so the checklist gets updated on successful claim/import. Update ImportDeviceModal to accept an optional onSuccess prop and invoke it on all success/partial-success paths. Add onAddDevice prop to OnboardingChecklist and map the 'add-device' step to open the choice dialog. Minor code style adjustments for consistency.
Comment out claim/import device handlers and related UI in the onboarding checklist. The WelcomePage no longer passes onClaimDevice/onImportDevice to the OnboardingChecklist, and the contextual help block that offered "Claim an AirQo device" / "add one from a different manufacturer" has been removed (commented) from the component. This temporarily disables the claim/import flows while preserving the code for easy restoration. Files changed: src/vertex/app/(authenticated)/home/page.tsx, src/vertex/components/features/home/onboarding-checklist.tsx.
Remove the collapsible behavior and dismiss control from OnboardingChecklist. Deleted the isCollapsed localStorage state, the collapse toggle and dismiss button (and their icon imports: ChevronDown, ChevronUp, X). Steps are now always rendered while the progress bar remains visible, simplifying the component and removing persisted collapsed state.
Introduce ClaimFlowMode ('guided' | 'fast') and a mode prop on ClaimDeviceModal (defaults to 'fast'). Guided mode limits flow to single-device QR/manual claiming and disables cohort assignment/bulk paths; fast mode preserves bulk, cohort import and auto-assignment behavior. Major refactor and cleanup of the modal: reorganized sections (modes, types, state, handlers, effects, render), consolidated cache invalidation helpers, improved effect dependencies, added QR parsing/confirmation flow, tracked previousStep for correct back behavior, separated single vs bulk handlers (add/remove/update/import/clear), and simplified cohort verification/assignment logic with shorter success timeouts. MethodSelectStep now accepts mode and only shows allowed methods per mode. Removed unused imports (cohorts API and Device), adjusted form defaults and dialog config/buttons to match modes and steps, and consolidated UI conditionals to hide fast-mode-only UI in guided mode.
-  Change banner background color
- Update banner copy
- Improved contrast across all severities for banner component — bumped background saturation, stronger border colors, and darker text for light mode, richer tones for dark
Codebmk added 7 commits June 3, 2026 15:06
Load the ClaimDeviceModal dynamically and open it in-place instead of navigating to /devices/claim. Replaced router.push(...) calls with local isClaimModalOpen state and rendered the modal in: my-devices page, devices overview page, deploy-device-component, and device-assignment-modal. Removed /devices/claim from prefetch routes and from the page title & routing config. Also removed unused useRouter imports and a redundant context comparison in useContextAwareRouting. This switches the Claim flow to a client-only modal (ssr: false) for a smoother in-app UX.
Raise frontend inactivity auto-logout from 30 minutes to 6 hours in src/vertex/core/auth/authProvider.tsx to reduce login friction during long-running tasks; updated the debug log message accordingly. Update changelog (src/vertex/app/changelog.md) to add a 'Session Management' section describing the change and include authProvider in the modified files list.
Use a namespaced cookie flag and preserve it on logout so the cookie banner preference survives session clears. Renamed localStorage key in CookieInfoBanner to `vertex_cookies_accepted` and added it to the session manager whitelist in `clearSessionData`. Updated changelog to document the session/onboarding changes and the modified files list.
Introduce cohort-scoped groups API and hook, expand types, and refactor the CohortOrganizationsCard to fetch and present organizations itself. Added groupsApi.getGroupsByCohortApi and useGroupsByCohort hook, plus CohortGroupsResponse and new Group fields (cohorts, organization_slug). CohortOrganizationsCard now loads organizations, shows a compact list of up to 3 entries with copy-ID and unassign actions, provides a "View more" dialog with a searchable table, and refetches after unassign; loading state and toasts added. Removed prop-driven organizations/loading from the cohort page and updated changelog count/notes to reflect the refactor.
Add changelog entry describing device management enhancements (guided/fast add flow, smart save routing, improved validation). In device-details-modal.tsx remove unused RefreshCw import, add onSubmit that routes to onSubmitLocal or onSubmitGlobal based on device.network === 'airqo', and consolidate the separate "Sync Global" / "Save Local" buttons into a single "Save" button with unified loading/icon handling.

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/vertex/components/features/devices/import-device-modal.tsx (2)

404-419: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope assignment banners to the dialog while the modal is still open.

In guided mode this ReusableDialog stays mounted, but both assignment banners are emitted with scoped: false. That sends the result to the global container behind the active modal, so assignment failures are easy to miss. Use the dialog banner slot while the import modal is open, and only fall back to global after it closes. Based on learnings, ReusableDialog has a built-in BannerSlot, and banners fired from within a modal should use scoped: true when they need to render inside that modal context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/devices/import-device-modal.tsx` around lines
404 - 419, The banners emitted from executeAssignment use scoped: false, causing
them to render behind the modal; update the assignDevicesToCohort.mutate
callbacks in executeAssignment to use scoped: true while the import modal
(ReusableDialog with BannerSlot) is mounted and only fall back to scoped: false
after the dialog closes — specifically change the showBannerWithDelay and
showBanner calls in executeAssignment to pass scoped: true so they render in the
dialog's BannerSlot, and add fallback logic that detects modal closed (or a
dialog close handler) to re-emit or display a global banner (scoped: false) if
needed.

217-276: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep invalid/cleared uploads in the bulk flow and clear the old parsed state.

Right now a cleared file on Line 225 flips the wizard back to "single", and the parse-error paths keep the previous parsedData/fileHeaders alive. If someone replaces a valid file with a bad one, Next can still operate on stale rows from the earlier upload. Reset the bulk-only state on every invalid upload path, but keep importFlow as "bulk".

Suggested fix
+  const resetBulkUploadState = () => {
+    setParsedData([]);
+    setFileHeaders([]);
+    setFieldMapping({});
+    setTransformedPreview([]);
+  };
+
   const handleFileUpload = async (file: File | null) => {
     setBulkFile(file);
     setErrors({});
     if (!file) {
-      setParsedData([]);
-      setFileHeaders([]);
-      setFieldMapping({});
-      setTransformedPreview([]);
-      if (currentStep === 0) {
-        setImportFlow('single');
-      }
+      resetBulkUploadState();
       return;
     }
@@
         complete: (results) => {
           const headers = results.meta.fields || [];
           const rows = results.data as Record<string, string | number | undefined>[];
@@
           if (rows.length === 0) {
+            resetBulkUploadState();
             setFileHeaders(headers);
-            setParsedData([]);
-            setFieldMapping({});
             showBanner({ severity: 'error', message: "The uploaded CSV does not contain any devices.", scoped: true });
             return;
           }
@@
         },
         error: (err: { message: string }) => {
+          resetBulkUploadState();
           showBanner({ severity: 'error', message: `Failed to parse CSV: ${err.message}`, scoped: true });
         }
       });
@@
         const devices = Array.isArray(json) ? json : (json.devices || []);
         if (!Array.isArray(devices) || devices.length === 0) {
+          resetBulkUploadState();
           showBanner({ severity: 'error', message: 'JSON file must contain an array of devices', scoped: true });
           return;
         }
@@
-      } catch {
+      } catch {
+        resetBulkUploadState();
         showBanner({ severity: 'error', message: 'Invalid JSON file format', scoped: true });
       }
     } else {
+      resetBulkUploadState();
       showBanner({ severity: 'error', message: 'Unsupported file type. Please upload a CSV or JSON file.', scoped: true });
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/devices/import-device-modal.tsx` around lines
217 - 276, handleFileUpload currently flips the wizard back to "single" when the
file is cleared and leaves stale parsed state on parse/JSON/unsupported-file
errors; change it so that you do NOT call setImportFlow('single') when file is
null and instead always clear bulk-only state (call setParsedData([]),
setFileHeaders([]), setFieldMapping({}), setTransformedPreview([])) on any
invalid upload path (CSV parse error callback, CSV empty rows branch, JSON
invalid/empty devices branch, and unsupported file branch) while keeping
setImportFlow('bulk') (or leaving importFlow unchanged) so the UI stays in bulk
flow but without stale data; update handleFileUpload to reference these
functions (setParsedData, setFileHeaders, setFieldMapping,
setTransformedPreview, setImportFlow) and add the clearing calls in each error
path and the file===null branch.
src/vertex/app/(authenticated)/admin/cohorts/[id]/page.tsx (1)

18-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the unused useGroups() call from the cohort details page

src/vertex/app/(authenticated)/admin/cohorts/[id]/page.tsx: allGroups is destructured (line 36) but never read, while keeping the useGroups hook call (line 36) active. That hook uses a useQuery with queryKey: ["groups"] and calls groupsApi.getGroupsApi(), so the page triggers an unnecessary groups request on every visit. Remove the useGroups import and the unused hook call unless that fetch is still required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/app/`(authenticated)/admin/cohorts/[id]/page.tsx at line 18, The
page imports and calls the useGroups hook but never uses the returned allGroups
value; remove the unused import and hook call to stop the unnecessary query:
delete the import of useGroups and remove the destructuring/assignment that
references useGroups (the allGroups variable) in the component (look for
useGroups() and the allGroups destructure) unless you actually need group
data—if group data is required, instead use a conditional or pass-through prop
so the query is only invoked when needed.
src/vertex/app/(authenticated)/devices/overview/page.tsx (1)

23-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use claim permission for the CTA disable state.

Both actions are disabled off DEVICE.UPDATE, but they are wired as claim flows and already declare permission={PERMISSIONS.DEVICE.CLAIM}. That blocks users who can claim devices but cannot update them from reaching the new modal entry.

Also applies to: 37-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/app/`(authenticated)/devices/overview/page.tsx around lines 23 -
24, The CTA and action-disable logic currently uses canUpdateDevice (from
usePermission(PERMISSIONS.DEVICE.UPDATE)) but these controls start a claim flow
and declare permission={PERMISSIONS.DEVICE.CLAIM}; change the disable-state to
check the claim permission instead: add or use a canClaimDevice =
usePermission(PERMISSIONS.DEVICE.CLAIM) and replace usages of canUpdateDevice
for disabling the CTA and related actions (the same components that already pass
permission={PERMISSIONS.DEVICE.CLAIM}). Ensure any update-specific permission
checks remain only where true update operations occur (leave
usePermission(PERMISSIONS.DEVICE.UPDATE) for actual update flows).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vertex/app/`(authenticated)/devices/my-devices/page.tsx:
- Around line 107-110: Gate both Claim Device CTAs by checking the claim
permission before rendering or enabling the button: wrap the Button that calls
setIsClaimModalOpen(true) (and the other CTA at the 165-171 region) with a
permission check such as hasPermission(PERMISSIONS.DEVICE.CLAIM) or
conditionally render based on useAuthorization/usePermissions; ensure you import
PERMISSIONS and the appropriate hook and only open the claim modal when the
permission check passes (leave internal modal validation as-is).

In `@src/vertex/components/features/cohorts/cohort-organizations-card.tsx`:
- Around line 47-92: The action column currently uses an explicit any key
("actions" as any) which violates `@typescript-eslint/no-explicit-any`; replace
that key with a real keyof Group that is not used by the other columns (e.g.,
add a dedicated unused property name from the Group type such as "meta" or
"flags" if available) and set sortable: false on that column in the tableColumns
array (the column object around the render that returns the Switch); also remove
the unused index parameter from the displayOrganizations.map callback (change
displayOrganizations.map((org, index) => ...) to displayOrganizations.map((org)
=> ...)) to fix the lint warning about unused variables.

In `@src/vertex/components/features/devices/deploy-device-component.tsx`:
- Around line 596-598: The modal open handler handleClaimDevice currently only
sets setIsClaimModalOpen(true) so the device list never refreshes after an
in-place claim; update the claim flow so that when the claim modal closes
successfully it triggers a refresh of the deployable device list (e.g., call the
existing fetch/reload method used to populate the combo-box or emit a refresh
callback). Concretely: augment the child modal’s onClose/onSuccess handler to
call the local reload function (or a prop like
reloadDeployableDevices/fetchDevices) and wire that into the same places as
setIsClaimModalOpen (refer to handleClaimDevice, isClaimModalOpen, and the
modal’s onClose/onSuccess); apply the same change to the similar block around
lines ~852-855 so both flows re-fetch the device options after a successful
claim.

In `@src/vertex/components/features/devices/device-details-modal.tsx`:
- Around line 236-242: The branch logic in onSubmit is reversed: currently
isAirqo -> onSubmitLocal and non-AirQo -> onSubmitGlobal; swap them so AirQo
devices call onSubmitGlobal(data) and non-AirQo devices call
onSubmitLocal(data). Update the onSubmit function (and the isAirqo check based
on device?.network) to call onSubmitGlobal for AirQo and onSubmitLocal for
others.

In `@src/vertex/core/urls.tsx`:
- Around line 3-6: The current logic for isProduction uses NEXT_PUBLIC_ENV
directly and defaults to staging if the env var is missing; change it to treat a
missing/undefined NEXT_PUBLIC_ENV as production-safe by default. Update the code
around isProduction and DEFAULT_ANALYTICS_BASE_URL to derive env with a safe
fallback (e.g., const env = process.env.NEXT_PUBLIC_ENV ?? 'production') and set
isProduction = env.toLowerCase() === 'production' before selecting the URL, so
forgot-password/sign-up links point to production when the env var is absent.

---

Outside diff comments:
In `@src/vertex/app/`(authenticated)/admin/cohorts/[id]/page.tsx:
- Line 18: The page imports and calls the useGroups hook but never uses the
returned allGroups value; remove the unused import and hook call to stop the
unnecessary query: delete the import of useGroups and remove the
destructuring/assignment that references useGroups (the allGroups variable) in
the component (look for useGroups() and the allGroups destructure) unless you
actually need group data—if group data is required, instead use a conditional or
pass-through prop so the query is only invoked when needed.

In `@src/vertex/app/`(authenticated)/devices/overview/page.tsx:
- Around line 23-24: The CTA and action-disable logic currently uses
canUpdateDevice (from usePermission(PERMISSIONS.DEVICE.UPDATE)) but these
controls start a claim flow and declare permission={PERMISSIONS.DEVICE.CLAIM};
change the disable-state to check the claim permission instead: add or use a
canClaimDevice = usePermission(PERMISSIONS.DEVICE.CLAIM) and replace usages of
canUpdateDevice for disabling the CTA and related actions (the same components
that already pass permission={PERMISSIONS.DEVICE.CLAIM}). Ensure any
update-specific permission checks remain only where true update operations occur
(leave usePermission(PERMISSIONS.DEVICE.UPDATE) for actual update flows).

In `@src/vertex/components/features/devices/import-device-modal.tsx`:
- Around line 404-419: The banners emitted from executeAssignment use scoped:
false, causing them to render behind the modal; update the
assignDevicesToCohort.mutate callbacks in executeAssignment to use scoped: true
while the import modal (ReusableDialog with BannerSlot) is mounted and only fall
back to scoped: false after the dialog closes — specifically change the
showBannerWithDelay and showBanner calls in executeAssignment to pass scoped:
true so they render in the dialog's BannerSlot, and add fallback logic that
detects modal closed (or a dialog close handler) to re-emit or display a global
banner (scoped: false) if needed.
- Around line 217-276: handleFileUpload currently flips the wizard back to
"single" when the file is cleared and leaves stale parsed state on
parse/JSON/unsupported-file errors; change it so that you do NOT call
setImportFlow('single') when file is null and instead always clear bulk-only
state (call setParsedData([]), setFileHeaders([]), setFieldMapping({}),
setTransformedPreview([])) on any invalid upload path (CSV parse error callback,
CSV empty rows branch, JSON invalid/empty devices branch, and unsupported file
branch) while keeping setImportFlow('bulk') (or leaving importFlow unchanged) so
the UI stays in bulk flow but without stale data; update handleFileUpload to
reference these functions (setParsedData, setFileHeaders, setFieldMapping,
setTransformedPreview, setImportFlow) and add the clearing calls in each error
path and the file===null branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f5f1912-ac1b-4e2b-83e4-ddc5dd28e61e

📥 Commits

Reviewing files that changed from the base of the PR and between 9ea2963 and 6ffd763.

📒 Files selected for processing (34)
  • src/vertex/app/(authenticated)/admin/cohorts/[id]/page.tsx
  • src/vertex/app/(authenticated)/cohorts/page.tsx
  • src/vertex/app/(authenticated)/devices/claim/page.tsx
  • src/vertex/app/(authenticated)/devices/my-devices/page.tsx
  • src/vertex/app/(authenticated)/devices/overview/page.tsx
  • src/vertex/app/(authenticated)/home/page.tsx
  • src/vertex/app/changelog.md
  • src/vertex/app/types/groups.ts
  • src/vertex/components/features/auth/cookie-info-banner.tsx
  • src/vertex/components/features/claim/claim-device-modal.tsx
  • src/vertex/components/features/claim/steps/MethodSelectStep.tsx
  • src/vertex/components/features/claim/steps/QRScanStep.tsx
  • src/vertex/components/features/claim/utils.ts
  • src/vertex/components/features/cohorts/cohort-organizations-card.tsx
  • src/vertex/components/features/cohorts/cohorts-empty-state.tsx
  • src/vertex/components/features/cohorts/create-cohort.tsx
  • src/vertex/components/features/devices/deploy-device-component.tsx
  • src/vertex/components/features/devices/device-assignment-modal.tsx
  • src/vertex/components/features/devices/device-details-modal.tsx
  • src/vertex/components/features/devices/import-device-modal.tsx
  • src/vertex/components/features/devices/import-steps/types.ts
  • src/vertex/components/features/home/HomeEmptyState.tsx
  • src/vertex/components/features/home/network-visibility-card.tsx
  • src/vertex/components/layout/layout.tsx
  • src/vertex/components/layout/secondary-sidebar.tsx
  • src/vertex/context/page-title-context.tsx
  • src/vertex/core/apis/organizations.ts
  • src/vertex/core/auth/authProvider.tsx
  • src/vertex/core/hooks/useContextAwareRouting.ts
  • src/vertex/core/hooks/useGroups.ts
  • src/vertex/core/hooks/useRecentlyVisited.ts
  • src/vertex/core/routes.ts
  • src/vertex/core/urls.tsx
  • src/vertex/core/utils/sessionManager.ts
💤 Files with no reviewable changes (6)
  • src/vertex/context/page-title-context.tsx
  • src/vertex/core/hooks/useRecentlyVisited.ts
  • src/vertex/app/(authenticated)/devices/claim/page.tsx
  • src/vertex/core/routes.ts
  • src/vertex/core/hooks/useContextAwareRouting.ts
  • src/vertex/components/layout/layout.tsx
✅ Files skipped from review due to trivial changes (3)
  • src/vertex/components/features/cohorts/cohorts-empty-state.tsx
  • src/vertex/components/features/devices/import-steps/types.ts
  • src/vertex/components/features/home/HomeEmptyState.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/vertex/core/utils/sessionManager.ts
  • src/vertex/components/features/claim/steps/QRScanStep.tsx
  • src/vertex/app/(authenticated)/cohorts/page.tsx
  • src/vertex/components/features/claim/utils.ts
  • src/vertex/components/features/home/network-visibility-card.tsx
  • src/vertex/app/(authenticated)/home/page.tsx
  • src/vertex/components/features/claim/claim-device-modal.tsx

Comment thread src/vertex/app/(authenticated)/devices/my-devices/page.tsx Outdated
Comment thread src/vertex/components/features/cohorts/cohort-organizations-card.tsx Outdated
Comment thread src/vertex/components/features/devices/deploy-device-component.tsx
Comment on lines +236 to +242
const onSubmit = async (data: DeviceUpdateFormData) => {
const isAirqoManufacturer = device?.network?.toLowerCase() === 'airqo';
if (isAirqoManufacturer) {
await onSubmitLocal(data);
} else {
await onSubmitGlobal(data);
}

@coderabbitai coderabbitai Bot Jun 3, 2026

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 | 🟠 Major | ⚡ Quick win

The local/global save routing is flipped.

This now sends non-AirQo devices through useUpdateDeviceGlobal() and routes AirQo devices to the local update path. That is the opposite of the old “Sync Global” vs “Save Local” split, so imported external devices will hit the wrong endpoint while AirQo devices lose their global sync path. Swap the branches here.

Suggested fix
   const onSubmit = async (data: DeviceUpdateFormData) => {
     const isAirqoManufacturer = device?.network?.toLowerCase() === 'airqo';
     if (isAirqoManufacturer) {
-      await onSubmitLocal(data);
-    } else {
       await onSubmitGlobal(data);
+    } else {
+      await onSubmitLocal(data);
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSubmit = async (data: DeviceUpdateFormData) => {
const isAirqoManufacturer = device?.network?.toLowerCase() === 'airqo';
if (isAirqoManufacturer) {
await onSubmitLocal(data);
} else {
await onSubmitGlobal(data);
}
const onSubmit = async (data: DeviceUpdateFormData) => {
const isAirqoManufacturer = device?.network?.toLowerCase() === 'airqo';
if (isAirqoManufacturer) {
await onSubmitGlobal(data);
} else {
await onSubmitLocal(data);
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vertex/components/features/devices/device-details-modal.tsx` around lines
236 - 242, The branch logic in onSubmit is reversed: currently isAirqo ->
onSubmitLocal and non-AirQo -> onSubmitGlobal; swap them so AirQo devices call
onSubmitGlobal(data) and non-AirQo devices call onSubmitLocal(data). Update the
onSubmit function (and the isAirqo check based on device?.network) to call
onSubmitGlobal for AirQo and onSubmitLocal for others.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

sync global saves uses soft add api which means no thingspeak update, while local saves to thingspeak

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.

@Codebmk, that clears it up — the current routing is correct. AirQo devices go through onSubmitLocal because they need the ThingSpeak write, while non-AirQo/external devices go through onSubmitGlobal (soft add API, no ThingSpeak). My suggestion had the logic backwards. Apologies for the noise.


✏️ Learnings added
Learnt from: Codebmk
Repo: airqo-platform/AirQo-frontend PR: 3572
File: src/vertex/components/features/devices/device-details-modal.tsx:236-242
Timestamp: 2026-06-03T13:33:03.977Z
Learning: In `src/vertex/components/features/devices/device-details-modal.tsx`, the `onSubmit` routing between `onSubmitLocal` and `onSubmitGlobal` is intentionally: AirQo devices (`device.network === 'airqo'`) → `onSubmitLocal` (writes to ThingSpeak), non-AirQo/external devices → `onSubmitGlobal` (uses the soft-add API, no ThingSpeak update). Do not flag this routing as reversed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Codebmk
Repo: airqo-platform/AirQo-frontend PR: 3009
File: src/vertex/core/hooks/useDevices.ts:62-73
Timestamp: 2025-11-13T16:03:14.687Z
Learning: In src/vertex/core/hooks/useDevices.ts and similar hooks, when fetching devices for AirQo groups (isAirQoGroup is true), passing an empty string for the group parameter (group: "") is the correct behavior - the API does not need the group title in this case.

Learnt from: Codebmk
Repo: airqo-platform/AirQo-frontend PR: 3092
File: src/vertex/app/changelog.md:7-45
Timestamp: 2025-12-12T05:19:00.702Z
Learning: In the AirQo-frontend repository, the vertex project's changelog (src/vertex/app/changelog.md) uses paths relative to the src/vertex/ directory (e.g., "app/layout.tsx" instead of "src/vertex/app/layout.tsx"). When verifying changelog entries against PR changes, always check with the full repository path prefix (src/vertex/).

Learnt from: OchiengPaul442
Repo: airqo-platform/AirQo-frontend PR: 2923
File: src/platform/package.json:40-40
Timestamp: 2025-09-14T21:47:35.917Z
Learning: PR `#2923` is focused on platform directory changes only - jwt-decode v4 upgrade and auth refactoring. Netmanager directory files are out of scope for this PR.

Comment thread src/vertex/core/urls.tsx Outdated
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

New azure vertex changes available for preview here

1 similar comment
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

New azure vertex changes available for preview here

Codebmk added 2 commits June 3, 2026 17:21
Refine wording around adding/registering AirQo sensors to improve clarity and user encouragement. Updated the welcome card prompt to "Have an AirQo sensor? Easily add it to your workspace" and adjusted the checklist step to "Register your new AirQo sensor..." — text-only changes, no functional behavior modified.
Wrap the unassign Switch in a Tooltip (TooltipProvider/TooltipTrigger/TooltipContent) to show a "Remove cohort assignment" hint on hover; import the tooltip components. Rename table column prop from `title` to `label` and update labels: "Organization Name" → "Name", "Group ID" → "Org ID", and "Unassign" → "Action". Use `formatTitle` (imported) and uppercase rendering for organization titles. Functional behavior (checked state, disabled `canUnassign`, and unassign handler) is preserved.
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

New azure vertex changes available for preview here

Standardize wording for claiming devices to 'Add AirQo device' across the app. Update UI button labels and modal error messages, adjust DownloadHero step text, correct the RBAC doc to reference DEVICE.CLAIM, and update the permissionService description for DEVICE.CLAIM. Modified files: my-devices page, claim-device-modal, DownloadHero, RBAC-Feature-Access.md, and permissionService.
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

New azure vertex changes available for preview here

@Baalmart
Baalmart merged commit ce0f32d into staging Jun 3, 2026
28 checks passed
@Baalmart
Baalmart deleted the personal-devices-claim branch June 3, 2026 17:15
@Baalmart Baalmart mentioned this pull request Jun 3, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants