Skip to content

Enhance recent readings functionality and PR preview fix#3561

Merged
Baalmart merged 9 commits into
stagingfrom
fix-recents-reading
Jun 2, 2026
Merged

Enhance recent readings functionality and PR preview fix#3561
Baalmart merged 9 commits into
stagingfrom
fix-recents-reading

Conversation

@OchiengPaul442

@OchiengPaul442 OchiengPaul442 commented May 30, 2026

Copy link
Copy Markdown
Contributor

Status of maturity (all need to be checked before merging):

  • I've tested this locally
  • I consider this code done

Screenshots (optional)

Summary by CodeRabbit

  • New Features

    • Deployment-time health checks for auth endpoints.
    • Favorites-aware locations list and improved map-reading retry behavior.
    • Richer analytics site cards with extra location metadata and robust fallback fetching.
  • Bug Fixes

    • Normalized post-login redirect handling to avoid malformed callback URLs.
  • Chores

    • Simplified start/build/runtime behavior and safer deployment env injection/validation.
    • Improved token refresh handling to reduce auth interruptions.
  • Documentation

    • Added Production Mode setup and startup instructions.

- Implement fetching of recent readings in analytics service with batching
- Normalize recent readings data for site cards
- Update device service to handle cohort details fallback
- Refactor environment variable handling in deployment workflow
- Improve login page redirect logic
- Add production mode instructions to README
- Clean up Dockerfile and package.json for better clarity
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

This PR batches and normalizes analytics recent-readings, updates analytics hooks/utilities to prefer recent-readings with chart-data fallback, removes user_id from map readings and types, normalizes auth callback URLs, computes per-PR NEXTAUTH URLs in CI with a runtime providers health check, simplifies build/start and Docker runtime env, tweaks UI/layout components, tightens user-fetch gating, and implements JWT single-flight refresh.

Changes

Analytics Service and Hooks Refactoring

Layer / File(s) Summary
Analytics service: payload normalization & batching
src/platform/src/shared/types/api.ts, src/platform/src/shared/services/analyticsService.ts
Adds RecentReadingsPayload normalization and reworks AnalyticsService.getRecentReadings to trim/de-duplicate site_id, batch requests, fetch concurrently, normalize and merge measurements, and handle aborts.
Analytics utils and hooks: SiteData enrichment and site-card flow
src/platform/src/modules/analytics/utils/index.ts, src/platform/src/modules/analytics/hooks/index.ts
Introduces helper to derive display/search name; validates pollutant values; extends SiteData with search_name, country, city, region, and aqi_category; adds preference-site normalization and buildSiteCardsFromRecentReadings; useAnalyticsPreferences resolves and de-duplicates selected-site ids and useAnalyticsSiteCards prefers recent-readings with compatibility fallback to chart-data.

Map Readings, Locations, and Authentication Normalization

Layer / File(s) Summary
DeviceService and recent-readings signature
src/platform/src/shared/services/deviceService.ts
getCohort gains try/catch with an endpoint-unavailable fallback; getMapReadingsWithToken signature drops user_id and only uses cohort_id when building params.
useMapReadings: retry & remove user context
src/platform/src/modules/airqo-map/hooks/useMapReadings.ts
Removes useUser dependency, stops passing user?.id to the service, adds abort-vs-server error classification and an auto-retry timer/ref flow for eligible errors, and broadens isLoading while fetching with no readings.
LocationsList: favorites and coordinate normalization
src/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx
Adds canonical ID and coordinate parsers, resolves favorites from preferences, gates internal fetch/pagination when favorites active, normalizes LocationData, and computes usable coordinates on selection.
MapPage & Login pages
src/platform/src/modules/airqo-map/MapPage.tsx, src/platform/src/app/(dashboard)/.../login/page.tsx
MapPage removes useSitesByCountry and controls selectedCountry locally; login pages normalize res?.url via normalizeCallbackUrl before redirect.

CI: Azure preview NEXTAUTH URL and runtime health check

Layer / File(s) Summary
Azure preview workflow: NEXTAUTH_URL & health check
.github/workflows/deploy-frontend-azurepreview.yml
Derives branch-based app name and container-app DEFAULT_DOMAIN, filters auth-related build-time env keys and injects NEXTAUTH_URL/NEXTAUTH_URL_INTERNAL/AUTH_TRUST_HOST, constructs ENV_VARS array for deployment with fast-fail when secrets missing, updates container app env vars via az containerapp update --replace-env-vars, and performs a runtime GET /api/auth/providers health check with diagnostics on non-200 responses.

Build and Container Configuration

Layer / File(s) Summary
Build scripts, Dockerfile, layout, README
src/platform/package.json, src/platform/Dockerfile, src/platform/src/app/layout.tsx, src/platform/README.md
Simplifies start script to next start and removes standalone-related scripts; removes explicit ENV PORT/ENV HOSTNAME from final Dockerfile stage; replaces next/font Inter usage with a static font-sans antialiased body class; adds a “Production Mode” README section documenting npm run build / npm run start.

