Skip to content

Hide intervals for events you didn't do#1330

Open
skyfallwastaken wants to merge 7 commits into
mainfrom
interval-event-hide
Open

Hide intervals for events you didn't do#1330
skyfallwastaken wants to merge 7 commits into
mainfrom
interval-event-hide

Conversation

@skyfallwastaken

Copy link
Copy Markdown
Member

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

Copilot AI review requested due to automatic review settings May 21, 2026 13:58
@greptile-apps

greptile-apps Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 event_participation column on users, populates it via a backfill service and a rake task, and updates HeartbeatIngest to record participation on every new heartbeat going forward.

  • Backend: Extracts event definitions into config/events.json with explicit bit assignments; TimeRangeFilterable validates contiguous bits at boot and builds EVENT_KEYS/EVENT_RANGES; active_flag stores the bitmask on the user; HeartbeatIngest atomically ORs participation bits for newly inserted heartbeats only.
  • Frontend: IntervalSelectBody.svelte derives visibleIntervals by filtering on a participated set (from the shared Inertia props) or falling back to a created_at ≤ event_ends_at heuristic for unbackfilled users and active/future events; custom zonedTimeToUtcMs handles timezone-aware end-time comparisons.
  • Tests: Two new service tests cover the backfill and the direct/import heartbeat ingestion paths.

Confidence Score: 5/5

Safe to merge; the backfill rake task must be run post-deploy to give existing users accurate event visibility.

The bitmask design is solid: explicit bit fields in events.json prevent ordering surprises, the boot-time contiguity check catches misconfigurations early, and the atomic SQL OR in heartbeat ingest avoids clobbering other bits. The migration correctly seeds existing users as unbackfilled and new users as already current. The frontend gracefully falls back to the created_at heuristic until the backfill runs. Tests cover the main service paths.

No files require special attention beyond the noted non-reactive constants in IntervalSelectBody.svelte.

Important Files Changed

Filename Overview
app/javascript/pages/Home/signedIn/IntervalSelectBody.svelte Core UI filtering logic — adds event-range parsing and visibleIntervals derived store; participated/userCreatedAt initialized as non-reactive consts which won't re-derive on Inertia prop updates.
app/services/event_participation_backfill.rb New service for backfilling historical participation; manually recomputes bitmask with 1 << index rather than the gem's to_i helper, creating two parallel bit-computation paths.
app/services/heartbeat_ingest.rb Adds record_event_participation using insert_all returning: to track only newly inserted heartbeats; correctly uses active_flag's to_i API and an atomic SQL OR to avoid clobbering other bits.
app/models/concerns/time_range_filterable.rb Extracts event definitions from config/events.json at boot; validates contiguous bit ordering and builds EVENT_KEYS/EVENT_RANGES constants used by both the flag definition and the backfill service.
config/events.json Canonical event registry with explicit bit fields (0–5); all_day: false only on Stardance for time-level precision; bit assignments consistent with active_flag array ordering.
db/migrate/20260521132654_add_event_participation_backfilled_to_users.rb Adds event_participation_backfilled boolean; correctly seeds existing users to false and changes the column default to true so new accounts are automatically considered current.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (4): Last reviewed commit: "Address event participation review nits" | Re-trigger Greptile

Comment thread app/services/heartbeat_ingest.rb Outdated
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)

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.

P1 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.

Suggested change
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.

Copilot AI 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.

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.json and refactored server-side time-range definitions to load event ranges from that config.
  • Added an event_participation bitmask (via active_flag) plus an event_participation_backfilled flag, and updated heartbeat ingestion to set participation bits when new heartbeats land.
  • Updated the signed-in interval picker to build event intervals from events.json and 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.

Comment thread vite.config.ts Outdated
Comment on lines +43 to +45
# 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
Comment on lines +59 to +66
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;
Comment thread db/schema.rb Outdated
Comment on lines +156 to +184
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
@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptileai review

@socket-security

socket-security Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgem/​active_flag@​2.1.1100100100100100

View full report

# Conflicts:
#	Gemfile
#	app/javascript/pages/Home/signedIn/IntervalSelect.svelte
#	app/models/concerns/time_range_filterable.rb
#	app/services/heartbeat_ingest.rb
#	db/schema.rb
@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptileai review

@skyfallwastaken

Copy link
Copy Markdown
Member Author

@greptileai review

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