Store mobile push working hours on the user preference

What does this MR do and why?

Stores a user's mobile push working hours on UserPreference, in one new jsonb column user_preferences.mobile_push_working_hours (DEFAULT '{}' NOT NULL, the same shape as the existing orbit_settings column on this table).

  • jsonb_accessor exposes the switch, the start and end in minutes after midnight, the weekday names and the IANA zone under the attribute names the API will use, stored under short keys.
  • A JSON schema (user_preference_mobile_push_working_hours, size_limit: 1.kilobyte) checks types, the minute range, the weekday names without duplicates, and the zone length. Model validations check that the zone is one Rails knows and, only while the switch is on, that the schedule is complete and the start differs from the end.
  • The validations are inert while the switch is off: every existing row holds {}, which passes the schema, so no other preference save gains a new way to fail.

Nothing reads the preference yet. The API fields and the push checks follow in the MRs above this one.

This is the only MR in the stack with a migration.

Documentation

No documentation change here on purpose: this MR adds storage only and exposes nothing to users. The feature becomes user-facing in MR D, Add the mobile push working hours to the user preferences API, which carries the documentation (doc/api/users.md, doc/api/mobile_push_subscriptions.md, and the OpenAPI spec).

References

Split from !260440 (closed), as agreed with the reviewer there: seven merge requests of 200 to 300 lines each, cut where the code does not reference the other parts. Each one targets its parent branch and is retargeted to master when the parent merges.

Merge request Targets
A Extract the mobile push recipient checks and the APNs fan-out master
B1 Add a value object for mobile push working hours A
B2 (this MR) Store mobile push working hours on the user preference B1
C1 Hold mobile pushes for to-dos outside working hours B2
C2a Add the held summary that resumes one user's mobile pushes C1
C2b Send the mobile push summaries when working hours begin C2a
D Add the mobile push working hours to the user preferences API C2b

Merge order is the chain order: A, B1, B2, C1, C2a, C2b, then D. D is the switch: it targets C2b, so the API cannot reach master before the server can hold a push and send its summary. Any other order would open a window on GitLab.com where users who enabled the schedule have their pushes held but not summarized until C2b deploys.

Screenshots or screen recordings

No UI changes.

How to set up and validate locally

In a Rails console:

user = User.find_by!(username: 'root')
user.user_preference.update!(
  mobile_push_working_hours_enabled: true,
  mobile_push_working_hours_start: 10 * 60,   # 10:00
  mobile_push_working_hours_end: 17 * 60,     # 17:00
  mobile_push_working_days: %w[monday tuesday wednesday thursday friday],
  mobile_push_working_hours_time_zone: 'Europe/Berlin'
)

Then user.user_preference.mobile_push_working_hours shows the stored JSON under the short keys enabled, start, end, days and time_zone. Setting mobile_push_working_hours_time_zone: 'Mars/Olympus' fails with "is not a valid time zone", and mobile_push_working_hours_enabled: true without a start fails with "is required".

Database

The migration adds one jsonb column to user_preferences (table_size: small) with DEFAULT '{}' NOT NULL. No index, no constraint, no data migration. On a Database Lab clone of production the ALTER TABLE ... ADD COLUMN took 42.4 ms. The db:gitlabcom-database-testing job for this MR ran the migration on GitLab.com data: 3.7 s total, ALTER TABLE ... ADD COLUMN 24.6 ms, +0 B (results).

The only new write is the save through the existing UserPreferences::UpdateService. Measured with EXPLAIN ANALYZE on a production clone (postgres.ai, gitlab-production-main) for the original merge request; the statements are unchanged. The id lists are synthetic.

  1. Saving the settings through the existing UserPreferences::UpdateService: UPDATE "user_preferences" SET "mobile_push_working_hours" = $1, "updated_at" = $2 WHERE "user_preferences"."id" = $3 over the primary key, run on the clone after adding the column: 26.460 ms, shared buffers 27 hit / 25 read.

    Plan
 Update on public.user_preferences  (cost=0.44..3.46 rows=0 width=0) (actual time=25.581..25.581 rows=0 loops=1)
   Buffers: shared hit=27 read=25 dirtied=9
   WAL: records=8 fpi=8 bytes=61590
   I/O Timings: read=25.010 write=0.000
   ->  Index Scan using user_preferences_pkey on public.user_preferences  (cost=0.44..3.46 rows=1 width=46) (actual time=4.114..4.117 rows=1 loops=1)
         Index Cond: (user_preferences.id = 12345)
         Buffers: shared read=4
         I/O Timings: read=4.060 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -1220253474535494047

Time: 26.460 ms
  - planning: 0.805 ms
  - execution: 25.655 ms
    - I/O read: 25.010 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 27 (~216.00 KiB) from the buffer pool
  - reads: 25 (~200.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 9 (~72.00 KiB)
  - writes: 0

MR acceptance checklist

Evaluated against the MR acceptance checklist.

🤖 Generated with Claude Code

Edited by Marcel van Remmerden

Merge request reports

Loading
Loading