Skip to content

Add Clean Air Forum selfie wall display and harden submission API#3756

Merged
Baalmart merged 23 commits into
stagingfrom
feature/clean-air-forum-selfie-filter-website
Jul 9, 2026
Merged

Add Clean Air Forum selfie wall display and harden submission API#3756
Baalmart merged 23 commits into
stagingfrom
feature/clean-air-forum-selfie-filter-website

Conversation

@2phonebabykeem

@2phonebabykeem 2phonebabykeem commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Can access via the Link on the footer (Faces of clean air)
image

image

Summary by CodeRabbit

  • New Features

    • Added a new “Faces of Clean Air” page with a selfie carousel (auto-advance), responsive mobile/desktop pagination, and swipe/keyboard navigation.
    • Added “Faces of Clean Air” to the site footer About links.
    • Introduced motion-based animations across multiple pages for smoother browsing.
  • Bug Fixes

    • Improved CI pull-request failure comments with more accurate failed-step details and direct log links.
  • Documentation

    • Added follow-up documentation for the Clean Air Forum “selfies” feature and refreshed website developer workflow documentation.

2phonebabykeem and others added 3 commits July 7, 2026 13:25
Backs the mobile app's new selfie filter feature with a `/selfies` wall page
for the conference venue display (auto-polling grid, double-tap + staff PIN
to moderate) and a temporary in-memory submissions API, so the feature is
demoable end-to-end before the real AirQo backend has equivalent endpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wall page used generic black/white styling with no AirQo branding, all
active submissions in one long scrolling grid, and a POST endpoint that
accepted any imageUrl with no auth — all flagged by the user and CodeRabbit
as needing fixes before this goes live at the event.

- Wall page now uses the site's real logo and brand blue (#145DFF) instead
  of generic dark-UI colors, and shows 8 selfies at a time in an
  auto-advancing slideshow (framer-motion crossfade + page dots) rather
  than everything at once
- Long-press added alongside double-tap for moderation, since a wall
  display has no cursor to hover/right-click with
- POST now requires a shared secret header from the mobile app
  (x-clean-air-forum-secret) and validates imageUrl actually points at the
  Cloudinary folder the app uploads to, closing the "anyone who finds this
  endpoint can push arbitrary images onto the public wall" gap
- Both the submission secret and moderation PIN checks now fail closed
  (with a logged warning) if unset in production, instead of silently
  leaving the endpoint open — they still skip the check in dev/preview
- Added a best-effort per-IP rate limit (5/minute) on submissions to blunt
  spam before staff can react

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents what this feature still needs from other teams before it's
production-grade: real backend endpoints to replace the temporary
in-memory store, Cloudinary upload-preset hardening, the new env
vars/secrets that need wiring into the deploy workflows (left as docs
rather than editing the workflow YAML directly), and a note to confirm
the hardcoded event dates before launch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds the Faces of Clean Air carousel feature and its wiring/docs, rewrites the website CI failure-comment script to use job/step data, and updates several site pages with motion, copy, and presentation changes.

Changes

Faces of Clean Air Feature

Layer / File(s) Summary
Config, polling, and selfies data
src/website/src/config/cleanAirForumConfig.ts, src/website/src/hooks/usePollingWithVisibility.ts, src/website/src/services/api/api-routes.ts, src/website/src/services/external/faces-of-clean-air.service.ts, src/website/src/services/external/index.ts
Adds the Clean Air Forum event and polling intervals, a visibility-aware polling hook, the selfies API route, the submission type, and the selfies service export.
Carousel page implementation
src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx
Builds the client carousel page with animated background, submission cards, empty/error states, pagination, auto-advance, keyboard handling, and swipe gestures.
Route and site wiring
src/website/src/app/(programs)/faces-of-clean-air/page.tsx, src/website/src/components/layout/Footer.tsx, src/website/src/components/FloatingMiniBillboardWrapper.tsx, docs/clean-air-forum-followups.md, src/website/AGENTS.md
Adds the route module, footer link, billboard suppression, and route-follow-up documentation updates.
Estimated code review effort: 4 (Complex) ~60 minutes

CI Failure Comment Workflow

Layer / File(s) Summary
Failure comment script rework
.github/workflows/website-ci.yml
Replaces workflow-run step inference with API-based failed-step detection, adds fix guidance and log links, and null-checks comment bodies before matching.
Estimated code review effort: 3 (Moderate) ~20 minutes