UI Layout and Components

Layer / File(s) Summary
MainLayout, Header, BottomNavigation
src/platform/src/shared/layouts/MainLayout.tsx, src/platform/src/shared/components/header/index.tsx, src/platform/src/shared/components/ui/bottom-navigation.tsx
Removes explicit bottom padding from <main>, makes header non-sticky (relative), and simplifies BottomNavigation to always render without scroll-driven visibility or AnimatePresence; container positioning changed from fixed bottom to relative.
Charts and Dashboard
src/platform/src/shared/components/charts/components/charts/DynamicChart.tsx, src/platform/src/modules/analytics/components/AnalyticsDashboard.tsx
DynamicChart computes numeric xAxisInterval from chartData.length and sets YAxis interval={0}; AnalyticsDashboard now sources country from siteData.country with fallback to siteData.location.

User Fetching and ApiClient

Layer / File(s) Summary
UserDataFetcher gating & normalization
src/platform/src/shared/providers/UserDataFetcher.tsx
Adds LoadingOverlay early return when persisted user is stale or fetched data missing; memoizes fetchedUser/fetchedGroups, conditions cohort hooks on availability, and refines when persisted user state is cleared/updated.
ApiClient: JWT decoding, single-flight refresh & 401 handling
src/platform/src/shared/services/apiClient.ts
Adds JWT expiry skew and base64url helpers to read exp, predicate to detect near-expiry tokens, reads token from request headers (fallback to NextAuth session), centralizes single in-flight refresh with queued requests via refreshAuthHeader(), dispatches auth:token-refreshed/auth:unauthorized events, refreshes near-expiry tokens in the request interceptor, and retries original requests after a successful refresh or rejects on failure.

Sequence Diagram(s)

sequenceDiagram
  participant Workflow as GitHub Workflow
  participant ContainerApp as Azure Container App
  participant BuildEnv as Build .env
  participant Deploy as Deployment
  participant HealthCheck as NextAuth Health Check

  Workflow->>ContainerApp: Fetch DEFAULT_DOMAIN for branch app
  Workflow->>BuildEnv: Filter auth keys and inject NEXTAUTH_URL/NEXTAUTH_URL_INTERNAL/AUTH_TRUST_HOST
  Workflow->>Deploy: Build ENV_VARS array with filtered secrets + computed auth URLs
  Workflow->>Deploy: Validate NEXTAUTH_SECRET/AUTH_SECRET present (fail fast)
  Deploy->>ContainerApp: az containerapp update --replace-env-vars "${ENV_VARS[@]}"
  ContainerApp-->>HealthCheck: container running
  HealthCheck->>ContainerApp: GET /api/auth/providers
  alt HTTP 200 OK
    HealthCheck-->>Workflow: Health check passed
  else Not 200
    HealthCheck->>Workflow: Print response body & recent container logs
    Workflow-->>Workflow: Exit with failure
  end
Loading

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Reviewers

  • Baalmart

"Batches hum like gears at night,
Auth URLs glow per‑PR light,
Charts tick true while headers rest,
Tokens queue and pass the test,
Favorites guide the map's swift flight."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title partially captures the changeset's scope. It mentions 'recent readings functionality' (accurate) and 'PR preview fix' (accurate for the Azure deployment workflow), but understates the breadth of changes including auth URL handling, font removal, container environment changes, and multiple component refactors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-recents-reading

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

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

❤️ Share

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

@OchiengPaul442 OchiengPaul442 changed the title Enhance recent readings functionality and improve README Enhance recent readings functionality and PR preview fix May 30, 2026

@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

🧹 Nitpick comments (1)
src/platform/Dockerfile (1)

46-46: ⚡ Quick win

Drop the “binds to localhost” critical concern: Next standalone defaults HOSTNAME to 0.0.0.0 when unset.

src/platform/Dockerfile runs CMD ["node", "server.js"] without ENV HOSTNAME, but Next.js standalone mode is documented to listen on 0.0.0.0 by default when HOSTNAME isn’t set—so -p 3000:3000 access shouldn’t be broken by this omission. Adding ENV HOSTNAME=0.0.0.0 would just be for consistency/explicitness.

🔧 Proposed fix
 USER nextjs
 
 EXPOSE 3000
 
+ENV HOSTNAME=0.0.0.0
 CMD ["node", "server.js"]
