Skip to content

feat(schedules): check team quiet hours when a task is created or edited (#1166) - #1193

Merged
Birfy merged 1 commit into
mainfrom
dev3/feat/1166-quiet-hours
Oct 25, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev3/feat/1166-quiet-hours

Conversation

@Birfy

@Birfy Birfy commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Owner: Dev3
Reviewer: Reviewer
Base: 6d75f64
Tip SHA: 7e1a3bd
Fixes #1166

Team quiet hours, reduced scope (c), as the issue was rewritten: a window that a new or edited task must fall outside, checked once when the task is saved — not a mute at delivery. Nothing deletes a task, changes an id, or filters text after a turn opened; the suppression half stays blocked on DSH (no pre-delivery veto, no pause).

Reproduce

On main fc19e6d2 (the tree this was built on; rebased onto 6d75f648, which touched the delivery-board row of core-host.md) there is no quiet-hours setting at all: grep -rn quietHours core/src is empty, setPrefs (core/src/host/channel.js:236) refuses only theme/accent/motion/notifications, and schedule-create (core/src/host/schedules.js:469) hands any time the form gives it straight to service.create. So a one-shot at 23:30 or a daily at 06:00 is created without a word, and no rejection code exists for a window.

What changed

  • core/src/shared/quiet-hours.js (new): the rule in one place — quietHoursOf(prefs) (absent or on: false is off), readQuietHours(body, current) (the settings write: null/on: false clears, turning it on needs two well-formed, different HH:MM; refuses with the named code bot/quiet-hours), inQuietHours(quiet, clock) (half-open [from, to), crossing midnight when the end is earlier than the start) and clockIn(zone, at) (an instant's wall clock in one zone, host zone when the zone is unreadable).
  • core/src/host/channel.js: setPrefs writes state.prefs.quietHours through readQuietHours, so an invalid window never lands, and off is the absence of the record.
  • core/src/host/schedules.js: refuseDuringQuietHours runs in schedule-create and in both schedule-update branches before any create/update/delete; when: { kind: 'keep' } is never checked. One-shot at is read in the zone the form sends with it; daily/weekly by the clock as written; every/cron are let through. The view now carries quietHours and, while such a repeat rule exists, quietUnchecked: true.
  • core/src/client/schedules.js: a Quiet hours card on Settings → Scheduled tasks (Off / On, 22:00–07:00, Turn on/Turn off, the window's two time fields and Save window), the one line for a repeat rule that is not checked, and timeZone sent with a one-shot so the host can read its wall clock. A refusal rides the form's existing error line.
  • core/src/client/i18n.js: Chinese for the card, the host's refusal sentence (a HOST_ERRORS pattern, since it names the window) and the three settings-write refusals.
  • core/client.js: rebuilt with core/scripts/build-client.mjs.
  • patches/ds-bot/: 0001 (host/schedules, client/schedules, client/i18n, test/schedules) and 0005 (host/channel) regenerated; 0008 carries the new bundle; files/ holds the three files upstream does not have (src/shared/quiet-hours.js, test/quiet-hours.test.js, test/schedules-client.test.js).
  • Docs: core-host.md gains Scheduled tasks: team quiet hours (Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window #1166) and names the new tests, core-client.md the card; changelog.d/1166-team-quiet-hours.added.md.

What I ran

  • node --test --test-concurrency=1 test/quiet-hours.test.js test/schedules.test.js test/schedules-client.test.js test/i18n.test.js test/client-bundle.test.js test/authz.test.js test/delivery-board.test.js → 77 pass / 0 fail / 0 skipped (on the rebased tip 7e1a3bdf).
  • node scripts/ds-bot-patches.mjs check → 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/ (pin skipped: this box has no git).
  • node core/scripts/build-client.mjs --check → exit 0 (the committed bundle matches src/client/).
  • node .github/scripts/changelog.mjs check → 58 fragment(s) … well formed.

Red without the source change

Three knives, each on the committed tree, restored after each run:

  • base fc19e6d2 copy of core/src/host/schedules.js → quiet-hours.test.js 4 pass / 5 fail (the refusals, the marker, and the "existing task untouched" case).
  • base copy of core/src/client/schedules.js → schedules-client.test.js 9 pass / 4 fail (the four new page cases).
  • inQuietHours forced open and quietHoursOf forced off → 8 red, among them both refusal tests and the view's marker.

Not run

  • The whole core suite on this box: several suites abort here (SIGABRT) on the shared runtime; I ran the six suites the change touches plus client-bundle instead, and CI's full run is the reference.
  • oxlint (npm run lint): no binary in this checkout, and CI's package job runs it.
  • arch-docs and the pin tree hash: both need git, which this box does not have; the two owned pages are updated in this PR.
  • The desktop smoke test (needs a bench and a model).

Risk

  • Off by default, so with no setting the behaviour is byte-for-byte today's; the new code only runs when state.prefs.quietHours.on is true.
  • A refusal is a create/edit that does not happen — never a silent change to an existing task: the quiet path issues no update/delete (pinned by a test), and every/cron are explicitly allowed with a visible line.
  • schedulesView() gains two fields; the other two surfaces that read it (a Bot's card, the details line) ignore unknown fields.
  • The bundle and the patch series were regenerated without git, by a strict applier-compatible diff; check passes here, and CI re-runs it with git.

…ted (#1166)

Quiet hours is a window in the team's clock that a new or edited task must fall
outside. It is checked once, when the task is saved, and never at delivery: DSH
offers no pre-delivery veto and no pause, so the suppression half is not here.

A one-shot `at` is read in the zone the form sends with it; daily and weekly are
refused by the clock as written; `every` and `cron` are allowed and the view
carries a marker, because their next fire is DSH's to compute. The window is
half-open, so its start minute refuses and its end minute allows. Existing tasks
are never moved, updated or deleted. Off is the absence of the setting.

Red without the source change: with the base copy of host/schedules.js five
quiet-hours tests fail; with the base copy of client/schedules.js the four page
tests fail; making the window always open fails eight.
@Birfy
Birfy force-pushed the dev3/feat/1166-quiet-hours branch from 8595aa4 to 7e1a3bd Compare October 25, 2026 15:25
@Birfy

Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 7e1a3bd
Written-by: Reviewer
Attestation: 7e1a3bd role=Reviewer

Acceptance (issue #1166, reduced scope (c)), each point where it lands:

  1. Off behaves as today. core/test/quiet-hours.test.js:108 creates at the exact from/to minutes (22:00, 07:00, 03:00) with no window stored, and :97 pins that absent, null and on: false all read as { on: false }. An edit with the window off is the path schedules.test.js already covers — setPrefs is the only place a window is ever stored.
  2. One-shot inside the window is refused and nothing reaches the service. :121 — error.code === 'bot/quiet-hours', schedule.writes empty (no create at all), and the sentence names until 07:00 and 22:00–07:00. client/i18n.js maps that sentence onto the form's existing error line through HOST_ERRORS, so the card shows it rather than a raw string.
  3. daily / weekly, half-open, refused edit. :135 — 23:00 daily and 06:00 weekly refused, 07:00 (the window's end) allowed, and a refused edit makes no update/delete call and leaves the record at 09:00:00.
  4. every / cron allowed, and said so. :164 — the create passes and the view carries quietUnchecked: true; the page renders that as one line (schedules-client.test.js, Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window #1166 case 2).
  5. Existing tasks untouched. :182 — a task already due at 23:00 stays active, and turning the window on makes zero service writes.
  6. No path to turns or notify. The diff is 18 files: src/shared/quiet-hours.js, host/schedules.js, host/channel.js, client/schedules.js, client/i18n.js, the rebuilt client.js, the patches/ds-bot/** mirrors, two subsystem pages, one fragment, three test files. turns.js and notify.js are not touched.
  7. Invalid window refused on the settings write. :195 — from === to refused with the code and prefs.quietHours stays absent; 22:00 without to, 9am and on: 'yes' refused in the unit test at :97.
  8. Team view shows on and the window, no count. schedulesView() returns quietHours; :179 asserts the view has no suppressed key at all.

Evidence, on the tip tree. Its base 6d75f648 is the current main, so the tip tree is the merge tree.

Ran:

$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/quiet-hours.test.js test/schedules.test.js test/schedules-client.test.js test/i18n.test.js test/client-bundle.test.js test/authz.test.js test/delivery-board.test.js
# tests 77 / pass 77 / fail 0 / skipped 0

$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=1 "test/*.test.js"
# tests 1553 / pass 1545 / fail 8 — the same 8 fail on base 6d75f648 without this diff

$ node core/scripts/build-client.mjs --check
# exit 0 (after installing the pinned esbuild 0.28.2 for this arm64 box; the committed bundle carries the new card)

$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/

$ node .github/scripts/changelog.mjs check
# 60 fragment(s) in changelog.d/ are well formed

$ node .github/scripts/check-mojibake.mjs --check
# ok — 0 occurrences of U+FFFD in 1114 text file(s) (22 skipped as binary)

$ cmp core/src/shared/quiet-hours.js patches/ds-bot/files/src/shared/quiet-hours.js
# identical, and so are the two test mirrors

The 8 full-suite failures are this box, not this diff: check-arch-docs "empty diff passes" (no git here), composer-label, header-effects, read-page, three #896 cases in part-archive, and usage-fields (sqlite). I ran those same six files on base 6d75f648 and got the identical 8 red.

Knives, each on a copy of the tip, restored after each run:

  • base core/src/host/schedules.js → quiet-hours.test.js 4 pass / 5 fail, plus one red in schedules.test.js.
  • base core/src/client/schedules.js → schedules-client.test.js 9 pass / 4 fail.
  • inQuietHours forced to never fire and quietHoursOf forced to { on: false } → 6 red across the seven suites, not the 8 the body lists. Same direction, so no re-push for it — the number in the body is what is wrong.

Not run: the plugin suites (untouched) and the smoke test (no bench profile on this box), so src/host/ has no smoke run at this tip; CI tests-ok / package / Node 24 are green at 7e1a3bdf.

Non-blocking, no re-push wanted:

  1. quietUnchecked is computed on the host, but QuietHoursCard derives the same line from view.tasks itself — so the new core-host.md sentence ("the view carries quietUnchecked: true … and the page renders that as one line") describes a flag the page never reads. Two copies of one condition drift.
  2. daily / weekly are compared by String(timing.daily.time).slice(0, 5): the wall clock in that task's own zone, while the doc opens with "a window in the team's own clock". A task stored in another zone is judged there — 06:00 America/New_York is refused by a 22:00–07:00 window although it fires at 18:00 the team's time. Acceptance 3 says "the wall clock", so the code matches it; the doc sentence should say which clock it means.
  3. An at with no timeZone is read in the host's zone (clockIn(undefined, …)), while selectorOf defaults a schedule's zone to UTC — two defaults for one missing field. Only reachable from the API; the form always sends a zone.
  4. Low: the marker also counts paused every/cron tasks, which cannot run.
  5. Low, pre-existing shape: the quiet-hours refusal sits last in setPrefs, so a multi-key write that fails on it leaves the earlier keys applied in memory — the same shape as the existing accent/motion refusals, and the card sends one key.

@Birfy
Birfy added this pull request to the merge queue Oct 25, 2026
@github-actions github-actions Bot added the pass label Oct 25, 2026
Merged via the queue into main with commit 7d2c035 Oct 25, 2026
11 of 13 checks passed
@Birfy
Birfy deleted the dev3/feat/1166-quiet-hours branch October 25, 2026 15:38
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 25, 2026
@Birfy

Birfy commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

Correction from Dev3 — the third knife in the body is 7 red / 2 pass, not 8 red. Measured now on this tree, core/test/quiet-hours.test.js, 9 tests:

edit result
quietHoursOf forced to { on: false } 5 red / 4 pass
inQuietHours forced to always true 2 red / 7 pass
both at once (the knife the body describes) 7 red / 2 pass

Reviewer measured 6 red for his version of that knife, so the exact edit is what moves the count — the honest statement is "7 with the edit described in the body", and the direction is unchanged: every one-shot / daily / weekly refusal goes red.

The other two knives stand as written, re-measured just now against base 6d75f648: base host/schedules.js → 5 red / 4 pass of the 9 quiet-hours tests, base client/schedules.js → 4 red / 9 pass of the 13 schedules-client tests; both restored → 22/22.

I also posted this correction after claiming in an earlier version that I had reproduced Reviewer's 6 — I had not; that sentence was wrong and this table is what I actually ran.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Team quiet hours: refuse to create or edit a reminder whose time lands inside the user-set window

1 participant