Site Motion and Copy Polish

Layer / File(s) Summary
Page motion updates
src/website/src/features/blogs/BlogsPage.tsx, src/website/src/features/packages/PackagesPage.tsx, src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx
Adds motion-based reveal animations to Blogs, Packages, and AirQo DevCon pages, and updates the DevCon applications state and CTA treatment.
Copy and UI adjustments
src/website/src/components/dialogs/EngagementDialog.tsx, src/website/src/features/explore-data/ExplorePage.tsx, src/website/src/features/home/HomePage.tsx, src/website/src/features/products/AnalyticsPage.tsx, src/website/src/components/FloatingMiniBillboard.tsx, src/website/src/components/FloatingMiniBillboardWrapper.tsx, src/website/src/lib/utils/airQuality.ts, src/website/src/app/layout.tsx, src/website/README.md
Updates wording in engagement, explore, home, and analytics pages, adjusts floating billboard styling and route behavior, changes the air-quality no-value color, updates the layout referrer policy, and refreshes the README badges.
Estimated code review effort: 3 (Moderate) ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FacesOfCleanAirPage
  participant usePollingWithVisibility
  participant facesOfCleanAirService
  participant API as GET /users/selfies
  FacesOfCleanAirPage->>usePollingWithVisibility: register fetchSubmissions()
  usePollingWithVisibility->>FacesOfCleanAirPage: initial callback + refresh on visibility
  FacesOfCleanAirPage->>facesOfCleanAirService: getSubmissions(eventId)
  facesOfCleanAirService->>API: GET eventId
  API-->>facesOfCleanAirService: selfies data
  facesOfCleanAirService-->>FacesOfCleanAirPage: CleanAirSubmission[]
Loading

Suggested labels: documentation

Suggested reviewers: Baalmart, Codebmk

Poem

A carousel wakes in gentle light,
With polling tides that blink just right.
The CI note learns how to speak,
And pages sway with motion sleek.
Clean words, clean cards, a calmer scene —
Soft feet on AirQo’s frontend green.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: the new Clean Air Forum selfie wall and submission API hardening.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/clean-air-forum-selfie-filter-website

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.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

Edition and date range confirmed correct with the events team, and
updated the PR reference now that #3753 is closed in favor of #3755
(mobile) and #3756 (website).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

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

Caution

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

⚠️ Outside diff range comments (1)
src/website/src/config/cleanAirForumConfig.ts (1)

1-24: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Wrong directory name breaks the build — config/ vs configs/.

This file lives at src/website/src/config/cleanAirForumConfig.ts, but every consumer imports it as @/configs/cleanAirForumConfig (plural), matching the @/configs/* alias from the coding guidelines. The CI logs confirm this exact mismatch: tsc fails with TS2307: Cannot find module '@/configs/cleanAirForumConfig' and the webpack build fails with Module not found: Can't resolve '@/configs/cleanAirForumConfig'. This is the root cause blocking the entire pipeline (affects route.ts and SelfiesWallPage.tsx downstream, which import the same path).

As per coding guidelines, src/website/src/**/*.{ts,tsx,js,jsx} should "Use path aliases configured in tsconfig.json (@/, ... @/configs/, ...) instead of relative paths" — the alias is plural, so the file must live under configs/.

🛠️ Proposed fix
mkdir -p src/website/src/configs
git mv src/website/src/config/cleanAirForumConfig.ts src/website/src/configs/cleanAirForumConfig.ts
🤖 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/website/src/config/cleanAirForumConfig.ts` around lines 1 - 24, The Clean
Air Forum config file is in the wrong folder for the alias that consumers use,
so imports from `@/configs/cleanAirForumConfig` fail to resolve. Move
`cleanAirForumConfig` from `config/` to `configs/` (keeping the same exported
constants like `CLEAN_AIR_FORUM_CURRENT_EVENT_ID`), and make sure all imports in
callers such as `route.ts` and `SelfiesWallPage.tsx` continue to use the
`@/configs/*` path.

Sources: Coding guidelines, Pipeline failures

🧹 Nitpick comments (3)
docs/clean-air-forum-followups.md (1)

33-35: 📐 Maintainability & Code Quality | 🔵 Trivial

Keep the website on the Next.js proxy path.

Calling the real API directly from SelfiesWallPage.tsx would reintroduce backend URL coupling on the web side. Keep the proxy route and have it forward to the backend instead of deleting it outright. As per coding guidelines, src/website/**/*.{ts,tsx,js,jsx} client-side calls should go through the Next.js proxy at /api/v2 and avoid hard-coded backend URLs.