🤖 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/platform/Dockerfile` at line 46, The Dockerfile currently ends with CMD
["node", "server.js"] and omits an explicit HOSTNAME, so add an ENV declaration
to make the listening address explicit: insert an ENV HOSTNAME=0.0.0.0 before
the CMD to ensure Next.js standalone runs bound to 0.0.0.0 (reference the
Dockerfile entry with CMD ["node", "server.js"] and add ENV HOSTNAME=0.0.0.0
above it).
🤖 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 @.github/workflows/deploy-frontend-azurepreview.yml:
- Around line 354-355: The BRANCH/APP values are currently built inside shell
run: steps from attacker-controlled needs.branch-name.outputs.lowercase,
exposing secrets to shell interpretation; move construction out of the run shell
and into GitHub Actions expressions or step/job env so no shell evaluation
occurs (e.g., set env: BRANCH: ${{ needs.branch-name.outputs.lowercase }} and
APP: ${{ format('{0}-analytics-p-preview',
substring(needs.branch-name.outputs.lowercase, 0, 12)) }} or use APP: ${{
concat(substring(needs.branch-name.outputs.lowercase,0,12),
'-analytics-p-preview') }}), and apply the same change to the second run block
and any other preview jobs that use BRANCH/APP; ensure uses of NEXTAUTH_URL and
az containerapp --name reference the env variables (BRANCH/APP) rather than
constructing them inside run so untrusted characters aren’t interpreted by the
shell.

In `@src/platform/package.json`:
- Line 8: The package.json "start" script is wrong for Next.js when
next.config.mjs uses output: 'standalone'; update the start script in
src/platform/package.json so it runs the standalone server (node
.next/standalone/server.js) instead of "next start" to match next.config.mjs and
the README.md "Production Mode" instructions and keep compatibility with the
Dockerfile that runs server.js from .next/standalone.

In `@src/platform/src/modules/analytics/hooks/index.ts`:
- Around line 522-562: The catch block for analyticsService.getRecentReadings
currently unconditionally calls getChartData on any non-abort error; change this
so only a narrowly defined compatibility error triggers the fallback (e.g.,
check error.code or error.response?.status and allow fallback only for the
specific expected compatibility case), and for all other errors (401, 5xx,
ERR_NETWORK, etc.) rethrow the error so the query fails normally; keep the
existing signal.aborted check, and if you need a helper, add an
isCompatibilityFallbackError(error) used before calling
createLatestSiteCardDateRange/getChartData and buildSiteCardsFromChartPoints.
- Around line 484-490: selectedSitesKey currently only includes site._id and
getSiteDisplayName(site) so changes to site.city/region/country (used by
buildSiteCardLocation) won't invalidate caches; update the useMemo that defines
selectedSitesKey to also include the location fields (site.city, site.region,
site.country) for each site so the key reflects those metadata changes and React
Query/SWR will refetch; keep the same selectedSitesKey useMemo and join logic
but append the location values (or a normalized location string) for each site
when mapping selectedSites.

In `@src/platform/src/shared/services/analyticsService.ts`:
- Around line 113-165: Promise.allSettled is hiding aborts; update the
post-allSettled handling in the method that calls
this.serverClient.get/normalizeRecentReadingsResponse so that any rejected
result whose reason is an AbortError (or otherwise indicates signal.aborted)
immediately rethrows that abort reason instead of continuing to merge fulfilled
batches. Concretely: after Promise.allSettled(resolve) check responses for any
rejected entry where the reason is an abort (e.g., reason.name === 'AbortError'
or a helper isAbortError(reason)); if found, throw that reason; otherwise fall
back to the existing logic that aggregates successfulResponses and throws a
non-abort error when no successfulResponses exist.

---

Nitpick comments:
In `@src/platform/Dockerfile`:
- Line 46: The Dockerfile currently ends with CMD ["node", "server.js"] and
omits an explicit HOSTNAME, so add an ENV declaration to make the listening
address explicit: insert an ENV HOSTNAME=0.0.0.0 before the CMD to ensure
Next.js standalone runs bound to 0.0.0.0 (reference the Dockerfile entry with
CMD ["node", "server.js"] and add ENV HOSTNAME=0.0.0.0 above it).
🪄 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: b8d2bfaf-2ef6-4b46-8e16-4db88117fc58

📥 Commits

Reviewing files that changed from the base of the PR and between 600683c and a1ffcbd.

📒 Files selected for processing (13)
  • .github/workflows/deploy-frontend-azurepreview.yml
  • src/platform/Dockerfile
  • src/platform/README.md
  • src/platform/package.json
  • src/platform/src/app/(dashboard)/(individual)/user/(auth)/login/page.tsx
  • src/platform/src/app/(dashboard)/(organization)/org/[org_slug]/(auth)/login/page.tsx
  • src/platform/src/app/layout.tsx
  • src/platform/src/modules/airqo-map/hooks/useMapReadings.ts
  • src/platform/src/modules/analytics/hooks/index.ts
  • src/platform/src/modules/analytics/utils/index.ts
  • src/platform/src/shared/services/analyticsService.ts
  • src/platform/src/shared/services/deviceService.ts
  • src/platform/src/shared/types/api.ts
💤 Files with no reviewable changes (1)
  • src/platform/src/shared/types/api.ts

Comment thread .github/workflows/deploy-frontend-azurepreview.yml
Comment thread src/platform/package.json
"prebuild": "yarn clean:next",
"postbuild": "node -e \"const fs=require('node:fs'); const path=require('node:path'); const projectRoot=process.cwd(); const standaloneRoot=path.join(projectRoot,'.next','standalone'); const publicSource=path.join(projectRoot,'public'); const publicTarget=path.join(standaloneRoot,'public'); const staticSource=path.join(projectRoot,'.next','static'); const staticTarget=path.join(standaloneRoot,'.next','static'); const copyDirectory=(source,target)=>{ if(!fs.existsSync(source)) return false; fs.mkdirSync(path.dirname(target),{recursive:true}); fs.rmSync(target,{recursive:true,force:true}); fs.cpSync(source,target,{recursive:true}); return true; }; if(!fs.existsSync(standaloneRoot)){ throw new Error(`Standalone output not found at ${standaloneRoot}. Run 'yarn build' first.`); } copyDirectory(publicSource, publicTarget); copyDirectory(staticSource, staticTarget);\"",
"start": "node -e \"const { loadEnvConfig } = require('@next/env'); loadEnvConfig(process.cwd()); process.env.HOSTNAME='localhost'; require('./.next/standalone/server.js')\"",
"start": "next start",

@coderabbitai coderabbitai Bot May 30, 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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if next.config uses standalone output mode

rg -n "output.*standalone" src/platform/next.config.mjs src/platform/next.config.js

Repository: airqo-platform/AirQo-frontend

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Confirm package.json start script
fd -a "package.json" src/platform -g "package.json" 2>/dev/null || true
cat -n src/platform/package.json | sed -n '1,120p'

# 2) Confirm next.config.mjs output mode
cat -n src/platform/next.config.mjs | sed -n '1,120p'

# 3) Locate Dockerfile and inspect relevant parts (standalone/server.js)
fd -a "Dockerfile" . | head -n 20
dockerfile="$(fd -a "Dockerfile" . | head -n 1)"
echo "Using Dockerfile: $dockerfile"
cat -n "$dockerfile" | sed -n '1,220p'

Repository: airqo-platform/AirQo-frontend

Length of output: 7279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

dockerfile="src/platform/Dockerfile"
echo "Using Dockerfile: $dockerfile"
test -f "$dockerfile" && echo "found" || { echo "missing"; exit 1; }

cat -n "$dockerfile" | sed -n '1,220p'

# Also double-check what it copies from .next during the runner stage
rg -n "standalone|server\\.js|next start|CMD|COPY --from=builder /app/\\.next" -S "$dockerfile"

Repository: airqo-platform/AirQo-frontend

Length of output: 1828


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'CMD \[|npm start|next start' src/platform/Dockerfile src/platform/*.Dockerfile 2>/dev/null || true

Repository: airqo-platform/AirQo-frontend

Length of output: 126


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "npm run start|next start|server\.js" -S . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/.next/**'

Repository: airqo-platform/AirQo-frontend

Length of output: 646


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n src/platform/README.md | sed -n '1,120p'
cat -n src/platform/README.md | sed -n '120,240p' || true

# Also check if platform Dockerfile comments mention npm start
cat -n src/platform/Dockerfile | sed -n '1,140p'

Repository: airqo-platform/AirQo-frontend

Length of output: 3302


🌐 Web query:

Next.js output: 'standalone' can you run next startor must you runnode server.js``

💡 Result:

If your Next.js build uses output: "standalone", you should start the server with node.next/standalone/server.js (the minimal standalone server), not with next start. The Next.js docs for output: "standalone" explicitly state that a minimal server.js is output and can be used instead of next start, and they also provide the command node.next/standalone/server.js. [1] Next.js also has an explicit warning that “next start” does not work with “output: standalone” and instructs you to use “node.next/standalone/server.js” instead. [2] So: - Works: node.next/standalone/server.js [1] - Not recommended / not supported for standalone output: next start [2][1]

Citations:


Fix npm run start for Next.js output: 'standalone'

src/platform/next.config.mjs uses output: 'standalone', but src/platform/package.json sets "start": "next start". With standalone output, Next.js doesn’t support next start; it must be started with node .next/standalone/server.js. This mismatch also conflicts with src/platform/README.md “Production Mode” instructions.

Current
"start": "next start",

Update the start script to node .next/standalone/server.js (and keep the Dockerfile as-is, since it already runs node server.js from .next/standalone).

🤖 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/platform/package.json` at line 8, The package.json "start" script is
wrong for Next.js when next.config.mjs uses output: 'standalone'; update the
start script in src/platform/package.json so it runs the standalone server (node
.next/standalone/server.js) instead of "next start" to match next.config.mjs and
the README.md "Production Mode" instructions and keep compatibility with the
Dockerfile that runs server.js from .next/standalone.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Still valid according to the nextjs docs on this no change needed here.

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.

