Skip to content

fix: fix bugs in event attendance page#309

Merged
jakehobbs merged 7 commits intomainfrom
jake/event-attendance-fixes
Feb 28, 2026
Merged

fix: fix bugs in event attendance page#309
jakehobbs merged 7 commits intomainfrom
jake/event-attendance-fixes

Conversation

@jakehobbs
Copy link
Member

@jakehobbs jakehobbs commented Feb 28, 2026

Summary by CodeRabbit

  • New Features

    • Suggestions now appear in a popover with improved keyboard handling.
    • Activists and lists include a last-event timestamp, shown and sorted by most recent.
    • Public API responses now include last-event timestamps.
  • Bug Fixes

    • Prevented crashes from missing attendee names.
    • Avoided state updates after unmount; improved sync stability.
    • Improved local database upgrade handling with clear error when blocked.
  • Documentation

    • Added component customization guidance.
  • Chores

    • Database schema bumped; migration forces full resync and adds new timestamp indexes.

…for autocomplete, address misc comments from prior changes
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds last-event timestamps end-to-end: DB migration and indexes, API schema and server model/queries exposing last_event_date, frontend storage/sorting and in-memory registry updates, and UI suggestion/popover changes for attendee input.

Changes

Cohort / File(s) Summary
Activist registry & storage
frontend-v2/src/app/event/activist-registry.ts, frontend-v2/src/app/event/activist-storage.ts
Added lastEventDate/last_event_date fields; introduced private sortActivists() and calls after load/merge/remove to keep activists sorted descending (zeros last). Bumped DB_VERSION to 2 and added migration clearing stores for full resync; improved IndexedDB upgrade handling.
Frontend API types
frontend-v2/src/lib/api.ts
Extended ActivistListBasicResp items with last_event_date: z.number().
Frontend UI & inputs
frontend-v2/src/app/event/attendee-input-field.tsx, frontend-v2/src/app/event/event-form.tsx
Replaced inline suggestion list with Popover/PopoverContent and improved keyboard/focus handling; added safe `(a.name
Frontend hooks / state
frontend-v2/src/app/event/useActivistRegistry.ts
Added mounted guards to effects, detect newer attendance via last_event_date, map to ms lastEventDate, include it in update payloads, and avoid state updates after unmount.
Backend models & handlers
server/src/model/activist.go, server/src/main.go
Added LastEventDate to DB and JSON structs; ToJSON() populates last_event_date. Changed handler and list/get functions to take modifiedSince time.Time (zero-value semantics) and updated GetActivistListBasicJSON to SELECT MAX(e.date) AS last_event_date with HAVING filtering.
DB migrations
pkg/shared/db-migrations/20260131054408_activist_timestamp_indexes.up.sql
Added idx_email_updated and idx_phone_updated indexes and a note about potential chapter-scoped timestamp indexes.
Docs
frontend-v2/src/components/ui/README.md
Added "Customizations" section documenting preserved SelectTrigger border styling during shadcn upgrades.

Sequence Diagram(s)

sequenceDiagram
    participant Backend as Backend (server/model)
    participant API as API (server handler)
    participant Frontend as Frontend (useActivistRegistry)
    participant Storage as Storage (IndexedDB)
    participant Registry as Registry (activist-registry)
    participant UI as UI (attendee-input-field)

    Backend->>Backend: Query activists with MAX(e.date) AS last_event_date
    Backend->>API: Return ActivistListBasicResp (includes last_event_date)
    API->>Frontend: Deliver activists payload
    Frontend->>Frontend: Compare last_event_date to local lastEventDate (ms)
    Frontend->>Storage: Persist StoredActivist including lastEventDate
    Frontend->>Registry: Load and sort activists by lastEventDate (desc, zeros last)
    Registry->>UI: Provide sorted suggestions for Popover
    UI->>Frontend: User selects/updates attendee -> update flows back to registry/storage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through timestamps, tidy and bright,
Lists sorted by the last event's light.
Popovers whisper names in line,
DB indexes hum — everything's fine.
A rabbit's record, neat and right.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'fix: fix bugs in event attendance page' is vague and generic, using non-descriptive language that doesn't convey the specific changes made. The title should be more specific and descriptive. Consider: 'fix: sort activist autocomplete by last event and use popover component' or similar, reflecting the main changes documented in the PR objectives.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jake/event-attendance-fixes

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

@jakehobbs jakehobbs marked this pull request as ready for review February 28, 2026 21:25
@jakehobbs jakehobbs requested a review from alexsapps as a code owner February 28, 2026 21:25
Copilot AI review requested due to automatic review settings February 28, 2026 21:25
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
server/src/model/activist.go (1)

1665-1754: ⚠️ Potential issue | 🟡 Minor

Formatting check is currently blocking CI.

Build logs indicate a misformatted file. Please run make fmt before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/model/activist.go` around lines 1665 - 1754, The file is failing
gofmt; run the repository formatter (make fmt) or gofmt/goimports over this file
(e.g., reformat server/src/model/activist.go) to fix spacing/indentation and
import formatting, then recommit; ensure functions/structs such as
ActivistBasicInfoJSON, ActivistBasicInfo, ToJSON, GetActivistListBasicJSON and
GetHiddenActivistIDs remain unchanged except for whitespace.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend-v2/src/app/event/activist-registry.ts`:
- Around line 153-160: The current per-keystroke sort in getSuggestions() is
expensive; remove the .sort(...) call from getSuggestions() and keep
this.activists pre-sorted by lastEventDate when you merge/update the registry
(e.g., in the method that inserts/updates activists or the
mergeActivists/updateActivists routine). Ensure merge/update maintains
descending order (treat lastEventDate===0 as lowest) by performing stable
insertion (binary insert) or sorting only once after batch updates, then have
getSuggestions() simply filter the pre-sorted this.activists and slice the top N
results.

In `@frontend-v2/src/app/event/attendee-input-field.tsx`:
- Around line 82-85: When clearing suggestions on Escape (the keydown 'Escape'
case) and in the blur handler that also calls setSuggestions([]) /
setOpen(false), also reset the selected suggestion state by calling
setSelectedSuggestionIndex(-1) (or the appropriate "no-selection" value) so
selectedSuggestionIndex cannot point to a stale entry; update the 'Escape'
branch and the blur/clear locations (where setSuggestions([]) and setOpen(false)
are used) to include this setter call.

In `@frontend-v2/src/app/event/event-form.tsx`:
- Around line 115-117: There is a duplicate call to
queryClient.invalidateQueries with queryKey [API_PATH.ACTIVIST_LIST_BASIC];
remove the earlier/extra invocation so only the intended single invalidation
remains (keep the existing invalidateQueries([API_PATH.ACTIVIST_LIST_BASIC])
call that runs later in the save flow). Locate the duplicate by searching for
queryClient.invalidateQueries and the queryKey API_PATH.ACTIVIST_LIST_BASIC in
event-form.tsx and delete the redundant call.

---

Outside diff comments:
In `@server/src/model/activist.go`:
- Around line 1665-1754: The file is failing gofmt; run the repository formatter
(make fmt) or gofmt/goimports over this file (e.g., reformat
server/src/model/activist.go) to fix spacing/indentation and import formatting,
then recommit; ensure functions/structs such as ActivistBasicInfoJSON,
ActivistBasicInfo, ToJSON, GetActivistListBasicJSON and GetHiddenActivistIDs
remain unchanged except for whitespace.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcd07d2 and 21124b7.

📒 Files selected for processing (10)
  • frontend-v2/src/app/event/activist-registry.ts
  • frontend-v2/src/app/event/activist-storage.ts
  • frontend-v2/src/app/event/attendee-input-field.tsx
  • frontend-v2/src/app/event/event-form.tsx
  • frontend-v2/src/app/event/useActivistRegistry.ts
  • frontend-v2/src/components/ui/README.md
  • frontend-v2/src/lib/api.ts
  • pkg/shared/db-migrations/20260131054408_activist_timestamp_indexes.up.sql
  • server/src/main.go
  • server/src/model/activist.go

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates activist basic-list syncing and UI autocomplete to better reflect recent event activity, adding support for “last event date” so the frontend can sort suggestions by most recent attendee activity.

Changes:

  • Server: extend activist “list_basic” payload with last_event_date, and update query/sync filtering to incorporate event attendance.
  • Frontend: store/consume last_event_date, sort autocomplete suggestions by recency, and render suggestions in a popover.
  • Misc: IndexedDB schema bump + migration behavior, cache invalidation after event save, and small robustness/docs tweaks.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/src/model/activist.go Adds last_event_date to basic activist payload and updates list query/sync filtering + ordering
server/src/main.go Switches modified_since handling from nullable pointer to time.Time zero-value semantics
pkg/shared/db-migrations/20260131054408_activist_timestamp_indexes.up.sql Adds/clarifies index/migration notes for timestamp-based filtering
frontend-v2/src/lib/api.ts Extends Zod schema to include last_event_date
frontend-v2/src/components/ui/README.md Documents shadcn UI component customization to preserve on upgrades
frontend-v2/src/app/event/useActivistRegistry.ts Adds mounted guards; maps last_event_date into registry records
frontend-v2/src/app/event/event-form.tsx Invalidates activist list query after save; guards undefined attendee names
frontend-v2/src/app/event/attendee-input-field.tsx Replaces always-visible suggestion list with a controlled popover-based autocomplete
frontend-v2/src/app/event/activist-storage.ts Adds lastEventDate, bumps IndexedDB version, and clears cache/metadata on upgrade
frontend-v2/src/app/event/activist-registry.ts Adds lastEventDate, sorts suggestions by recency, and adjusts method docs/logic

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

@jakehobbs
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
frontend-v2/src/app/event/activist-registry.ts (1)

76-79: 🧹 Nitpick | 🔵 Trivial

Consider removing O(n×m) lookup in mergeActivists.

findIndex per incoming activist can get expensive on large full-sync batches. Building an ID→index map once per merge keeps this path linear before the final sort.

♻️ Refactor sketch
   async mergeActivists(newActivists: ActivistRecord[]): Promise<void> {
     if (newActivists.length === 0) {
       return
     }

+    const indexById = new Map(this.activists.map((a, i) => [a.id, i]))
+
     for (const activist of newActivists) {
-      const existingIndex = this.activists.findIndex(
-        (a) => a.id === activist.id,
-      )
+      const existingIndex = indexById.get(activist.id) ?? -1

       if (existingIndex >= 0) {
         // Update existing activist (handles renames properly)
         const oldActivist = this.activists[existingIndex]
         this.activists[existingIndex] = activist
@@
       } else {
         // Add new activist
         this.activists.push(activist)
+        indexById.set(activist.id, this.activists.length - 1)
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/event/activist-registry.ts` around lines 76 - 79, The
mergeActivists function currently does an O(n×m) lookup by calling
this.activists.findIndex for each activist in newActivists; instead, build a
single Map (or plain object) mapping existing activist id → index from
this.activists once, then iterate newActivists and use that map to locate and
update existing entries (or push new ones) before performing the final sort.
Update references to existingIndex to use the map lookup, and ensure the map is
built from this.activists only once at the start of mergeActivists so the merge
work is linear in the number of activists plus the sort.
frontend-v2/src/app/event/activist-storage.ts (1)

