Hide intervals for events you didn't do#1330
Conversation
Greptile SummaryThis PR hides event intervals (High Seas, Scrapyard, etc.) in the interval picker for users who weren't active during those events. It introduces a bitmask
Confidence Score: 5/5Safe to merge; the backfill rake task must be run post-deploy to give existing users accurate event visibility. The bitmask design is solid: explicit No files require special attention beyond the noted non-reactive constants in Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User (Browser)
participant IC as InertiaController
participant HI as HeartbeatIngest
participant DB as Database
participant BF as EventParticipationBackfill
Note over DB: Migration: add event_participation (int),<br/>event_participation_backfilled (bool, default true)
BF->>DB: find_each(backfilled: false)
DB-->>BF: old users
BF->>DB: check heartbeats in each event range
BF->>DB: update_columns(event_participation: mask, backfilled: true)
U->>HI: send heartbeat
HI->>DB: insert_all(returning: ["time"])
DB-->>HI: inserted rows
HI->>HI: record_event_participation(times)
HI->>DB: "UPDATE ... event_participation = COALESCE(...) | mask"
U->>IC: page load (GET /)
IC->>DB: current_user
DB-->>IC: user (with event_participation bitmap)
IC-->>U: "Inertia props: event_participation[] | null, created_at"
U->>U: visibleIntervals filter
Note over U: participated set OR created_at heuristic
Reviews (4): Last reviewed commit: "Address event participation review nits" | Re-trigger Greptile |
| 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 |
|
@greptileai review |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
# Conflicts: # Gemfile # app/javascript/pages/Home/signedIn/IntervalSelect.svelte # app/models/concerns/time_range_filterable.rb # app/services/heartbeat_ingest.rb # db/schema.rb
832548d to
972ca8e
Compare
|
@greptileai review |
|
@greptileai review |
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