@OchiengPaul442, fair point — and worth noting that the production Dockerfile already handles this correctly by running node server.js directly from .next/standalone, so the package.json start script won't be invoked in containerized deployments anyway. If next start works for your local dev/testing workflow per the docs you're referencing, that's a reasonable call. Acknowledged.

🐇 ╮(╯_╰)╭

Comment thread src/platform/src/modules/analytics/hooks/index.ts
Comment thread src/platform/src/modules/analytics/hooks/index.ts
Comment thread src/platform/src/shared/services/analyticsService.ts

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

🧹 Nitpick comments (1)
src/platform/src/modules/analytics/utils/index.ts (1)

245-255: 💤 Low value

Field priority order differs from shared getSiteDisplayName.

This helper uses name before location_name, whereas the shared getSiteDisplayName utility (in siteUtils.ts) prioritizes location_name before name. If this is intentional for siteDetails from recent readings, consider adding a brief comment explaining the rationale. If not, aligning with the shared utility would ensure consistent display names across the codebase.

🤖 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/platform/src/modules/analytics/utils/index.ts` around lines 245 - 255,
The getRecentReadingSiteName function currently chooses siteDetails.name before
siteDetails.location_name which conflicts with the shared getSiteDisplayName
ordering; either reorder the priority in getRecentReadingSiteName to match
getSiteDisplayName (use location_name before name) or, if the different order is
intentional for RecentReading data, add a concise comment above
getRecentReadingSiteName explaining why its priority differs and referencing
getSiteDisplayName for clarity.
🤖 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/platform/src/shared/providers/UserDataFetcher.tsx`:
- Around line 171-177: The LoadingOverlay in UserDataFetcher.tsx currently uses
delayMs={0} causing immediate full-screen flicker; change it to a short debounce
(e.g., 150–300ms) or replace with a subtler inline spinner to avoid white flash
by the LoadingOverlay component, and confirm activeGroupMissingFromFreshGroups
logic still resolves when setGroups in userSlice.ts updates activeGroup (no
change needed there) so the overlay won’t deadlock—update the LoadingOverlay
usage (symbol: LoadingOverlay in UserDataFetcher.tsx) to use a non-zero delay or
subtler UI.