25-42: ⚠️ Potential issue | 🟠 Major

Add onblocked and onversionchange handlers to prevent indefinite loading from IndexedDB upgrade conflicts.

With DB_VERSION at 2, database upgrades can be blocked by an older open tab. The openDB() method currently lacks a request.onblocked handler and does not close connections on versionchange, allowing the promise to hang indefinitely and keep the UI in a loading state.

🔧 Proposed fix
       this.dbPromise = new Promise((resolve, reject) => {
         const request = indexedDB.open(DB_NAME, DB_VERSION)

         request.onerror = () => reject(request.error)
+        request.onblocked = () => {
+          reject(
+            new Error(
+              'IndexedDB upgrade blocked by another open tab. Close other tabs and retry.',
+            ),
+          )
+        }
         request.onsuccess = () => resolve(request.result)
+        request.onsuccess = () => {
+          const db = request.result
+          db.onversionchange = () => {
+            db.close()
+          }
+          resolve(db)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/event/activist-storage.ts` around lines 25 - 42, The
openDB() promise can hang during upgrades; add a request.onblocked handler to
reject the promise with a clear error when another tab is preventing the
upgrade, and after request.onsuccess set the returned DB's onversionchange to
immediately close the connection (db.onversionchange = () => db.close()) so
other tabs can upgrade and the UI won't remain stuck; reference DB_VERSION,
openDB(), the IndexedDB request object (request.onblocked) and the resolved
IDBDatabase (db.onversionchange) when making these changes.
♻️ Duplicate comments (1)
frontend-v2/src/app/event/attendee-input-field.tsx (1)

64-75: ⚠️ Potential issue | 🟠 Major

Validate suggestion index bounds before resolving selected values.

At Line 90 and Line 106, suggestions[selectedSuggestionIndex] is used when index is only checked for >= 0. Combined with Line 64-75 (ArrowDown/ArrowUp), this can produce out-of-range access and submit undefined on Enter/Tab when suggestions are empty.

🔧 Proposed fix
       case 'ArrowDown': {
+        if (suggestions.length === 0) {
+          return
+        }
         e.preventDefault()
         setSelectedSuggestionIndex((prev) =>
           prev === suggestions.length - 1 ? 0 : prev + 1,
         )
         return
       }
       case 'ArrowUp': {
+        if (suggestions.length === 0) {
+          return
+        }
         e.preventDefault()
         setSelectedSuggestionIndex((prev) =>
           prev === 0 ? suggestions.length - 1 : prev - 1,
         )
         return
       }
@@
-        const selectedValue =
-          selectedSuggestionIndex >= 0
-            ? suggestions[selectedSuggestionIndex]
-            : trimmedValue
+        const hasSelectedSuggestion =
+          selectedSuggestionIndex >= 0 &&
+          selectedSuggestionIndex < suggestions.length
+        const selectedValue = hasSelectedSuggestion
+          ? suggestions[selectedSuggestionIndex]
+          : trimmedValue
         handleSelectSuggestion(selectedValue)
         return
       }
@@
-        const selectedValue =
-          selectedSuggestionIndex >= 0
-            ? suggestions[selectedSuggestionIndex]
-            : trimmedValue
+        const hasSelectedSuggestion =
+          selectedSuggestionIndex >= 0 &&
+          selectedSuggestionIndex < suggestions.length
+        const selectedValue = hasSelectedSuggestion
+          ? suggestions[selectedSuggestionIndex]
+          : trimmedValue
         handleSelectSuggestion(selectedValue)
       }

Also applies to: 90-93, 106-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/event/attendee-input-field.tsx` around lines 64 - 75, The
arrow key handlers and Enter/Tab handlers can produce out-of-range accesses when
suggestions is empty or selectedSuggestionIndex points outside
0..suggestions.length-1; update the logic in the ArrowDown/ArrowUp handlers
(where setSelectedSuggestionIndex is used) to guard with suggestions.length>0
and keep selectedSuggestionIndex within [0, suggestions.length-1] (e.g., set to
-1 or 0 appropriately when suggestions is empty), and in the Enter/Tab handling
(where suggestions[selectedSuggestionIndex] is read) validate
suggestions.length>0 and selectedSuggestionIndex >= 0 && selectedSuggestionIndex
< suggestions.length before using suggestions[selectedSuggestionIndex] — if the
check fails, avoid submitting/using undefined and handle the empty case (no-op
or fallback) instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@frontend-v2/src/app/event/activist-registry.ts`:
- Around line 76-79: The mergeActivists function currently does an O(n×m) lookup
by calling this.activists.findIndex for each activist in newActivists; instead,
build a single Map (or plain object) mapping existing activist id → index from
this.activists once, then iterate newActivists and use that map to locate and
update existing entries (or push new ones) before performing the final sort.
Update references to existingIndex to use the map lookup, and ensure the map is
built from this.activists only once at the start of mergeActivists so the merge
work is linear in the number of activists plus the sort.

In `@frontend-v2/src/app/event/activist-storage.ts`:
- Around line 25-42: The openDB() promise can hang during upgrades; add a
request.onblocked handler to reject the promise with a clear error when another
tab is preventing the upgrade, and after request.onsuccess set the returned DB's
onversionchange to immediately close the connection (db.onversionchange = () =>
db.close()) so other tabs can upgrade and the UI won't remain stuck; reference
DB_VERSION, openDB(), the IndexedDB request object (request.onblocked) and the
resolved IDBDatabase (db.onversionchange) when making these changes.

---

Duplicate comments:
In `@frontend-v2/src/app/event/attendee-input-field.tsx`:
- Around line 64-75: The arrow key handlers and Enter/Tab handlers can produce
out-of-range accesses when suggestions is empty or selectedSuggestionIndex
points outside 0..suggestions.length-1; update the logic in the
ArrowDown/ArrowUp handlers (where setSelectedSuggestionIndex is used) to guard
with suggestions.length>0 and keep selectedSuggestionIndex within [0,
suggestions.length-1] (e.g., set to -1 or 0 appropriately when suggestions is
empty), and in the Enter/Tab handling (where
suggestions[selectedSuggestionIndex] is read) validate suggestions.length>0 and
selectedSuggestionIndex >= 0 && selectedSuggestionIndex < suggestions.length
before using suggestions[selectedSuggestionIndex] — if the check fails, avoid
submitting/using undefined and handle the empty case (no-op or fallback)
instead.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcd07d2 and 35f530b.

📒 Files selected for processing (10)
  • frontend-v2/src/app/event/activist-registry.ts
  • frontend-v2/src/app/event/activist-storage.ts
  • frontend-v2/src/app/event/attendee-input-field.tsx
  • frontend-v2/src/app/event/event-form.tsx
  • frontend-v2/src/app/event/useActivistRegistry.ts
  • frontend-v2/src/components/ui/README.md
  • frontend-v2/src/lib/api.ts
  • pkg/shared/db-migrations/20260131054408_activist_timestamp_indexes.up.sql
  • server/src/main.go
  • server/src/model/activist.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
frontend-v2/src/app/event/event-form.tsx (1)

307-314: ⚠️ Potential issue | 🟡 Minor

Guard ensureMinimumEmptyFields against undefined attendee names.

Line 313 still dereferences it.name.length directly. If undefined names are possible (as handled in the updated segments), this can still throw at runtime.

♻️ Proposed fix
   const ensureMinimumEmptyFields = () => {
     const currentAttendees = form.state.values.attendees
-    const emptyCount = currentAttendees.filter((it) => !it.name.length).length
+    const emptyCount = currentAttendees.filter(
+      (it) => (it.name || '').trim() === '',
+    ).length
     if (emptyCount < MIN_EMPTY_FIELDS) {
       form.pushFieldValue('attendees', { name: '' })
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/event/event-form.tsx` around lines 307 - 314, In
ensureMinimumEmptyFields, avoid dereferencing undefined attendee names by
changing the emptyCount calculation to check names safely; when iterating
currentAttendees (from form.state.values.attendees) use a guarded expression
such as (it.name || '') instead of it.name so you compute emptyCount via
((it.name || '').length === 0) or !((it.name || '').length); update the
emptyCount line in the ensureMinimumEmptyFields function accordingly to prevent
runtime exceptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend-v2/src/components/ui/README.md`:
- Around line 5-15: Add a concrete code example and upgrade notes for the
SelectTrigger customization: paste the actual SelectTrigger JSX from select.tsx
showing the className composition (reference the SelectTrigger component, the
className prop, and the cn utility) and highlight the custom classes
"hover:border-gray-400", "focus:border-primary", and
"focus:hover:border-primary"; add a short upgrade instruction telling
maintainers to run `npx shadcn@latest add select` then manually reapply those
classes to the SelectTrigger's className if the generator overwrites the file;
finally add a one-line rationale stating these classes were added to improve
hover/focus visual feedback during user interaction.

---

Outside diff comments:
In `@frontend-v2/src/app/event/event-form.tsx`:
- Around line 307-314: In ensureMinimumEmptyFields, avoid dereferencing
undefined attendee names by changing the emptyCount calculation to check names
safely; when iterating currentAttendees (from form.state.values.attendees) use a
guarded expression such as (it.name || '') instead of it.name so you compute
emptyCount via ((it.name || '').length === 0) or !((it.name || '').length);
update the emptyCount line in the ensureMinimumEmptyFields function accordingly
to prevent runtime exceptions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcd07d2 and 35f530b.

📒 Files selected for processing (10)
  • frontend-v2/src/app/event/activist-registry.ts
  • frontend-v2/src/app/event/activist-storage.ts
  • frontend-v2/src/app/event/attendee-input-field.tsx
  • frontend-v2/src/app/event/event-form.tsx
  • frontend-v2/src/app/event/useActivistRegistry.ts
  • frontend-v2/src/components/ui/README.md
  • frontend-v2/src/lib/api.ts
  • pkg/shared/db-migrations/20260131054408_activist_timestamp_indexes.up.sql
  • server/src/main.go
  • server/src/model/activist.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
frontend-v2/src/app/event/activist-registry.ts (2)

195-201: 🧹 Nitpick | 🔵 Trivial

Regex compilation on every filter call is acceptable but could be optimized.

The nameFilter function compiles a new RegExp for each activist in the filter loop. For large activist lists, this could be optimized by compiling once per getSuggestions call.

♻️ Compile regex once per search
-  getSuggestions(input: string, maxResults = 10): string[] {
+  getSuggestions(input: string, maxResults = 10): string[] {
     const trimmedInput = input.trim()
     if (!trimmedInput.length) {
       return []
     }

-    // this.activists is pre-sorted by lastEventDate descending
-    return this.activists
-      .filter(({ name }) => nameFilter(name, input))
+    const pattern = trimmedInput
+      .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+      .replace(/ +/g, '.*')
+    const regex = new RegExp(pattern, 'i')
+
+    return this.activists
+      .filter(({ name }) => regex.test(name))
       .slice(0, maxResults)
       .map((a) => a.name)
   }

This compiles the regex once per getSuggestions call instead of once per activist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/event/activist-registry.ts` around lines 195 - 201, The
current nameFilter(text: string, input: string) compiles a new RegExp for every
activist; change the approach so the regex is compiled once in getSuggestions
and then reused for each activist: add an overload or new helper (e.g.,
nameFilterWithRegex or change nameFilter signature to nameFilter(text: string,
pattern: RegExp)) and move the escaping/replace logic out of the per-item loop
into getSuggestions where you build the RegExp once from the raw input, then
call the updated nameFilter for each activist; ensure existing callers are
updated to pass a RegExp and keep the same case-insensitive behavior (/i) and
escaping logic.

112-126: 🧹 Nitpick | 🔵 Trivial

Consider adding early return for empty ids array.

When ids is empty, the method still creates a Set, filters the array, and potentially calls storage. Adding an early return would avoid unnecessary work.

♻️ Add early return for empty input
   async removeActivistsByIds(ids: number[]): Promise<void> {
+    if (ids.length === 0) return
+
     const idsToRemove = new Set(ids)

     this.activists = this.activists.filter((activist) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/event/activist-registry.ts` around lines 112 - 126, The
method removeActivistsByIds currently does work even when ids is empty; add an
early return at the start of removeActivistsByIds to return immediately if
ids.length === 0 to avoid creating idsToRemove, iterating this.activists, and
calling this.storage.deleteActivistsByIds; keep the existing logic intact for
non-empty ids and ensure the signature and Promise<void> behavior remain
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend-v2/src/app/event/activist-storage.ts`:
- Around line 41-55: The cached dbPromise can become stale after a version
change; update the handlers so that when db.onversionchange runs you both close
the connection and reset the shared dbPromise (e.g., set it to null/undefined)
so subsequent callers can reopen the DB, and likewise ensure the
request.onblocked path clears the cached dbPromise when rejecting so the cache
doesn't hold a rejected/stale promise; locate the logic around
request.onsuccess, db.onversionchange and request.onblocked to implement these
changes.

In `@frontend-v2/src/app/event/attendee-input-field.tsx`:
- Around line 36-43: The open boolean is derived from suggestions.length > 0 &&
isFocused so remove the useEffect and the open state variable; instead compute a
derived value (e.g., const open = suggestions.length > 0 && isFocused or use
useMemo) and remove any setOpen(...) calls in handlers (references: useEffect
block, setOpen calls in handlers that currently close the popover). Ensure
handlers that cleared suggestions still work (they should just clear suggestions
or blur focus) so the derived open becomes false automatically.
- Around line 73-79: The ArrowUp handler for suggestions can produce -2 when
selectedSuggestionIndex is -1; update the logic in the ArrowUp case (where
setSelectedSuggestionIndex is called) to explicitly handle prev === -1 by
setting the index to suggestions.length - 1, otherwise keep the existing wrap
logic (prev === 0 ? suggestions.length - 1 : prev - 1); ensure this change is
made inside the same ArrowUp branch that checks suggestions.length > 0 so
keyboard cycling from no selection behaves correctly.

In `@frontend-v2/src/app/event/useActivistRegistry.ts`:
- Around line 78-91: Remove query.error from the useEffect dependency list so
the effect only reacts to query.isError changes; inside the effect, read
query.error only when query.isError is true (for logging) or capture it into a
local variable at the top of the effect if you need its value for the log, and
keep the mounted guard and setIsServerLoaded usage (functions/methods involved:
useEffect, mounted flag, query.isError, query.error, setIsServerLoaded,
toast.error, console.error).

---

Outside diff comments:
In `@frontend-v2/src/app/event/activist-registry.ts`:
- Around line 195-201: The current nameFilter(text: string, input: string)
compiles a new RegExp for every activist; change the approach so the regex is
compiled once in getSuggestions and then reused for each activist: add an
overload or new helper (e.g., nameFilterWithRegex or change nameFilter signature
to nameFilter(text: string, pattern: RegExp)) and move the escaping/replace
logic out of the per-item loop into getSuggestions where you build the RegExp
once from the raw input, then call the updated nameFilter for each activist;
ensure existing callers are updated to pass a RegExp and keep the same
case-insensitive behavior (/i) and escaping logic.
- Around line 112-126: The method removeActivistsByIds currently does work even
when ids is empty; add an early return at the start of removeActivistsByIds to
return immediately if ids.length === 0 to avoid creating idsToRemove, iterating
this.activists, and calling this.storage.deleteActivistsByIds; keep the existing
logic intact for non-empty ids and ensure the signature and Promise<void>
behavior remain unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35f530b and 81b9501.

📒 Files selected for processing (4)
  • frontend-v2/src/app/event/activist-registry.ts
  • frontend-v2/src/app/event/activist-storage.ts
  • frontend-v2/src/app/event/attendee-input-field.tsx
  • frontend-v2/src/app/event/useActivistRegistry.ts

@jakehobbs jakehobbs changed the title fix: sort activist autocomplete by last event, use popover component for autocomplete, address misc comments from prior changes fix: fix bugs in event attendance page Feb 28, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend-v2/src/app/event/useActivistRegistry.ts`:
- Around line 118-129: The current merge logic in useActivistRegistry overwrites
the entire activist record when either attendance or profile is newer, causing
fresher profile fields to be rolled back when only attendance is newer
(hasNewAttendanceData) and vice versa; change the push into activistsToUpdate to
do a field-wise merge: start from existingActivist (if any), then overwrite only
profile fields when incomingTimestamp > existingTimestamp, and overwrite only
lastEventDate when hasNewAttendanceData (convert last_event_date * 1000), and
set lastUpdated only when the profile timestamp is newer; ensure you reference
hasNewAttendanceData, incomingTimestamp, existingTimestamp, last_event_date,
lastUpdated and lastEventDate while composing the merged object so untouched
newer fields are preserved.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81b9501 and 9e4fe9b.

📒 Files selected for processing (4)
  • frontend-v2/src/app/event/activist-registry.ts
  • frontend-v2/src/app/event/activist-storage.ts
  • frontend-v2/src/app/event/attendee-input-field.tsx
  • frontend-v2/src/app/event/useActivistRegistry.ts

@jakehobbs jakehobbs merged commit c0779c1 into main Feb 28, 2026
2 checks passed
@jakehobbs jakehobbs deleted the jake/event-attendance-fixes branch February 28, 2026 22:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants