feat: First-Time User Onboarding Flow, Modular Device Addition, and Personal Cohorts Integration (vertex app)#3572
Conversation
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
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.
There was a problem hiding this comment.
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 winScope assignment banners to the dialog while the modal is still open.
In guided mode this
ReusableDialogstays mounted, but both assignment banners are emitted withscoped: 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,ReusableDialoghas a built-inBannerSlot, and banners fired from within a modal should usescoped: truewhen 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 winKeep 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 previousparsedData/fileHeadersalive. If someone replaces a valid file with a bad one,Nextcan still operate on stale rows from the earlier upload. Reset the bulk-only state on every invalid upload path, but keepimportFlowas"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 winRemove the unused
useGroups()call from the cohort details page
src/vertex/app/(authenticated)/admin/cohorts/[id]/page.tsx:allGroupsis destructured (line 36) but never read, while keeping theuseGroupshook call (line 36) active. That hook uses auseQuerywithqueryKey: ["groups"]and callsgroupsApi.getGroupsApi(), so the page triggers an unnecessary groups request on every visit. Remove theuseGroupsimport 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 winUse claim permission for the CTA disable state.
Both actions are disabled off
DEVICE.UPDATE, but they are wired as claim flows and already declarepermission={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
📒 Files selected for processing (34)
src/vertex/app/(authenticated)/admin/cohorts/[id]/page.tsxsrc/vertex/app/(authenticated)/cohorts/page.tsxsrc/vertex/app/(authenticated)/devices/claim/page.tsxsrc/vertex/app/(authenticated)/devices/my-devices/page.tsxsrc/vertex/app/(authenticated)/devices/overview/page.tsxsrc/vertex/app/(authenticated)/home/page.tsxsrc/vertex/app/changelog.mdsrc/vertex/app/types/groups.tssrc/vertex/components/features/auth/cookie-info-banner.tsxsrc/vertex/components/features/claim/claim-device-modal.tsxsrc/vertex/components/features/claim/steps/MethodSelectStep.tsxsrc/vertex/components/features/claim/steps/QRScanStep.tsxsrc/vertex/components/features/claim/utils.tssrc/vertex/components/features/cohorts/cohort-organizations-card.tsxsrc/vertex/components/features/cohorts/cohorts-empty-state.tsxsrc/vertex/components/features/cohorts/create-cohort.tsxsrc/vertex/components/features/devices/deploy-device-component.tsxsrc/vertex/components/features/devices/device-assignment-modal.tsxsrc/vertex/components/features/devices/device-details-modal.tsxsrc/vertex/components/features/devices/import-device-modal.tsxsrc/vertex/components/features/devices/import-steps/types.tssrc/vertex/components/features/home/HomeEmptyState.tsxsrc/vertex/components/features/home/network-visibility-card.tsxsrc/vertex/components/layout/layout.tsxsrc/vertex/components/layout/secondary-sidebar.tsxsrc/vertex/context/page-title-context.tsxsrc/vertex/core/apis/organizations.tssrc/vertex/core/auth/authProvider.tsxsrc/vertex/core/hooks/useContextAwareRouting.tssrc/vertex/core/hooks/useGroups.tssrc/vertex/core/hooks/useRecentlyVisited.tssrc/vertex/core/routes.tssrc/vertex/core/urls.tsxsrc/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
| const onSubmit = async (data: DeviceUpdateFormData) => { | ||
| const isAirqoManufacturer = device?.network?.toLowerCase() === 'airqo'; | ||
| if (isAirqoManufacturer) { | ||
| await onSubmitLocal(data); | ||
| } else { | ||
| await onSubmitGlobal(data); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
sync global saves uses soft add api which means no thingspeak update, while local saves to thingspeak
There was a problem hiding this comment.
@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.
|
New azure vertex changes available for preview here |
1 similar comment
|
New azure vertex changes available for preview here |
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.
|
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.
|
New azure vertex changes available for preview here |
📝 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
/devices/claimstandalone page. All "Add Device" actions across the platform (My Devices, Org Overview, Assignment Modal, Deploy Wizard) now trigger the modularClaimDeviceModaldirectly in-place.🏷️ Type of Change
🧪 How Has This Been Tested?
🖼️ Screenshots (if applicable)
(Add screenshots of the new guided modal for adding devices, the onboarding checklist, and the updated organization picker here)
















✅ Checklist
CHANGELOG.mdfile with the relevant release entry.Summary by CodeRabbit
New Features
Improvements
Documentation