🤖 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 `@docs/clean-air-forum-followups.md` around lines 33 - 35, Keep the website on
the Next.js proxy path: do not delete the Next.js routes or switch
`SelfiesWallPage.tsx` to call the backend directly. Update the proxy handlers so
the web app continues to forward requests through `/api/v2` to the real API,
preserving the Next.js proxy layer and avoiding hard-coded backend URLs in
`src/website/**/*.{ts,tsx,js,jsx}`.

Source: Coding guidelines

src/website/src/app/api/clean-air-forum/selfies/route.ts (1)

57-63: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unbounded growth of submissionTimestampsByIp.

Entries are added per distinct IP but never removed once their recent-timestamp array empties out implicitly (it never actually empties — recent.push(now) guarantees at least one entry). Over a multi-day conference with many distinct visitor IPs, this Map grows indefinitely for the life of the process.

♻️ Proposed cleanup
 function isRateLimited(ip: string): boolean {
   const now = Date.now();
   const recent = (submissionTimestampsByIp.get(ip) ?? []).filter(
     (timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS,
   );
   recent.push(now);
-  submissionTimestampsByIp.set(ip, recent);
+  if (recent.length === 0) {
+    submissionTimestampsByIp.delete(ip);
+  } else {
+    submissionTimestampsByIp.set(ip, recent);
+  }
   return recent.length > RATE_LIMIT_MAX_SUBMISSIONS;
 }

Note: since recent always has at least the pushed now, a periodic full sweep of stale keys (e.g. on each call, occasionally) would be more effective than per-call pruning alone.

Also applies to: 71-79

🤖 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/website/src/app/api/clean-air-forum/selfies/route.ts` around lines 57 -
63, The rate-limit cache in selflies/route.ts grows without bound because
submissionTimestampsByIp only accumulates keys and never evicts stale IPs.
Update the logic around globalForRateLimit, submissionTimestampsByIp, and the
request-handling path that uses recent.push(now) to periodically sweep and
delete IP entries whose timestamps are all older than the window, or otherwise
cap/expire the Map so inactive visitors are removed over time.
src/website/src/services/cleanAirForumAuth.ts (1)

21-31: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use a constant-time comparison for shared secrets.

providedSecret === configuredSecret is a standard string comparison, which leaks timing information proportional to matching prefix length. Since this guards both the submission secret and the wall moderation PIN, consider crypto.timingSafeEqual (with a length check first, since it throws on mismatched buffer lengths).

🔒 Proposed fix
+import { timingSafeEqual } from 'crypto';
+
+function safeCompare(a: string, b: string): boolean {
+  const bufA = Buffer.from(a);
+  const bufB = Buffer.from(b);
+  return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
+}
+
 export function checkSharedSecret({
   ...
-  return providedSecret === configuredSecret;
+  return (
+    typeof providedSecret === 'string' &&
+    safeCompare(providedSecret, configuredSecret)
+  );
 }
🤖 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/website/src/services/cleanAirForumAuth.ts` around lines 21 - 31, The
shared-secret check in cleanAirForumAuth currently uses a normal string equality
comparison, which should be replaced with a constant-time comparison. Update the
secret validation logic to compare providedSecret and configuredSecret with a
timing-safe method, after first verifying both values have the same length so
the comparison does not throw. Keep the existing behavior for the isProduction
and missing-secret branches unchanged.
🤖 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/website/src/app/api/clean-air-forum/selfies/`[id]/route.ts:
- Around line 18-36: The PATCH handler in the selfies route performs a
shared-secret PIN check without any throttling, making repeated guesses
brute-forceable. Add the same per-IP rate limiting pattern used in the sibling
POST route before or around the checkSharedSecret call in PATCH, using the
existing route handler and params flow, so repeated unauthorized attempts are
limited before returning Incorrect PIN.

In `@src/website/src/app/api/clean-air-forum/selfies/route.ts`:
- Around line 65-69: The getClientIp helper is trusting the first
x-forwarded-for entry, which can be client-controlled and bypass rate limiting.
Update getClientIp in the selfie route to use the ingress-added IP from the
forwarded chain instead of split(',')[0], and keep the x-real-ip fallback only
when the trusted proxy IP is unavailable. Make the change in the getClientIp
function so the 5/min limit keys off the server-added address rather than a
spoofable header value.

---

Outside diff comments:
In `@src/website/src/config/cleanAirForumConfig.ts`:
- Around line 1-24: The Clean Air Forum config file is in the wrong folder for
the alias that consumers use, so imports from `@/configs/cleanAirForumConfig`
fail to resolve. Move `cleanAirForumConfig` from `config/` to `configs/`
(keeping the same exported constants like `CLEAN_AIR_FORUM_CURRENT_EVENT_ID`),
and make sure all imports in callers such as `route.ts` and
`SelfiesWallPage.tsx` continue to use the `@/configs/*` path.

---

Nitpick comments:
In `@docs/clean-air-forum-followups.md`:
- Around line 33-35: Keep the website on the Next.js proxy path: do not delete
the Next.js routes or switch `SelfiesWallPage.tsx` to call the backend directly.
Update the proxy handlers so the web app continues to forward requests through
`/api/v2` to the real API, preserving the Next.js proxy layer and avoiding
hard-coded backend URLs in `src/website/**/*.{ts,tsx,js,jsx}`.

In `@src/website/src/app/api/clean-air-forum/selfies/route.ts`:
- Around line 57-63: The rate-limit cache in selflies/route.ts grows without
bound because submissionTimestampsByIp only accumulates keys and never evicts
stale IPs. Update the logic around globalForRateLimit, submissionTimestampsByIp,
and the request-handling path that uses recent.push(now) to periodically sweep
and delete IP entries whose timestamps are all older than the window, or
otherwise cap/expire the Map so inactive visitors are removed over time.

In `@src/website/src/services/cleanAirForumAuth.ts`:
- Around line 21-31: The shared-secret check in cleanAirForumAuth currently uses
a normal string equality comparison, which should be replaced with a
constant-time comparison. Update the secret validation logic to compare
providedSecret and configuredSecret with a timing-safe method, after first
verifying both values have the same length so the comparison does not throw.
Keep the existing behavior for the isProduction and missing-secret branches
unchanged.
🪄 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: 3d1003ae-2717-4d8c-b6a8-f3cb00092dd9

📥 Commits

Reviewing files that changed from the base of the PR and between b9bcbcc and 9eb6980.

📒 Files selected for processing (9)
  • docs/clean-air-forum-followups.md
  • src/website/.env.sample
  • src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts
  • src/website/src/app/api/clean-air-forum/selfies/route.ts
  • src/website/src/app/selfies/page.tsx
  • src/website/src/config/cleanAirForumConfig.ts
  • src/website/src/services/cleanAirForumAuth.ts
  • src/website/src/services/cleanAirForumSelfiesStore.ts
  • src/website/src/views/selfies/SelfiesWallPage.tsx

Comment thread src/website/src/app/api/clean-air-forum/selfies/[id]/route.ts Outdated
Comment thread src/website/src/app/api/clean-air-forum/selfies/route.ts Outdated
- Deleted selfies API routes and associated logic for handling submissions and moderation.
- Removed SelfiesWallPage component and its related styles.
- Introduced new Faces of Clean Air page and service to display air quality selfies.
- Updated footer and navigation to include links to the new Faces of Clean Air section.
- Implemented a new service for fetching submissions from the backend.
- Added new UI components for displaying submissions with improved styling and animations.
@airqo-platform airqo-platform deleted a comment from github-actions Bot Jul 7, 2026
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 1.80180% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.79%. Comparing base (e4a9e69) to head (8e16ceb).
⚠️ Report is 35 commits behind head on staging.

Additional details and impacted files
Flag Coverage Δ
website 79.91% <1.80%> (-2.64%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

@@             Coverage Diff             @@
##           staging    #3756      +/-   ##
===========================================
- Coverage    65.69%   64.79%   -0.90%     
===========================================
  Files          156      158       +2     
  Lines         7812     7922     +110     
  Branches      1918     1920       +2     
===========================================
+ Hits          5132     5133       +1     
- Misses        2620     2727     +107     
- Partials        60       62       +2     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

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

🧹 Nitpick comments (2)
.github/workflows/website-ci.yml (1)

167-175: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Downloading the full job-log archive here is wasted work.

downloadJobLogsForWorkflowRun follows the redirect and pulls the entire log archive for the job, yet the result is only used as a truthiness gate — logUrl on Line 174 is built purely from context values and doesn't need logs at all. On top of that, when a single job has multiple failed steps you re-download the same archive and push duplicate log links pointing at the same job URL.

Since the URL is static, you can drop the download entirely (or hoist it to once-per-job if you actually intend to inspect content later).

♻️ Build the log link without the archive download
-                  // Attempt to fetch the step log for context
-                  try {
-                    const { data: logs } = await github.rest.actions.downloadJobLogsForWorkflowRun({
-                      owner: context.repo.owner,
-                      repo: context.repo.repo,
-                      job_id: job.id
-                    });
-                    if (logs) {
-                      failedLogs.push({ step: step.name, logUrl: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${job.id}` });
-                    }
-                  } catch (e) {
-                    // Logs may not be available; continue without them
-                  }
+                  failedLogs.push({
+                    step: step.name,
+                    logUrl: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${job.id}`
+                  });
🤖 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 @.github/workflows/website-ci.yml around lines 167 - 175, The failed-step log
collection in the workflow is downloading the full job archive unnecessarily in
the `downloadJobLogsForWorkflowRun` path. Remove that call from the loop in the
log-building logic and build the `logUrl` directly from `context` and `job.id`
wherever `failedLogs.push` is used; if you still need to inspect logs later,
hoist any download to once per job instead of repeating it per failed step. Also
ensure duplicate failed steps for the same job do not append repeated identical
log links.
src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx (1)

450-455: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use cn() for conditional Tailwind classes.

Both spots build conditional class strings via raw template literals. As per coding guidelines, src/website/src/**/*.{ts,tsx}: "Use cn() from @/lib/utils for conditional Tailwind class names."

♻️ Suggested cn() usage
+import { cn } from '`@/lib/utils`';
...
                 <div
-                  className={`relative overflow-hidden ${
-                    hasSecondRow
-                      ? 'min-h-[280px] lg:min-h-[580px]'
-                      : 'min-h-[280px]'
-                  }`}
+                  className={cn(
+                    'relative overflow-hidden min-h-[280px]',
+                    hasSecondRow && 'lg:min-h-[580px]',
+                  )}
                 >
...
                         <button
                           key={index}
                           type="button"
                           onClick={() => goToPage(index)}
                           aria-label={`Show selfie page ${index + 1} of ${totalPages}`}
                           aria-current={isActive ? 'page' : undefined}
-                          className={`h-2.5 rounded-full transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 ${
-                            isActive
-                              ? 'w-10 bg-white'
-                              : 'w-2.5 bg-white/40 hover:bg-white/60'
-                          }`}
+                          className={cn(
+                            'h-2.5 rounded-full transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2',
+                            isActive ? 'w-10 bg-white' : 'w-2.5 bg-white/40 hover:bg-white/60',
+                          )}
                         />

Also applies to: 504-508

🤖 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/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx` around
lines 450 - 455, The conditional Tailwind class strings in FacesOfCleanAirPage
should use cn() instead of raw template literals. Update the div className logic
in FacesOfCleanAirPage (including the second conditional spot referenced in the
review) to build classes through cn() from `@/lib/utils`, keeping the existing
static and conditional min-h classes equivalent.

Source: Coding guidelines

🤖 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/website-ci.yml:
- Around line 196-201: The "Failed step(s)" section in the workflow message
currently emits only table body rows from the failedSteps loop, so GitHub won’t
render it as a table. Update the body construction in the notification-building
logic to stop using table-style rows here and instead format failedSteps as a
simple bullet list or other plain markdown list, keeping the existing Failed
step(s) heading and the loop over failedSteps.

In `@CLAUDE.md`:
- Around line 7-20: The fenced directory tree block in CLAUDE.md is missing a
language tag, which will trigger markdown lint warnings. Update that code fence
to specify a language such as text so the block stays lint-clean and renders
correctly.

In `@src/website/src/app/`(programs)/faces-of-clean-air/page.tsx:
- Around line 5-10: The page metadata is being defined manually instead of using
the shared metadata configuration. Update the `metadata` export in
`faces-of-clean-air/page.tsx` to source its values from `METADATA_CONFIGS` via
`@/lib/metadata`, following the pattern used by other `page`/`layout` files, and
keep the page-specific title/description/robots settings within that
config-driven setup.

---

Nitpick comments:
In @.github/workflows/website-ci.yml:
- Around line 167-175: The failed-step log collection in the workflow is
downloading the full job archive unnecessarily in the
`downloadJobLogsForWorkflowRun` path. Remove that call from the loop in the
log-building logic and build the `logUrl` directly from `context` and `job.id`
wherever `failedLogs.push` is used; if you still need to inspect logs later,
hoist any download to once per job instead of repeating it per failed step. Also
ensure duplicate failed steps for the same job do not append repeated identical
log links.

In `@src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx`:
- Around line 450-455: The conditional Tailwind class strings in
FacesOfCleanAirPage should use cn() instead of raw template literals. Update the
div className logic in FacesOfCleanAirPage (including the second conditional
spot referenced in the review) to build classes through cn() from `@/lib/utils`,
keeping the existing static and conditional min-h classes equivalent.
🪄 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: 4b4dffe8-b2c5-49ed-bd6d-0409deacc8e9

📥 Commits

Reviewing files that changed from the base of the PR and between 9eb6980 and a8816fc.

⛔ Files ignored due to path filters (2)
  • src/website/public/assets/images/white-logo.png is excluded by !**/*.png
  • src/website/public/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (12)
  • .github/workflows/website-ci.yml
  • CLAUDE.md
  • src/website/.env.sample
  • src/website/AGENTS.md
  • src/website/src/app/(programs)/faces-of-clean-air/page.tsx
  • src/website/src/components/FloatingMiniBillboardWrapper.tsx
  • src/website/src/components/layout/Footer.tsx
  • src/website/src/config/cleanAirForumConfig.ts
  • src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx
  • src/website/src/services/api/api-routes.ts
  • src/website/src/services/external/faces-of-clean-air.service.ts
  • src/website/src/services/external/index.ts
✅ Files skipped from review due to trivial changes (2)
  • src/website/src/services/api/api-routes.ts
  • src/website/.env.sample

Comment thread .github/workflows/website-ci.yml
Comment thread CLAUDE.md Outdated
Comment on lines +7 to +20
```
src/
├── website/ # Next.js 14 website (airqo.net) - PRIMARY APP
├── platform/ # Analytics platform
├── mobile/ # Flutter mobile app
├── mobile-dashboard/ # Mobile dashboard
├── beacon/ # Beacon device UI
├── calibrate/ # Calibration tool
├── vertex/ # Vertex device UI
├── vertex-template/ # Vertex template
├── vertex-desktop/ # Vertex desktop app
├── docs-website/ # Documentation site
└── .github/ # GitHub Actions workflows
```

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the fenced block.

markdownlint-cli2 will flag this bare code fence. Adding a language (for example text) keeps the doc lint-clean and improves rendering.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@CLAUDE.md` around lines 7 - 20, The fenced directory tree block in CLAUDE.md
is missing a language tag, which will trigger markdown lint warnings. Update
that code fence to specify a language such as text so the block stays lint-clean
and renders correctly.

Source: Linters/SAST tools

Comment thread src/website/src/app/(programs)/faces-of-clean-air/page.tsx Outdated
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

@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/website/src/features/blogs/BlogsPage.tsx (1)

20-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting shared animation variants to a common utility.

The containerVariants and itemVariants objects are identical to those defined in AirQoDevConPage.tsx and PackagesPage.tsx. Extracting them to a shared module (e.g., lib/animation-variants.ts) would eliminate triplication and provide a single source of truth for scroll-reveal animation tuning.

♻️ Suggested shared variants module
+// src/website/src/lib/animation-variants.ts
+import type { Variants } from 'framer-motion';
+
+export const scrollContainerVariants: Variants = {
+  hidden: { opacity: 0 },
+  visible: {
+    opacity: 1,
+    transition: { staggerChildren: 0.1, duration: 0.5, ease: 'easeOut' },
+  },
+};
+
+export const scrollItemVariants: Variants = {
+  hidden: { opacity: 0, y: 30 },
+  visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } },
+};

Then in each page file:

-const containerVariants = {
-  hidden: { opacity: 0 },
-  visible: {
-    opacity: 1,
-    transition: { staggerChildren: 0.1, duration: 0.5, ease: 'easeOut' },
-  },
-};
-
-const itemVariants = {
-  hidden: { opacity: 0, y: 30 },
-  visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } },
-};
+import { scrollContainerVariants, scrollItemVariants } from '`@/lib/animation-variants`';
🤖 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/website/src/features/blogs/BlogsPage.tsx` around lines 20 - 32, The
animation variant objects in BlogsPage should not be duplicated across pages;
extract the shared scroll-reveal definitions used by containerVariants and
itemVariants into a common utility module, then import and reuse them in
BlogsPage, AirQoDevConPage, and PackagesPage so there is a single source of
truth for these motion settings.
🤖 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/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx`:
- Around line 120-121: The status copy in AirQoDevConPage now says “Applications
closed” while the CTA section and both Apply Now buttons in the same component
still invite users to apply, so align the whole flow in AirQoDevConPage. Update
the CTA text near the registration section to reflect closed registration, and
either remove or disable the apply actions that currently point to
DEVCON_APPLY_URL so they no longer contradict the closed status. Keep the
wording consistent across the status pill, the descriptive CTA copy, and the
button labels/behavior.

---

Nitpick comments:
In `@src/website/src/features/blogs/BlogsPage.tsx`:
- Around line 20-32: The animation variant objects in BlogsPage should not be
duplicated across pages; extract the shared scroll-reveal definitions used by
containerVariants and itemVariants into a common utility module, then import and
reuse them in BlogsPage, AirQoDevConPage, and PackagesPage so there is a single
source of truth for these motion settings.
🪄 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: 0212a744-5ca6-4b10-9ff6-ade124b09408

📥 Commits

Reviewing files that changed from the base of the PR and between a8816fc and fd3f5c4.

📒 Files selected for processing (10)
  • .github/workflows/website-ci.yml
  • src/website/src/app/(programs)/faces-of-clean-air/page.tsx
  • src/website/src/components/dialogs/EngagementDialog.tsx
  • src/website/src/config/cleanAirForumConfig.ts
  • src/website/src/features/blogs/BlogsPage.tsx
  • src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx
  • src/website/src/features/explore-data/ExplorePage.tsx
  • src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx
  • src/website/src/features/home/HomePage.tsx
  • src/website/src/features/packages/PackagesPage.tsx
✅ Files skipped from review due to trivial changes (3)
  • src/website/src/features/home/HomePage.tsx
  • src/website/src/components/dialogs/EngagementDialog.tsx
  • src/website/src/features/explore-data/ExplorePage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/website-ci.yml
  • src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx

@OchiengPaul442
OchiengPaul442 requested a review from Baalmart July 8, 2026 09:29

@OchiengPaul442 OchiengPaul442 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.

Previous implementation has been cleaned up

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

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

Caution

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

⚠️ Outside diff range comments (1)
src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx (1)

170-173: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Countdown copy still invites registration despite "Applications closed" status.

The countdown section says "Register your interest now and we will share joining details and preparation notes with registered students," which contradicts the "Applications closed" status pill and the final panel. Update this copy to match the closed state.

📝 Suggested copy update
             <p className="text-[15px] text-[`#71717a`] leading-[1.6]">
               AirQo DevCon runs on 17&ndash;18 June 2026 at Makerere University.
-              Register your interest now and we will share joining details and
-              preparation notes with registered students.
+              Applications are now closed. We will share joining details and
+              preparation notes with registered students.
             </p>
🤖 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/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx` around
lines 170 - 173, Update the countdown copy in AirQoDevConPage so it matches the
closed state shown by the status pill and final panel. In the section that
renders the event details and interest message, replace the “Register your
interest now” wording with closed-state language that no longer invites new
registration, while still referencing the 17–18 June 2026 event details in a way
consistent with the existing AirQoDevConPage content.
🧹 Nitpick comments (1)
src/website/src/hooks/usePollingWithVisibility.ts (1)

48-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fetch immediately when the tab regains visibility.

When the tab becomes visible again, start() creates a new interval but does not invoke the callback right away. If activeIntervalMs is large, users see stale data until the first tick fires. The PR objective calls out "visibility-aware fetching for better data loading" — an immediate refresh on resume would align with that goal.

♻️ Proposed fix: trigger immediate fetch on visibility regain
     const handleVisibilityChange = () => {
       if (document.visibilityState === 'visible') {
+        void invoke();
         start();
       } else {
         stop();
       }
     };
🤖 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/website/src/hooks/usePollingWithVisibility.ts` around lines 48 - 55, The
visibility handler in usePollingWithVisibility should trigger an immediate
refresh when the tab becomes visible again, not just restart the interval.
Update the handleVisibilityChange logic inside useEffect so that the visible
branch still calls start() but also invokes the polling callback immediately (or
uses the existing polling helper that does so) to avoid stale data after resume.
Keep the change localized around the useEffect/handleVisibilityChange behavior
in usePollingWithVisibility.
🤖 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/website/src/features/products/AnalyticsPage.tsx`:
- Around line 146-151: The Documentation link in AnalyticsPage opens in the same
tab because the onClick handler calls window.open without a target. Update the
existing window.open call in the AnalyticsPage component’s Documentation button
to use '_blank', matching the other external link on this page so it opens in a
new tab consistently.

In `@src/website/src/hooks/usePollingWithVisibility.ts`:
- Around line 28-33: The start callback in usePollingWithVisibility currently
schedules callbackRef.current() on a fixed setInterval cadence, which can
overlap async runs and let stale responses win; update the start/stop polling
flow to track an in-flight flag (or similar) around the interval handler so a
new tick is skipped while the previous callback is still pending. Use the
existing start, stop, callbackRef, and intervalRef logic in
usePollingWithVisibility to implement the guard cleanly without changing the
consumer API.

---

Outside diff comments:
In `@src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx`:
- Around line 170-173: Update the countdown copy in AirQoDevConPage so it
matches the closed state shown by the status pill and final panel. In the
section that renders the event details and interest message, replace the
“Register your interest now” wording with closed-state language that no longer
invites new registration, while still referencing the 17–18 June 2026 event
details in a way consistent with the existing AirQoDevConPage content.

---

Nitpick comments:
In `@src/website/src/hooks/usePollingWithVisibility.ts`:
- Around line 48-55: The visibility handler in usePollingWithVisibility should
trigger an immediate refresh when the tab becomes visible again, not just
restart the interval. Update the handleVisibilityChange logic inside useEffect
so that the visible branch still calls start() but also invokes the polling
callback immediately (or uses the existing polling helper that does so) to avoid
stale data after resume. Keep the change localized around the
useEffect/handleVisibilityChange behavior in usePollingWithVisibility.
🪄 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: 485b0d49-6b8e-4ab6-83c0-990ca92ae029

📥 Commits

Reviewing files that changed from the base of the PR and between fd3f5c4 and 0692f8f.

📒 Files selected for processing (9)
  • src/website/README.md
  • src/website/src/app/layout.tsx
  • src/website/src/components/FloatingMiniBillboard.tsx
  • src/website/src/config/cleanAirForumConfig.ts
  • src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx
  • src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx
  • src/website/src/features/products/AnalyticsPage.tsx
  • src/website/src/hooks/usePollingWithVisibility.ts
  • src/website/src/lib/utils/airQuality.ts
✅ Files skipped from review due to trivial changes (4)
  • src/website/src/app/layout.tsx
  • src/website/README.md
  • src/website/src/components/FloatingMiniBillboard.tsx
  • src/website/src/lib/utils/airQuality.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/website/src/config/cleanAirForumConfig.ts
  • src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx

Comment thread src/website/src/features/products/AnalyticsPage.tsx Outdated
Comment thread src/website/src/hooks/usePollingWithVisibility.ts Outdated
…tial backoff for error handling in usePollingWithVisibility hook
…ors and improve polling logic to prevent stale data
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

New azure website changes available for preview here

@Baalmart
Baalmart merged commit 04086b1 into staging Jul 9, 2026
27 checks passed
@Baalmart
Baalmart deleted the feature/clean-air-forum-selfie-filter-website branch July 9, 2026 05:04
@Baalmart Baalmart mentioned this pull request Jul 9, 2026
6 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