---

Nitpick comments:
In `@src/platform/src/modules/analytics/utils/index.ts`:
- Around line 245-255: The getRecentReadingSiteName function currently chooses
siteDetails.name before siteDetails.location_name which conflicts with the
shared getSiteDisplayName ordering; either reorder the priority in
getRecentReadingSiteName to match getSiteDisplayName (use location_name before
name) or, if the different order is intentional for RecentReading data, add a
concise comment above getRecentReadingSiteName explaining why its priority
differs and referencing getSiteDisplayName for clarity.
🪄 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: 5d34e7d3-a367-41f2-97e9-cbb4291d6d91

📥 Commits

Reviewing files that changed from the base of the PR and between a1ffcbd and 4cdb0e5.

📒 Files selected for processing (10)
  • src/platform/src/modules/analytics/components/AnalyticsDashboard.tsx
  • src/platform/src/modules/analytics/hooks/index.ts
  • src/platform/src/modules/analytics/utils/index.ts
  • src/platform/src/shared/components/charts/components/charts/DynamicChart.tsx
  • src/platform/src/shared/components/header/index.tsx
  • src/platform/src/shared/components/ui/bottom-navigation.tsx
  • src/platform/src/shared/layouts/MainLayout.tsx
  • src/platform/src/shared/providers/UserDataFetcher.tsx
  • src/platform/src/shared/services/analyticsService.ts
  • src/platform/src/shared/services/apiClient.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/platform/src/shared/services/analyticsService.ts

