Skip to content

feat: Admin 'Mark Past as Completed' button — auto-complete past events from UI - #39

Open
jerry-shimizutech wants to merge 1 commit into
mainfrom
feature/admin-auto-complete-past-events
Open

feat: Admin 'Mark Past as Completed' button — auto-complete past events from UI#39
jerry-shimizutech wants to merge 1 commit into
mainfrom
feature/admin-auto-complete-past-events

Conversation

@jerry-shimizutech

@jerry-shimizutech jerry-shimizutech commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses inbox request: 'fix up the admin side so the admin side is where we could do all this stuff... make sure nothing is hardcoded'

What This Does

Adds a "Mark Past as Completed" button to the Events admin page. One click automatically finds all upcoming/published events whose date has passed and marks them as completed — no migrations, no rake tasks, no hardcoded values needed.

Changes

Backend:

  • POST /api/v1/admin/events/auto_complete_past — new collection action (requires staff auth)
    • Queries COALESCE(end_date, date) < today to find stale upcoming events
    • Updates status to completed, returns count + names of updated events
    • Safe to run multiple times (idempotent — won't touch already-completed events)

Frontend:

  • New "Mark Past as Completed" button in EventsAdmin header (next to Create Event)
  • Shows loading spinner while running
  • Displays success message: "Marked N event(s) as completed." or "No past events needed updating."
  • Reloads the event table after completion

Usage

  1. Go to Admin → Events
  2. Click "Mark Past as Completed"
  3. Done — past events are now marked correctly

No more need to run rake tasks or write migrations when an event finishes.

Greptile Summary

This PR adds a "Mark Past as Completed" button to the Events admin page, backed by a new POST /api/v1/admin/events/auto_complete_past endpoint. The feature is a convenient replacement for manual rake tasks — it finds all upcoming/published events whose date has passed and flips their status to completed.

Issues found:

  • Unscoped query (P1): auto_complete_past queries Event.where(...) directly instead of scoping to org.events like every other action in the controller. If more than one organization record exists in the database (e.g., staging data), this will affect events across all tenants.
  • No transaction around the update loop (P1): Individual event.update! calls inside a plain each loop mean that if any update raises (e.g., a validation error), earlier events will already be committed as completed with no rollback — resulting in a partially-completed operation with no clear recovery path.
  • N+1 UPDATE queries (P2): The loop issues one SQL UPDATE per event. A single update_all call would be significantly more efficient, especially as the event list grows.
  • Uncleared setTimeout in handleAutoCompletePast (P2): The 5-second timeout that clears the success message is never cancelled if the component unmounts before it fires.

Confidence Score: 2/5

  • Not safe to merge — the controller action lacks a transaction and an organization scope, creating real risks of partial data corruption and cross-tenant contamination.
  • Two P1 issues in the controller: (1) the missing transaction means a mid-loop failure leaves the database in a partially-updated state with no rollback, and (2) the unscoped Event.where(...) query bypasses the organization boundary that every other action in this controller respects. Both issues are straightforward to fix but have meaningful data-integrity consequences.
  • api/app/controllers/api/v1/admin/events_controller.rb — specifically the auto_complete_past action needs a transaction wrapper and organization scoping before this is safe to ship.

Important Files Changed

Filename Overview
api/app/controllers/api/v1/admin/events_controller.rb New auto_complete_past action has two P1 issues: unscoped Event.where(...) query operates across all organizations instead of scoping to org.events, and individual update! calls in a loop lack a transaction, risking partial updates on failure. Also performs N+1 updates instead of a single update_all.
api/config/routes.rb Route added correctly as a collection action under admin events — no issues.
web/src/pages/admin/EventsAdmin.tsx New button and handler are cleanly implemented; minor concern is an uncleared setTimeout that could call setSuccess on an unmounted component after navigation.
web/src/services/api.ts New autoCompletePastEvents API method is correctly typed and properly calls the new endpoint with POST and admin auth — no issues.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin UI (EventsAdmin.tsx)
    participant API as api.ts (autoCompletePastEvents)
    participant Controller as Admin::EventsController
    participant DB as Database (Event table)

    Admin->>API: POST /api/v1/admin/events/auto_complete_past
    API->>Controller: auto_complete_past (requires staff auth)
    Controller->>DB: SELECT * FROM events WHERE status IN ('upcoming','published')\nAND COALESCE(end_date, date) < today
    DB-->>Controller: candidate events
    loop For each candidate (N+1 updates)
        Controller->>DB: UPDATE events SET status='completed' WHERE id=?
    end
    DB-->>Controller: updated records
    Controller-->>API: { message, updated_count, updated_events }
    API-->>Admin: response
    Admin->>Admin: setSuccess(res.message)
    Admin->>Admin: await loadEvents() — reload table
    Admin->>Admin: setTimeout → clear success after 5s
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 91-107

Comment:
**Missing transaction — partial updates on failure**

The loop calls `event.update!` on each candidate individually with no wrapping transaction. If any update raises (e.g., a validation error on the 4th of 10 events), the first 3 events will already have been committed to `completed` while the remaining 7 are left unchanged. The endpoint will also return a 500 instead of a meaningful error, with no indication of which events were actually changed.

Wrap the block in a transaction so the whole operation succeeds or rolls back atomically:

```ruby
def auto_complete_past
  today = Date.current
  candidates = Event.where(status: %w[upcoming published])
    .where("COALESCE(end_date, date) < ?", today)

  updated = []
  Event.transaction do
    candidates.each do |event|
      event.update!(status: "completed")
      updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
    end
  end

  render json: {
    message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.",
    updated_count: updated.count,
    updated_events: updated
  }
end
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 93-94

Comment:
**Unscoped query — affects all organizations**

Every other action in this controller scopes its queries to a specific organization (e.g., `Organization.first``org.events`). This action queries `Event` directly across **all** organizations. If the app ever has more than one organization in the database (even for testing or staging), this will blindly complete events that belong to a different tenant.

Scope the query to the organization for consistency:

```ruby
org = Organization.first
return render json: { error: "No organization configured" }, status: :unprocessable_entity unless org

candidates = org.events.where(status: %w[upcoming published])
  .where("COALESCE(end_date, date) < ?", today)
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 96-100

Comment:
**N+1 updates — consider bulk update**

Issuing one `UPDATE` query per event can be slow if many events need completing. `update_all` sends a single SQL statement. You can fetch the metadata you need in a separate (much cheaper) query before applying the bulk update:

```ruby
updated = candidates.map { |e| { id: e.id, name: e.name, date: e.date, slug: e.slug } }
candidates.update_all(status: "completed")
```

This trades individual round-trips for two queries total (one `SELECT`, one bulk `UPDATE`), which is significantly faster at any meaningful scale.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: web/src/pages/admin/EventsAdmin.tsx
Line: 51-64

Comment:
**`setTimeout` not cleared on unmount**

`setTimeout(() => setSuccess(''), 5000)` fires after 5 seconds, but if the user navigates away before it runs, it will call `setSuccess` on an unmounted component. While React 18 doesn't throw for this, it's still a state update after unmount that can cause confusion. Consider storing the timeout ref and clearing it in a cleanup effect, or using a pattern that cancels the timer on unmount.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "feat: admin 'Mark Pa..."

Greptile also left 4 inline comments on this PR.

…ts from UI

Admins can now mark all past-dated events as completed with one click.
No more hardcoded migrations or rake tasks needed.

- POST /api/v1/admin/events/auto_complete_past (requires staff auth)
  - Finds all upcoming/published events where date < today
  - Updates status to 'completed', returns summary of changes
- Frontend: 'Mark Past as Completed' button in Events admin header
  - Shows spinner while running, displays success message with count
  - Reloads event table after completion

Addresses inbox request: admin side should handle event status updates
without hardcoded values.
@netlify

netlify Bot commented Mar 18, 2026

Copy link
Copy Markdown

Deploy Preview for marianas-open ready!

Name Link
🔨 Latest commit b7902be
🔍 Latest deploy log https://app.netlify.com/projects/marianas-open/deploys/69ba976eaf3b18000867fb52
😎 Deploy Preview https://deploy-preview-39--marianas-open.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 972e7d55-2f39-45e4-918c-1cfc0f0bfa2a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/admin-auto-complete-past-events
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Comment on lines +91 to +107
def auto_complete_past
today = Date.current
candidates = Event.where(status: %w[upcoming published])
.where("COALESCE(end_date, date) < ?", today)

updated = []
candidates.each do |event|
event.update!(status: "completed")
updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
end

render json: {
message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.",
updated_count: updated.count,
updated_events: updated
}
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing transaction — partial updates on failure

The loop calls event.update! on each candidate individually with no wrapping transaction. If any update raises (e.g., a validation error on the 4th of 10 events), the first 3 events will already have been committed to completed while the remaining 7 are left unchanged. The endpoint will also return a 500 instead of a meaningful error, with no indication of which events were actually changed.

Wrap the block in a transaction so the whole operation succeeds or rolls back atomically:

def auto_complete_past
  today = Date.current
  candidates = Event.where(status: %w[upcoming published])
    .where("COALESCE(end_date, date) < ?", today)

  updated = []
  Event.transaction do
    candidates.each do |event|
      event.update!(status: "completed")
      updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
    end
  end

  render json: {
    message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.",
    updated_count: updated.count,
    updated_events: updated
  }
end
Prompt To Fix With AI
This is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 91-107

Comment:
**Missing transaction — partial updates on failure**

The loop calls `event.update!` on each candidate individually with no wrapping transaction. If any update raises (e.g., a validation error on the 4th of 10 events), the first 3 events will already have been committed to `completed` while the remaining 7 are left unchanged. The endpoint will also return a 500 instead of a meaningful error, with no indication of which events were actually changed.

Wrap the block in a transaction so the whole operation succeeds or rolls back atomically:

```ruby
def auto_complete_past
  today = Date.current
  candidates = Event.where(status: %w[upcoming published])
    .where("COALESCE(end_date, date) < ?", today)

  updated = []
  Event.transaction do
    candidates.each do |event|
      event.update!(status: "completed")
      updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
    end
  end

  render json: {
    message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.",
    updated_count: updated.count,
    updated_events: updated
  }
end
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +93 to +94
candidates = Event.where(status: %w[upcoming published])
.where("COALESCE(end_date, date) < ?", today)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unscoped query — affects all organizations

Every other action in this controller scopes its queries to a specific organization (e.g., Organization.firstorg.events). This action queries Event directly across all organizations. If the app ever has more than one organization in the database (even for testing or staging), this will blindly complete events that belong to a different tenant.

Scope the query to the organization for consistency:

org = Organization.first
return render json: { error: "No organization configured" }, status: :unprocessable_entity unless org

candidates = org.events.where(status: %w[upcoming published])
  .where("COALESCE(end_date, date) < ?", today)
Prompt To Fix With AI
This is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 93-94

Comment:
**Unscoped query — affects all organizations**

Every other action in this controller scopes its queries to a specific organization (e.g., `Organization.first``org.events`). This action queries `Event` directly across **all** organizations. If the app ever has more than one organization in the database (even for testing or staging), this will blindly complete events that belong to a different tenant.

Scope the query to the organization for consistency:

```ruby
org = Organization.first
return render json: { error: "No organization configured" }, status: :unprocessable_entity unless org

candidates = org.events.where(status: %w[upcoming published])
  .where("COALESCE(end_date, date) < ?", today)
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +96 to +100
updated = []
candidates.each do |event|
event.update!(status: "completed")
updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 N+1 updates — consider bulk update

Issuing one UPDATE query per event can be slow if many events need completing. update_all sends a single SQL statement. You can fetch the metadata you need in a separate (much cheaper) query before applying the bulk update:

updated = candidates.map { |e| { id: e.id, name: e.name, date: e.date, slug: e.slug } }
candidates.update_all(status: "completed")

This trades individual round-trips for two queries total (one SELECT, one bulk UPDATE), which is significantly faster at any meaningful scale.

Prompt To Fix With AI
This is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 96-100

Comment:
**N+1 updates — consider bulk update**

Issuing one `UPDATE` query per event can be slow if many events need completing. `update_all` sends a single SQL statement. You can fetch the metadata you need in a separate (much cheaper) query before applying the bulk update:

```ruby
updated = candidates.map { |e| { id: e.id, name: e.name, date: e.date, slug: e.slug } }
candidates.update_all(status: "completed")
```

This trades individual round-trips for two queries total (one `SELECT`, one bulk `UPDATE`), which is significantly faster at any meaningful scale.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +51 to +64
const handleAutoCompletePast = async () => {
setAutoCompleting(true)
setError('')
try {
const res = await api.admin.autoCompletePastEvents()
setSuccess(res.message)
await loadEvents()
setTimeout(() => setSuccess(''), 5000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Auto-complete failed')
} finally {
setAutoCompleting(false)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 setTimeout not cleared on unmount

setTimeout(() => setSuccess(''), 5000) fires after 5 seconds, but if the user navigates away before it runs, it will call setSuccess on an unmounted component. While React 18 doesn't throw for this, it's still a state update after unmount that can cause confusion. Consider storing the timeout ref and clearing it in a cleanup effect, or using a pattern that cancels the timer on unmount.

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/pages/admin/EventsAdmin.tsx
Line: 51-64

Comment:
**`setTimeout` not cleared on unmount**

`setTimeout(() => setSuccess(''), 5000)` fires after 5 seconds, but if the user navigates away before it runs, it will call `setSuccess` on an unmounted component. While React 18 doesn't throw for this, it's still a state update after unmount that can cause confusion. Consider storing the timeout ref and clearing it in a cleanup effect, or using a pattern that cancels the timer on unmount.

How can I resolve this? If you propose a fix, please make it concise.

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.

1 participant