Hide intervals for events you didn't do#1330
Conversation
Greptile SummaryThis PR hides event intervals (High Seas, Scrapyard, etc.) from the interval picker for users who weren't active during those events. It introduces a bitmask column (
Confidence Score: 3/5Not safe to merge without fixing the method name typo in heartbeat_ingest.rb — it will crash on every heartbeat processed during an active event window. The app/services/heartbeat_ingest.rb has the method name bug; db/migrate/20260521132654_add_event_participation_backfilled_to_users.rb highlights the missing backfill story. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as Browser (Svelte)
participant Controller as InertiaController
participant User as User Model
participant Ingest as HeartbeatIngest
participant DB as Database
Client->>Controller: Page load (GET /)
Controller->>User: current_user.event_participation
User->>DB: SELECT event_participation, event_participation_backfilled FROM users
DB-->>User: bitmap integer + backfilled flag
Controller-->>Client: "{ created_at, event_participation: string[] | null }"
Note over Client: visibleIntervals derived:<br/>ended event + backfilled → participation.has(key)<br/>active/future or not backfilled → created_at ≤ ends_at
Client->>Controller: POST /heartbeats (heartbeat data)
Controller->>Ingest: HeartbeatIngest.call(...)
Ingest->>DB: INSERT heartbeat (ignore duplicate)
Ingest->>Ingest: record_event_participation([time])
loop each EVENT_RANGES entry
Ingest->>User: event_participation.set?(key) [in-memory]
alt bit not set AND time in range
Ingest->>DB: "UPDATE users SET event_participation = event_participation | bit"
Ingest->>User: event_participation.set(key) [in-memory sync]
end
end
|
| to_i = range.end.to_i | ||
| next unless times.any? { |t| t >= from_i && t <= to_i } | ||
|
|
||
| User.where(id: @user.id).event_participations.set_all!(key) |
There was a problem hiding this comment.
Wrong method name —
NoMethodError at runtime
active_flag exposes a class/relation method named after the column verbatim: event_participation (singular). Calling event_participations (plural) on the ActiveRecord::Relation will raise NoMethodError: undefined method 'event_participations' on every heartbeat ingested during an active event window, silently failing to record participation. The correct call is User.where(id: @user.id).event_participation.set_all!(key).
Per the gem's README: Profile.languages.set_all!(:chinese) — the receiver matches the flag column name exactly.
| User.where(id: @user.id).event_participations.set_all!(key) | |
| User.where(id: @user.id).event_participation.set_all!(key) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/services/heartbeat_ingest.rb
Line: 221
Comment:
**Wrong method name — `NoMethodError` at runtime**
`active_flag` exposes a class/relation method named after the column verbatim: `event_participation` (singular). Calling `event_participations` (plural) on the `ActiveRecord::Relation` will raise `NoMethodError: undefined method 'event_participations'` on every heartbeat ingested during an active event window, silently failing to record participation. The correct call is `User.where(id: @user.id).event_participation.set_all!(key)`.
Per the gem's README: `Profile.languages.set_all!(:chinese)` — the receiver matches the flag column name exactly.
```suggestion
User.where(id: @user.id).event_participation.set_all!(key)
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Pull request overview
This PR aims to hide event-based date intervals in the dashboard interval picker when the current user did not participate in (or did not exist for) those events. It introduces a shared events configuration and tracks per-user event participation so the UI can filter event intervals more accurately.
Changes:
- Added
config/events.jsonand refactored server-side time-range definitions to load event ranges from that config. - Added an
event_participationbitmask (viaactive_flag) plus anevent_participation_backfilledflag, and updated heartbeat ingestion to set participation bits when new heartbeats land. - Updated the signed-in interval picker to build event intervals from
events.jsonand hide ended events based on participation/backfill status.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
vite.config.ts |
Attempts to enable TS path alias resolution for Vite. |
tsconfig.json |
Adds $config/* path mapping for importing config JSON from TS/Svelte. |
Gemfile / Gemfile.lock |
Adds active_flag gem for bitmask flags on User. |
db/schema.rb |
Updates schema version and includes DB changes (currently includes large unrelated diffs). |
db/migrate/20260521131313_add_event_participation_to_users.rb |
Adds users.event_participation integer column. |
db/migrate/20260521132654_add_event_participation_backfilled_to_users.rb |
Adds users.event_participation_backfilled with “existing false, new true” default behavior. |
config/events.json |
Introduces centralized event definitions (dates/timezone/human name). |
app/services/heartbeat_ingest.rb |
Sets event participation bits when direct/import heartbeats are persisted. |
app/models/user.rb |
Declares the event_participation flag on User. |
app/models/concerns/time_range_filterable.rb |
Loads event ranges/keys from config/events.json and merges into available ranges. |
app/javascript/types/index.ts |
Adds created_at and event_participation to the typed current user payload. |
app/javascript/pages/Home/signedIn/IntervalSelect.svelte |
Builds event intervals dynamically and filters visible event intervals based on user participation/existence. |
app/controllers/inertia_controller.rb |
Exposes created_at and event_participation (or nil pre-backfill) to the frontend. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # mahad says: NEVER remove entries from the events JSON | ||
| # if you need to get rid of an event, add a retired flag or something | ||
| EVENT_KEYS = EVENT_DEFINITIONS.keys.sort.map(&:to_sym).freeze |
| const endsAt = Date.parse(range.ends_at); | ||
| // Ended event + backfilled: show only if the user actually participated. | ||
| // Otherwise (active/future event, or not-yet-backfilled user) fall back | ||
| // to the cheap "did the user exist before the event ended" check. | ||
| if (endsAt < Date.now() && participated) { | ||
| return participated.has(interval.key); | ||
| } | ||
| return userCreatedAt <= endsAt; |
| create_table "github_app_installations", force: :cascade do |t| | ||
| t.bigint "account_github_id" | ||
| t.string "account_login", null: false | ||
| t.string "account_type", null: false | ||
| t.datetime "created_at", null: false | ||
| t.bigint "installation_id", null: false | ||
| t.datetime "suspended_at" | ||
| t.datetime "updated_at", null: false | ||
| t.bigint "user_id" | ||
| t.index ["account_github_id"], name: "index_github_app_installations_on_account_github_id" | ||
| t.index ["installation_id"], name: "index_github_app_installations_on_installation_id", unique: true | ||
| t.index ["user_id"], name: "index_github_app_installations_on_user_id" | ||
| end | ||
|
|
||
| create_table "github_app_repositories", force: :cascade do |t| | ||
| t.boolean "archived", default: false, null: false | ||
| t.datetime "created_at", null: false | ||
| t.string "full_name", null: false | ||
| t.bigint "github_app_installation_id", null: false | ||
| t.bigint "github_repo_id", null: false | ||
| t.string "html_url", null: false | ||
| t.boolean "private", default: false, null: false | ||
| t.bigint "repository_id" | ||
| t.datetime "updated_at", null: false | ||
| t.index ["full_name"], name: "index_github_app_repositories_on_full_name" | ||
| t.index ["github_app_installation_id"], name: "index_github_app_repositories_on_github_app_installation_id" | ||
| t.index ["github_repo_id"], name: "index_github_app_repositories_on_github_repo_id", unique: true | ||
| t.index ["repository_id"], name: "index_github_app_repositories_on_repository_id" | ||
| end |
Summary of the problem
The interval picker would show events like Scrapyard and High Seas when you didn't even have an account then.
Describe your changes
Hide events you weren't around for.
Screenshots / Media
N/A