Comment on lines +171 to +177
if (
hasStalePersistedUser ||
(status === 'authenticated' && !!userId && isLoading && !data) ||
activeGroupMissingFromFreshGroups
) {
return <LoadingOverlay delayMs={0} />;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's external logic that handles stale/invalid activeGroup clearing
rg -n "activeGroup" --type=ts -g '!*.test.*' -g '!*.spec.*' -A3 -B3 | head -100

Repository: airqo-platform/AirQo-frontend

Length of output: 7212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect UserDataFetcher gating logic + surrounding logout logic
sed -n '130,220p' src/platform/src/shared/providers/UserDataFetcher.tsx | cat -n

# 2) Find where activeGroupMissingFromFreshGroups is defined/computed
rg -n "activeGroupMissingFromFreshGroups" src/platform/src/shared/providers/UserDataFetcher.tsx -n
rg -n "activeGroupMissingFromFreshGroups" src/platform/src -g'!*.test.*' -g'!*.spec.*'

# 3) Inspect userSlice reducers for any "sync activeGroup with groups" behavior
sed -n '140,320p' src/vertex/core/redux/slices/userSlice.ts | cat -n

# 4) Search for any action/reducer that clears activeGroup when group list changes
rg -n "setActiveGroup|userGroups|activeGroup" src/vertex/core/redux/slices/userSlice.ts -n

Repository: airqo-platform/AirQo-frontend

Length of output: 11454


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full block where activeGroupMissingFromFreshGroups is computed and used (plus nearby effects)
sed -n '1,220p' src/platform/src/shared/providers/UserDataFetcher.tsx | cat -n | sed -n '1,220p'

# Focused searches inside this file for any mismatch-handling
rg -n "activeGroupMissingFromFreshGroups" src/platform/src/shared/providers/UserDataFetcher.tsx
rg -n "setActiveGroup\\(|activeGroup" src/platform/src/shared/providers/UserDataFetcher.tsx
rg -n "\\blogout\\(" src/platform/src/shared/providers/UserDataFetcher.tsx

# Confirm whether logout reducer clears activeGroup in userSlice.ts
rg -n "logout\\(state" -n src/vertex/core/redux/slices/userSlice.ts
sed -n '1,140p' src/vertex/core/redux/slices/userSlice.ts | cat -n

Repository: airqo-platform/AirQo-frontend

Length of output: 12804


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate selectActiveGroup implementation
rg -n "selectActiveGroup" src/platform/src/shared/store -g'*.ts*'

# 2) Inspect shared userSlice reducers for setGroups/logout affecting activeGroup
rg -n "setGroups\\b|logout\\b|activeGroup" src/platform/src/shared/store/userSlice.ts -n

# 3) Print relevant sections from shared userSlice (setGroups + logout + any activeGroup sync)
sed -n '1,260p' src/platform/src/shared/store/userSlice.ts | cat -n

Repository: airqo-platform/AirQo-frontend

Length of output: 4795


Reduce LoadingOverlay flicker; mismatch won’t deadlock

  • delayMs={0} makes the overlay appear immediately; adjust to a small delay (e.g., 150–300ms) or use a subtler loading UI to avoid full-white flicker.
  • The “infinite loading” scenario is unlikely: when fresh groups arrive, setGroups in src/platform/src/shared/store/userSlice.ts preserves activeGroup only if it exists in the new list; otherwise it switches activeGroup to the AIRQO group (or first group) or sets it to null, which clears activeGroupMissingFromFreshGroups on the next render.
🤖 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/platform/src/shared/providers/UserDataFetcher.tsx` around lines 171 -
177, The LoadingOverlay in UserDataFetcher.tsx currently uses delayMs={0}
causing immediate full-screen flicker; change it to a short debounce (e.g.,
150–300ms) or replace with a subtler inline spinner to avoid white flash by the
LoadingOverlay component, and confirm activeGroupMissingFromFreshGroups logic
still resolves when setGroups in userSlice.ts updates activeGroup (no change
needed there) so the overlay won’t deadlock—update the LoadingOverlay usage
(symbol: LoadingOverlay in UserDataFetcher.tsx) to use a non-zero delay or
subtler UI.

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

🧹 Nitpick comments (3)
src/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx (2)

246-257: 💤 Low value

The shouldFetchSites conditional is dense; consider extracting for clarity.

This boolean expression chains multiple conditions with different semantic meanings (searching, country filtering, preference resolution). It works correctly, but readability would benefit from either inline comments or extracting named sub-conditions.

That said, the logic is sound and tested via the consuming code paths. Optional refinement if you revisit this area.

🤖 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/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx`
around lines 246 - 257, The complex boolean assigned to shouldFetchSites is hard
to read; refactor by extracting meaningful named sub-conditions (e.g.,
isFilteringByCountry, needsFavoriteResolution, hasNoFavorites, isActiveSearch)
and then compose shouldFetchSites from those names so the logic is
self-documenting; update the call-site to use the new shouldFetchSites and keep
the same behavior for useSitesByCountry (referencing shouldFetchSites,
selectedCountry, isSearching, shouldResolveInitialFavorites,
isWaitingForFavoriteDecision, hasPreferenceContext, favoriteLocations.length).

80-90: 💤 Low value

Null-island filtering treats (0, 0) as invalid coordinates.

The function returns null when both latitude and longitude are exactly 0. While (0, 0) is a valid geographic coordinate (Gulf of Guinea), it's an extremely unlikely location for AirQo monitoring sites. This appears to be intentional null-island/default-value filtering.

If this is indeed the intent, a brief comment would clarify for future readers:

// Reject (0, 0) as a likely default/unset value rather than a real location
if (latitude === 0 && longitude === 0) {
  return null;
}

No action required if the behavior is intentional—just a clarity suggestion.

🤖 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/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx`
around lines 80 - 90, The getUsableCoordinates function currently treats
latitude===0 && longitude===0 as invalid and returns null; if this is
intentional as a "null‑island" / default-unset filter, add a brief clarifying
comment directly above that conditional in getUsableCoordinates explaining
"Reject (0,0) as a likely default/unset value rather than a real location
(null‑island)". Leave the logic unchanged but document the intent so future
readers understand why (0,0) is excluded.
src/platform/src/modules/airqo-map/hooks/useMapReadings.ts (1)

48-55: ⚡ Quick win

Auto-retry policy conflicts with coding guidelines.

The shouldAutoRetryMapReadings function triggers retries for both abort-like errors and 5xx responses. The coding guidelines explicitly state to "Avoid retrying 5xx, ERR_NETWORK, or aborted requests to prevent retry storms."

While the implementation sensibly limits retries to one per cohort key (via autoRetriedKeyRef), this still creates an implicit retry for classes of errors the guidelines recommend against. Consider either:

  1. Removing the auto-retry for these error classes entirely, or
  2. Documenting why a single bounded retry is acceptable here (e.g., transient network hiccups on initial load)

If option 2, a brief comment explaining the deviation would help future maintainers.

🤖 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/platform/src/modules/airqo-map/hooks/useMapReadings.ts` around lines 48 -
55, The auto-retry logic in shouldAutoRetryMapReadings currently retries for
abort-like errors and 5xx status codes which conflicts with the coding guideline
to avoid retrying aborted, network, or 5xx responses; update the
shouldAutoRetryMapReadings function so it does not trigger retries for these
classes of errors (e.g., always return false for isAbortLikeError(error) and for
status codes >=500 && <600), or if you intentionally permit a single bounded
retry, add a concise comment above shouldAutoRetryMapReadings explaining the
exception and why one retry is acceptable (mentioning the cohort-key
single-retry bound via autoRetriedKeyRef and the transient-initial-load
rationale) so future maintainers understand the deviation.
🤖 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/platform/src/modules/airqo-map/hooks/useMapReadings.ts`:
- Around line 140-145: The return currently exposes error?.message which can
surface abort/cancellation messages to users; update useMapReadings so the
returned error is null for cancellation errors by filtering them before exposing
to UI—add a small predicate (e.g., isCancellationError) that checks common
signals (Axios cancel/ isCancel, error.name === 'CanceledError' or 'AbortError',
or error.message === 'canceled') and use it when constructing the returned error
field (keep readings, isLoading, and refetch logic unchanged).

---

Nitpick comments:
In `@src/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx`:
- Around line 246-257: The complex boolean assigned to shouldFetchSites is hard
to read; refactor by extracting meaningful named sub-conditions (e.g.,
isFilteringByCountry, needsFavoriteResolution, hasNoFavorites, isActiveSearch)
and then compose shouldFetchSites from those names so the logic is
self-documenting; update the call-site to use the new shouldFetchSites and keep
the same behavior for useSitesByCountry (referencing shouldFetchSites,
selectedCountry, isSearching, shouldResolveInitialFavorites,
isWaitingForFavoriteDecision, hasPreferenceContext, favoriteLocations.length).
- Around line 80-90: The getUsableCoordinates function currently treats
latitude===0 && longitude===0 as invalid and returns null; if this is
intentional as a "null‑island" / default-unset filter, add a brief clarifying
comment directly above that conditional in getUsableCoordinates explaining
"Reject (0,0) as a likely default/unset value rather than a real location
(null‑island)". Leave the logic unchanged but document the intent so future
readers understand why (0,0) is excluded.

In `@src/platform/src/modules/airqo-map/hooks/useMapReadings.ts`:
- Around line 48-55: The auto-retry logic in shouldAutoRetryMapReadings
currently retries for abort-like errors and 5xx status codes which conflicts
with the coding guideline to avoid retrying aborted, network, or 5xx responses;
update the shouldAutoRetryMapReadings function so it does not trigger retries
for these classes of errors (e.g., always return false for
isAbortLikeError(error) and for status codes >=500 && <600), or if you
intentionally permit a single bounded retry, add a concise comment above
shouldAutoRetryMapReadings explaining the exception and why one retry is
acceptable (mentioning the cohort-key single-retry bound via autoRetriedKeyRef
and the transient-initial-load rationale) so future maintainers understand the
deviation.
🪄 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: 652559ca-2e3d-4a80-af66-a249d0469369

📥 Commits

Reviewing files that changed from the base of the PR and between 4cdb0e5 and edef796.

📒 Files selected for processing (3)
  • src/platform/src/modules/airqo-map/MapPage.tsx
  • src/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx
  • src/platform/src/modules/airqo-map/hooks/useMapReadings.ts

Comment on lines 140 to 145
return {
readings,
isLoading,
isLoading: isLoading || (readings.length === 0 && isFetching),
error: error?.message ?? null,
refetch,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Abort/cancellation errors may still surface to users.

Line 143 returns error?.message ?? null, which means if a request is cancelled (e.g., due to component unmount or cohort change), the abort error message ("canceled") could be displayed to users. The coding guidelines specify: "Ensure cancellation errors are not surfaced to users as real failures."

Consider filtering abort-like errors before exposing them:

Proposed fix
  return {
    readings,
    isLoading: isLoading || (readings.length === 0 && isFetching),
-   error: error?.message ?? null,
+   error: error && !isAbortLikeError(error) ? error.message : null,
    refetch,
  };
🤖 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/platform/src/modules/airqo-map/hooks/useMapReadings.ts` around lines 140
- 145, The return currently exposes error?.message which can surface
abort/cancellation messages to users; update useMapReadings so the returned
error is null for cancellation errors by filtering them before exposing to
UI—add a small predicate (e.g., isCancellationError) that checks common signals
(Axios cancel/ isCancel, error.name === 'CanceledError' or 'AbortError', or
error.message === 'canceled') and use it when constructing the returned error
field (keep readings, isLoading, and refetch logic unchanged).

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

New azure analytics_platform changes available for preview here

Copilot AI 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.

Pull request overview

This PR updates the analytics/map “recent readings” and favorites flows, improves token refresh behavior, and adjusts layout/navigation behavior to better support PR preview deployments.

Changes:

  • Improve analytics site cards by fetching recent readings (with batching + fallback to chart-data) and enriching site-card location metadata.
  • Harden auth/session handling: proactive JWT refresh in the API client and safer callback URL normalization after login.
  • PR preview/deployment updates for analytics platform (dynamic NEXTAUTH_URL injection + health check), plus map and layout refinements (favorites-aware locations list, map readings retry).

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/platform/src/shared/types/api.ts Removes user_id from recent readings request type.
src/platform/src/shared/services/deviceService.ts Adds cohort-details fallback behavior; removes user_id from map readings token call.
src/platform/src/shared/services/apiClient.ts Adds JWT expiry detection + proactive refresh and consolidates refresh logic.
src/platform/src/shared/services/analyticsService.ts Batches recent-readings requests and normalizes varied payload shapes.
src/platform/src/shared/providers/UserDataFetcher.tsx Adds guarded loading overlay to prevent stale user/group state flashes.
src/platform/src/shared/layouts/MainLayout.tsx Adjusts main scroll/padding to work with non-fixed bottom nav.
src/platform/src/shared/components/ui/bottom-navigation.tsx Simplifies bottom nav (no scroll-hide), changes positioning.
src/platform/src/shared/components/header/index.tsx Updates header positioning to match new layout scroll model.
src/platform/src/shared/components/charts/components/charts/DynamicChart.tsx Tweaks axis tick interval behavior for readability.
src/platform/src/modules/analytics/utils/index.ts Enriches recent-reading normalization (names, location fields, no-value handling).
src/platform/src/modules/analytics/hooks/index.ts Uses recent readings for site cards with compatibility fallback; normalizes preference sites.
src/platform/src/modules/analytics/components/AnalyticsDashboard.tsx Uses explicit country when present for “More insights” selection.
src/platform/src/modules/airqo-map/MapPage.tsx Removes country hook linkage; keeps map filtering driven by state.
src/platform/src/modules/airqo-map/hooks/useMapReadings.ts Adds one-shot auto-retry for transient failures + improved loading signaling.
src/platform/src/modules/airqo-map/components/sidebar/LocationsList.tsx Adds favorites-aware list + improved skeleton/loading logic and coordinate handling.
src/platform/src/app/layout.tsx Removes next/font Inter usage; uses font-sans.
src/platform/src/app/(dashboard)/(organization)/org/[org_slug]/(auth)/login/page.tsx Normalizes callback URL before redirect.
src/platform/src/app/(dashboard)/(individual)/user/(auth)/login/page.tsx Normalizes callback URL before redirect.
src/platform/README.md Adds production build/start instructions.
src/platform/package.json Simplifies start script to next start.
src/platform/Dockerfile Removes explicit PORT/HOSTNAME env lines (keeps standalone server start).
.github/workflows/deploy-frontend-azurepreview.yml Injects PR-preview auth env vars, validates secrets, adds health check, and refines env-var handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +73 to +82
normalizedSitesById.set(siteId, {
search_name:
selectedSite.search_name ||
selectedSite.formatted_name ||
selectedSite.generated_name ||
selectedSite.name ||
siteId,
...selectedSite,
_id: siteId,
});
Comment on lines 328 to 335
@@ -109,14 +333,17 @@ export const LocationsList: React.FC<LocationsListProps> = ({
name: location.title,
});
} else {
Comment on lines +375 to +377
BRANCH="${{ needs.branch-name.outputs.lowercase }}"
APP="${BRANCH:0:12}-analytics-p-preview"

@Baalmart
Baalmart merged commit aa88980 into staging Jun 2, 2026
27 of 28 checks passed
@Baalmart
Baalmart deleted the fix-recents-reading branch June 2, 2026 09:57
@Baalmart Baalmart mentioned this pull request Jun 2, 2026
5 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Jul 19, 2026
3 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.

3 participants