Publishing quiet hours with timezone support #129

Open
opened 2026-08-10 21:55:47 +02:00 by myrmidex · 0 comments
Owner

Summary

Suppress automatic publishing during configured hours, interpreted in a user-set timezone.

Split out of #90 "Publishing schedule control", which bundled four independent features.

Why timezone is part of this ticket

config/app.php sets 'timezone' => 'UTC'. Quiet hours of 08:00–22:00 interpreted in UTC are not the hours a user in Brussels means. Quiet hours is unshippable without a timezone setting, so the two cannot be separate tickets — timezone is a prerequisite, not a sibling feature.

Settings

Key Default Notes
publishing_timezone UTC validated against DateTimeZone::listIdentifiers()
quiet_hours_enabled false off = current behaviour
quiet_hours_start 22:00 HH:MM
quiet_hours_end 08:00 HH:MM

Defaulting to disabled matters: an existing install must publish exactly as it does today until someone opts in.

Design

  • A PublishingWindow value object holding the quiet-hours logic, unit-testable without touching the job.
  • Gate in PublishNextArticleJob before article selection — quiet hours are article-independent, so there's no reason to run the candidate query.
  • Midnight crossover: when start > end the window wraps, so the check is now >= start || now < end. Otherwise now >= start && now < end.
  • Use Carbon with the configured timezone throughout. No arithmetic on naive times.

Tasks

  • publishing_timezone setting + validation + Setting accessors
  • quiet_hours_enabled / quiet_hours_start / quiet_hours_end settings
  • PublishingWindow value object
  • Gate in PublishNextArticleJob
  • Settings UI (dark mode from the start — see #88)
  • API surface in SettingsController
  • Tests

Edge cases that must be tested

  • Window not crossing midnight (08:00–22:00)
  • Window crossing midnight (22:00–08:00)
  • start == end
  • Exactly on the boundary minute (inclusive start, exclusive end)
  • A non-UTC timezone where local date and UTC date differ
  • DST spring-forward — a wall-clock hour that does not exist
  • DST autumn-back — a wall-clock hour that occurs twice
  • Disabled = current behaviour exactly (regression guard)

Constraints

PublishNextArticleJob is ShouldBeUnique with uniqueFor = 300 on a five-minute schedule. Confirm an early return releases the unique lock rather than suppressing the following tick, before adding a new early-return path.

Do not weaken #123 (Cache::lock duplicate guard) or #119 (scopeDueForPublishing is the single source of truth for publish eligibility — do not re-express that threshold in a new query).

## Summary Suppress automatic publishing during configured hours, interpreted in a user-set timezone. Split out of #90 "Publishing schedule control", which bundled four independent features. ## Why timezone is part of this ticket `config/app.php` sets `'timezone' => 'UTC'`. Quiet hours of 08:00–22:00 interpreted in UTC are not the hours a user in Brussels means. Quiet hours is **unshippable without** a timezone setting, so the two cannot be separate tickets — timezone is a prerequisite, not a sibling feature. ## Settings | Key | Default | Notes | |---|---|---| | `publishing_timezone` | `UTC` | validated against `DateTimeZone::listIdentifiers()` | | `quiet_hours_enabled` | `false` | off = current behaviour | | `quiet_hours_start` | `22:00` | `HH:MM` | | `quiet_hours_end` | `08:00` | `HH:MM` | Defaulting to disabled matters: an existing install must publish exactly as it does today until someone opts in. ## Design - A `PublishingWindow` value object holding the quiet-hours logic, unit-testable without touching the job. - Gate in `PublishNextArticleJob` **before** article selection — quiet hours are article-independent, so there's no reason to run the candidate query. - Midnight crossover: when `start > end` the window wraps, so the check is `now >= start || now < end`. Otherwise `now >= start && now < end`. - Use Carbon with the configured timezone throughout. No arithmetic on naive times. ## Tasks - [ ] `publishing_timezone` setting + validation + `Setting` accessors - [ ] `quiet_hours_enabled` / `quiet_hours_start` / `quiet_hours_end` settings - [ ] `PublishingWindow` value object - [ ] Gate in `PublishNextArticleJob` - [ ] Settings UI (dark mode from the start — see #88) - [ ] API surface in `SettingsController` - [ ] Tests ## Edge cases that must be tested - Window not crossing midnight (08:00–22:00) - Window crossing midnight (22:00–08:00) - `start == end` - Exactly on the boundary minute (inclusive start, exclusive end) - A non-UTC timezone where local date and UTC date differ - DST spring-forward — a wall-clock hour that does not exist - DST autumn-back — a wall-clock hour that occurs twice - Disabled = current behaviour exactly (regression guard) ## Constraints `PublishNextArticleJob` is `ShouldBeUnique` with `uniqueFor = 300` on a five-minute schedule. Confirm an early return releases the unique lock rather than suppressing the following tick, before adding a new early-return path. Do not weaken #123 (`Cache::lock` duplicate guard) or #119 (`scopeDueForPublishing` is the single source of truth for publish eligibility — do not re-express that threshold in a new query).
myrmidex added this to the v2.0.0 milestone 2026-08-10 21:55:47 +02:00
myrmidex added the
enhancement
label 2026-08-10 21:55:47 +02:00
myrmidex removed this from the v2.0.0 milestone 2026-08-15 12:56:12 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/fedi-feed-router#129
No description provided.