Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2bcbbf0
feat(shell): URL-held right sheet, foldable sidebar groups, on-call pill
Makisuo Nov 9, 2026
5535bc2
fix(shell): sheet history, on-call freshness, foldable active group
Makisuo Nov 9, 2026
0c0b786
Merge remote-tracking branch 'origin/main' into ux/shell
Makisuo Nov 9, 2026
18c83a3
feat(alerts): sections at their own paths, sidebar sub-items, silence…
Makisuo Nov 9, 2026
5888395
fix(shell): read window and document on subscribe, not on import
Makisuo Nov 9, 2026
f649d8f
Merge remote-tracking branch 'origin/ux/shell' into ux/alerting-pages
Makisuo Nov 9, 2026
280ea47
fix(alerts): review follow-ups for section routes and the silence panel
Makisuo Nov 9, 2026
5068806
feat(alerts): live coverage preview in the schedule editor and detail…
Makisuo Nov 9, 2026
5fd8fbc
feat(alerts): oncall lab states for a covered and a gappy schedule draft
Makisuo Nov 9, 2026
56b1631
fix(alerts): preview beside the form from 2xl, cap the gap links at six
Makisuo Nov 9, 2026
ef30170
test(shell): lab on-call fixture no longer expires on 16 Nov 2026
Makisuo Nov 9, 2026
7b09065
feat(ui): shift timeline gap tone, labels that leave when they cannot…
Makisuo Nov 10, 2026
b551d14
fix(alerts): coverage preview review follow-ups
Makisuo Nov 10, 2026
f9d010f
fix(views): side panel dims the page lightly, and comes in a compact …
Makisuo Nov 10, 2026
f1179ea
fix(shell): one-row on-call pill, calmer sidebar states
Makisuo Nov 10, 2026
3c3e996
fix(alerts): one primary action per section, chips and viewer time in…
Makisuo Nov 10, 2026
21974a5
Merge remote-tracking branch 'origin/main' into ux/shell
Makisuo Nov 10, 2026
1c0bfcd
Merge remote-tracking branch 'origin/ux/shell' into ux/alerting-pages
Makisuo Nov 10, 2026
8c9f5c6
Merge remote-tracking branch 'origin/ux/alerting-pages' into ux/sched…
Makisuo Nov 10, 2026
7e5000c
fix(alerts): layer a saved schedule's overrides into the editor preview
Makisuo Nov 10, 2026
d17a22e
Merge remote-tracking branch 'origin/main' into ux/shell
Makisuo Nov 10, 2026
b4fe115
Merge branch 'ux/shell' into ux/alerting-pages
Makisuo Nov 10, 2026
9974496
Merge branch 'ux/alerting-pages' into ux/schedule-preview
Makisuo Nov 10, 2026
cd3b2c5
Merge branch 'ux/shell' into ux/alerting-pages
Makisuo Nov 10, 2026
43c444a
Merge remote-tracking branch 'origin/main' into ux/shell
Makisuo Nov 10, 2026
1abff55
Merge branch 'ux/alerting-pages' into ux/schedule-preview
Makisuo Nov 10, 2026
78ebb4a
fix(alerts): silence form says what is missing instead of a silent di…
Makisuo Nov 10, 2026
5afb72a
fix(alerts): silence chips split on commas and keep a chip typed twice
Makisuo Nov 10, 2026
a037e66
fix(alerts): silence chips hint sits in the form, not in a popup over…
Makisuo Nov 10, 2026
2b33c5e
fix(shell): re-check a stale 'no schedules' answer so a teammate's fi…
Makisuo Nov 10, 2026
82145ab
fix(shell): only the current page's link carries aria-current in the …
Makisuo Nov 10, 2026
f675406
fix(alerts): silence save button names its end on the 24h clock the p…
Makisuo Nov 10, 2026
d599b1d
fix(ui): make the sidebar row hover visible in the light theme
Makisuo Nov 10, 2026
717a12f
fix(alerts): first-run Inbox and Rules leave the create actions to th…
Makisuo Nov 10, 2026
994eb5a
fix(alerts): hub replaces away a ?sheet= that opens nothing
Makisuo Nov 10, 2026
9a1429a
fix(alerts): long schedule names truncate in the shifts heading and w…
Makisuo Nov 10, 2026
a8edc84
fix(alerts): coverage summary names the period it counts, not the nex…
Makisuo Nov 10, 2026
fc1ac22
test(e2e): a saved rule returns to the Rules section
Makisuo Nov 10, 2026
5f2c136
Merge remote-tracking branch 'origin/main' into ux/shell
Makisuo Nov 10, 2026
c51ad1b
Merge branch 'ux/shell' into ux/alerting-pages
Makisuo Nov 10, 2026
ed1e3ca
Merge branch 'ux/alerting-pages' into ux/schedule-preview
Makisuo Nov 10, 2026
c5adf7d
fix(shell): an org without schedules is asked again hourly, so an ope…
Makisuo Nov 10, 2026
655534e
docs(design-rules): list the form sheet size
Makisuo Nov 10, 2026
479e46c
fix(shell): a folded group keeps the current sub-item; on-call is re-…
Makisuo Nov 10, 2026
7360717
docs(shell): navRow's comment follows the folded group's current sub-…
Makisuo Nov 10, 2026
3c7b214
Merge branch 'ux/shell' into ux/alerting-pages
Makisuo Nov 10, 2026
8310e9d
Merge branch 'ux/shell' into ux/alerting-pages
Makisuo Nov 10, 2026
1b74ea2
Merge branch 'ux/alerting-pages' into ux/schedule-preview
Makisuo Nov 10, 2026
6a9012e
Merge remote-tracking branch 'origin/main' into ux/alerting-pages
Makisuo Nov 10, 2026
48de1e8
Merge branch 'ux/alerting-pages' into ux/schedule-preview
Makisuo Nov 10, 2026
82bc2b6
Merge remote-tracking branch 'origin/main' into ux/schedule-preview
Makisuo Nov 10, 2026
72e2854
fix(alerts): the schedule editor's preview follows the clock while it…
Makisuo Nov 10, 2026
63832b8
fix(alerts): schedule editor goes two-column only once the timeline f…
Makisuo Nov 10, 2026
00a9419
fix(alerts): count the gap and the preview's padding in the schedule …
Makisuo Nov 10, 2026
8ea5171
Merge remote-tracking branch 'origin/main' into ux/schedule-preview
Makisuo Nov 10, 2026
58a34f0
fix(alerts): schedule editor's two-column breakpoint also clears the …
Makisuo Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@fontsource-variable/geist-mono": "^5.3.0",
"@maple-dev/effect-sdk": "workspace:*",
"@maple/agent-sessions": "workspace:*",
"@maple/alerting-core": "workspace:*",
"@maple/chat-platform": "workspace:*",
"@maple/domain": "workspace:*",
"@maple/effect-db": "workspace:*",
Expand Down
49 changes: 47 additions & 2 deletions apps/web/src/lab/oncall-lab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export const LabState = Schema.Literals([
"schedules",
"strip",
"editor-new",
"editor-covered",
"editor-gaps",
"editor-early",
"editor-override",
"editor-edit",
"editor-invalid",
"editor-error",
Expand Down Expand Up @@ -186,8 +190,14 @@ const hubFor = (state: LabState): Step<HubModel.Model, HubMessage.Message> => {
const formStep = (model: FormModel.Model, message: FormMessage.Message) =>
FormUpdate.update(model, message, CONTEXT)

const addPerson = (index: number) =>
FormMessage.Message.GotParticipantMenuMessage({
key: "l0",
message: { _tag: "ClickedItem", index, item: { kind: "Item", value: memberId(index) } },
})

const formFor = (state: LabState): Step<FormModel.Model, FormMessage.Message> => {
const isEdit = state === "editor-edit" || state === "editor-error"
const isEdit = state === "editor-edit" || state === "editor-error" || state === "editor-override"
const start = FormUpdate.init(
{ _tag: "AlertsScheduleNew", search: isEdit ? { scheduleId: schedulePublicId(1) } : {} },
CONTEXT,
Expand All @@ -206,6 +216,10 @@ const formFor = (state: LabState): Step<FormModel.Model, FormMessage.Message> =>
key: start.model.existing.key ?? "",
result: Result.succeed(checkoutSchedule(ONCALL_NOW)),
}),
FormMessage.Message.CompletedFetchOverrides({
key: start.model.overrides.key ?? "",
result: Result.succeed(checkoutOverrides(ONCALL_NOW)),
}),
]
: []),
]
Expand Down Expand Up @@ -252,7 +266,38 @@ const formFor = (state: LabState): Step<FormModel.Model, FormMessage.Message> =>
]
: state === "editor-new"
? [FormMessage.Message.ChangedName({ value: "Checkout primary" })]
: []
: state === "editor-covered" || state === "editor-gaps" || state === "editor-early"
? [
FormMessage.Message.ChangedName({ value: "Checkout primary" }),
addPerson(0),
addPerson(1),
// Weeknights only: the days and weekends show as gaps.
...(state === "editor-gaps"
? [FormMessage.Message.ClickedAddRestriction({ key: "l0" })]
: []),
// A start two days back: the rotation is already under way.
...(state === "editor-early"
? [
FormMessage.Message.PickedRotation({
key: "l0",
rotation: "daily",
}),
FormMessage.Message.ChangedLayerText({
key: "l0",
field: "startsAt",
value: "2026-11-08T09:00",
}),
]
: []),
]
: state === "editor-override"
? [
// One weeknights layer, so the saved overrides sit over real gaps.
FormMessage.Message.ClickedRemoveLayer({ key: "l2" }),
FormMessage.Message.ClickedRemoveLayer({ key: "l1" }),
FormMessage.Message.ClickedAddRestriction({ key: "l0" }),
]
: []
return fold(start, [...base, ...extra], formStep)
}

Expand Down
32 changes: 30 additions & 2 deletions apps/web/src/page/alerts/hub/hub-oncall.scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { AlertsTab } from "../../../route/search"
import { OrgId } from "@maple/domain"
import { EscalationChainId, OnCallScheduleId } from "@maple/domain/http"
import type { V2OnCallSchedule } from "@maple/domain/http/v2"
import { Result, Schema, Array as Arr } from "effect"
import { Result, Schema, Array as Arr, String as Str } from "effect"
import * as Menu from "@maple/views/ui/menu"
import { expect as expectView, given, role, scene, text, withViewInputs } from "foldkit/scene"
import * as Dialog from "@maple/views/ui/dialog"
import { MountDialog } from "@maple/views/ui/dialog"
import { expect as expectView, given, Mount, role, scene, text, within, withViewInputs } from "foldkit/scene"
import { describe, expect, test } from "vitest"
import type { PageContext } from "../../contract"
import { chainDestinations, chainUuid } from "../shared/escalation-test-fixtures"
Expand Down Expand Up @@ -220,6 +222,32 @@ describe("alerts hub, on-call schedules", () => {
)
})

test("the delete confirm is named for its schedule, a long unbroken name in full", () => {
const name = `checkout-primary-${Str.repeat(120)("x")}`
const scheduleId = Schema.decodeSync(OnCallScheduleId)(scheduleUuid(1))
const asked = pageUpdate(
onTab(
"on-call",
ENGINEER,
Arr.map(SCHEDULES, (schedule) =>
schedule.id === scheduleId ? { ...schedule, name } : schedule,
),
),
Message.GotRowMenuMessage({
menuId: scheduleMenuId(scheduleId),
message: Menu.Message.ClickedItem({ index: 1, item: { kind: "Item", value: "delete" } }),
}),
).model
expect(asked.confirmDeleteScheduleId).toBe(scheduleId)
scene(
{ update: pageUpdate, view },
given(asked),
Mount.resolve(MountDialog, Dialog.Message.CompletedMountDialog()),
expectView(role("alertdialog", { name: `Delete ${name}?` })).toExist(),
expectView(within(role("alertdialog"), role("heading", { name: `Delete ${name}?` }))).toExist(),
)
})

test("a refused delete names the chains that still page the schedule", () => {
const model = onTab("on-call", ENGINEER, SCHEDULES)
const scheduleId = Schema.decodeSync(OnCallScheduleId)(scheduleUuid(1))
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/page/alerts/hub/view-oncall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,8 @@ export const deleteScheduleDialog = (h: H, model: Model): Html => {
slotId: DELETE_SCHEDULE_DIALOG_ID,
model: model.deleteScheduleDialog,
toParentMessage: (message) => Message.GotDeleteScheduleDialogMessage({ message }),
title: `Delete ${name}?`,
// A long unbroken name wraps inside the dialog instead of running out of it.
title: [typography(h, { className: "wrap-anywhere" }, [`Delete ${name}?`])],
description: "Its overrides go with it. This cannot be undone.",
children:
model.deleteScheduleError === null
Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/page/alerts/schedule-form/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Clock, Effect, Schema } from "effect"
import { Command } from "foldkit"
import { readGrants } from "../../../data/session-tenant"
import { browserZone, readSupportedTimeZones } from "../../../platform/time-zone"
import { retrieveOnCallSchedule, saveOnCallSchedule } from "../shared/oncall-schedules"
import { listOverrides, retrieveOnCallSchedule, saveOnCallSchedule } from "../shared/oncall-schedules"
import { listOrgMembers } from "../shared/org-members"
import { Message } from "./message"

Expand All @@ -27,6 +27,17 @@ export const FetchExistingSchedule = Command.define("FetchScheduleFormSchedule",
),
})

/** The edited schedule's overrides that have not ended, for the preview. */
export const FetchScheduleFormOverrides = Command.define("FetchScheduleFormOverrides", {
args: { key: Schema.String, scheduleId: OnCallScheduleId },
messages: [Message.CompletedFetchOverrides],
execute: ({ key, scheduleId }) =>
listOverrides(scheduleId).pipe(
Effect.result,
Effect.map((result) => Message.CompletedFetchOverrides({ key, result })),
),
})

export const FetchScheduleFormMembers = Command.define("FetchScheduleFormMembers", {
messages: [Message.CompletedFetchMembers],
execute: listOrgMembers.pipe(
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/page/alerts/schedule-form/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ export type LayerErrors = Readonly<{
restrictions: ReadonlyArray<Option.Option<string>>
}>

const restrictionError = (restriction: RestrictionDraft): Option.Option<string> =>
export const restrictionError = (restriction: RestrictionDraft): Option.Option<string> =>
restriction.weekdays.length === 0
? Option.some("Pick at least one day")
: !isLocalTime(restriction.startTime) || !isLocalTime(restriction.endTime)
? Option.some("Use HH:MM times")
: Option.none()

/** The layer's start: the saved instant while untouched, else its text on the zone's clock. */
const layerStartMs = (layer: LayerDraft, timeZone: string): Option.Option<number> =>
export const layerStartMs = (layer: LayerDraft, timeZone: string): Option.Option<number> =>
layer.startsAtMs === null ? localInputMs(layer.startsAt, timeZone) : Option.some(layer.startsAtMs)

export const layerErrors = (layer: LayerDraft, timeZone: string): LayerErrors => {
Expand Down
22 changes: 21 additions & 1 deletion apps/web/src/page/alerts/schedule-form/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { definePage } from "../../contract"
import { Clock, Effect, Schema, Stream } from "effect"
import { Subscription } from "foldkit"
import type { Resources } from "../../../platform/resources"
import { definePage, type PageSubscriptionInput } from "../../contract"
import { oncallTabHref } from "../shared/oncall-schedules"
import { Message } from "./message"
import { Model } from "./model"
import { init, update } from "./update"
import { view } from "./view"

/** The preview's now line follows the clock, on the schedule page's 30-second cadence. */
const CLOCK_TICK = "30 seconds"

const subscriptions = Subscription.make<PageSubscriptionInput<Model>, Message, Resources>()((entry) => ({
clock: entry(Schema.Struct({}).fields, {
modelToDependencies: () => ({}),
dependenciesToStream: () =>
Stream.tick(CLOCK_TICK).pipe(
Stream.drop(1),
Stream.mapEffect(() =>
Effect.map(Clock.currentTimeMillis, (nowMs) => Message.ReadClock({ nowMs })),
),
),
}),
}))

/** `/alerts/schedules/new`: one instance per schedule being edited, or the new one. */
export const page = definePage(
"AlertsScheduleNew",
Expand All @@ -15,6 +34,7 @@ export const page = definePage(
init,
update,
view,
subscriptions,
breadcrumbs: () => [
{ label: "Alerts", href: "/alerts" },
{ label: "On-call", href: oncallTabHref() },
Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/page/alerts/schedule-form/message.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as Policy from "@maple/domain/policy"
import { OnCallWeekday, UserId } from "@maple/domain/http"
import { V2OnCallSchedule } from "@maple/domain/http/v2"
import { V2OnCallOverride, V2OnCallSchedule } from "@maple/domain/http/v2"
import { Schema } from "effect"
import { defineMessageUnion } from "foldkit/message"
import { LoadFailure } from "@maple/views/data/load-failure"
import * as Combobox from "@maple/views/ui/combobox"
import * as Menu from "@maple/views/ui/menu"
import { OrgMember } from "../shared/org-members"
import { RangeDays } from "../shared/coverage-preview"
import { RotationKind } from "./form"

const outcome = <S extends Schema.Top>(success: S) => Schema.Result(success, LoadFailure)
Expand All @@ -20,8 +21,11 @@ export type LayerField = typeof LayerField.Type

export const Message = defineMessageUnion({
ReadDefaults: { timeZone: Schema.String, nowMs: Schema.Number, zones: Schema.Array(Schema.String) },
ReadClock: { nowMs: Schema.Number },
CompletedFetchExisting: { key: Schema.String, result: outcome(V2OnCallSchedule) },
ClickedRetryExisting: {},
CompletedFetchOverrides: { key: Schema.String, result: outcome(Schema.Array(V2OnCallOverride)) },
ClickedRetryOverrides: {},
CompletedFetchMembers: { result: Schema.Result(Schema.Array(OrgMember), Schema.String) },
CompletedFetchSession: { orgId: Schema.String, grants: Schema.Array(Policy.PermissionName) },
ChangedName: { value: Schema.String },
Expand All @@ -46,6 +50,13 @@ export const Message = defineMessageUnion({
edge: Schema.Literals(["start", "end"]),
value: Schema.String,
},
ClickedPreviewPrevious: {},
ClickedPreviewNext: {},
ClickedPreviewToday: {},
PickedPreviewRange: { days: RangeDays },
/** A gap in the summary: its bar on the timeline takes focus. */
ClickedPreviewGap: { index: Schema.Number },
CompletedFocusGap: {},
ClickedSave: {},
CompletedSave: { result: outcome(V2OnCallSchedule) },
})
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/page/alerts/schedule-form/model.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as Policy from "@maple/domain/policy"
import { OnCallScheduleId } from "@maple/domain/http"
import { V2OnCallSchedule } from "@maple/domain/http/v2"
import { V2OnCallOverride, V2OnCallSchedule } from "@maple/domain/http/v2"
import { Array as Arr, Order, Schema, String as Str } from "effect"
import { KeyedSchema } from "@maple/views/data/keyed"
import { LoadFailure } from "@maple/views/data/load-failure"
import * as Combobox from "@maple/views/ui/combobox"
import * as Menu from "@maple/views/ui/menu"
import { AlertsScheduleNewSearch } from "../../../route/search"
import { OrgMember } from "../shared/org-members"
import { RangeDays } from "../shared/coverage-preview"
import { Form } from "./form"

// `/alerts/schedules/new`: create an on-call schedule, or edit one (`?scheduleId=`).
Expand All @@ -18,6 +19,8 @@ export const Model = Schema.Struct({
editingId: Schema.NullOr(OnCallScheduleId),
/** The edited schedule's read; the form fills from it once. */
existing: KeyedSchema(V2OnCallSchedule, LoadFailure),
/** The edited schedule's overrides, which the preview layers over the draft; a new one has none. */
overrides: KeyedSchema(Schema.Array(V2OnCallOverride), LoadFailure),
form: Form,
/** The browser's zone and clock arrived: a new form starts from them. */
isReady: Schema.Boolean,
Expand All @@ -35,6 +38,9 @@ export const Model = Schema.Struct({
saveFailure: Schema.NullOr(LoadFailure),
grants: Schema.Array(Policy.PermissionName),
isSessionLoaded: Schema.Boolean,
/** The coverage preview: weeks from today, and how many days it shows. */
previewWeekOffset: Schema.Number,
previewRange: RangeDays,
})
export type Model = typeof Model.Type

Expand Down
85 changes: 85 additions & 0 deletions apps/web/src/page/alerts/schedule-form/preview.scene.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as Policy from "@maple/domain/policy"
import { OrgId } from "@maple/domain"
import { Option, Result, Schema, Array as Arr } from "effect"
import {
click,
expect as expectView,
find,
given,
role,
scene,
tap,
text,
type,
withViewInputs,
} from "foldkit/scene"
import { expect, test } from "vitest"
import type { PageContext } from "../../contract"
import { memberId, ONCALL_NOW, oncallMembers } from "../shared/oncall-test-fixtures"
import { ORG } from "../shared/test-rows"
import { Message } from "./message"
import type { Model } from "./model"
import { init, update } from "./update"
import { view as pageView } from "./view"

// The preview renders the whole schedule, so it must not redo that work on edits it does not read.

const context: PageContext = { orgId: Schema.decodeSync(OrgId)(ORG), pinnedNamespace: null, timeZone: "UTC" }
const pageUpdate = (model: Model, message: Message) => update(model, message, context)
const view = withViewInputs(pageView, { context })()

const staffed = (): Model =>
Arr.reduce(
[
Message.ReadDefaults({
timeZone: "Europe/Berlin",
nowMs: ONCALL_NOW,
zones: ["UTC", "Europe/Berlin"],
}),
Message.CompletedFetchMembers({ result: Result.succeed(oncallMembers) }),
Message.CompletedFetchSession({ orgId: ORG, grants: Policy.grantsOf(["org:engineer"]) }),
Message.GotParticipantMenuMessage({
key: "l0",
message: { _tag: "ClickedItem", index: 0, item: { kind: "Item", value: memberId(0) } },
}),
],
init({ _tag: "AlertsScheduleNew", search: {} }, context).model,
(model, message) => pageUpdate(model, message).model,
)

test("typing the name keeps the drawn preview; a new range draws it again", () => {
// The preview's rendered node: the same object while nothing it reads has changed.
const drawn: Array<unknown> = []
const keep = tap<Model, Message>((simulation) => {
drawn.push(Option.getOrNull(find(simulation.html, '[data-slot="coverage-preview"]')))
})
scene(
{ update: pageUpdate, view },
given(staffed()),
expectView(text("Covered in this period")).toExist(),
keep,
type(role("textbox", { name: "Name" }), "Checkout primary"),
type(role("textbox", { name: "Description (optional)" }), "Who swaps with whom"),
keep,
click(role("button", { name: "4 weeks" })),
expectView(text("Covered in this period")).toExist(),
keep,
)
expect(drawn[0]).not.toBeNull()
expect(drawn[1]).toBe(drawn[0])
expect(drawn[2]).not.toBe(drawn[0])
})

test("the clock moves the preview's now line while the editor stays open", () => {
scene(
{ update: pageUpdate, view },
given(staffed()),
expectView(text("Now, Tue 10 Nov, 11:30")).toExist(),
)
scene(
{ update: pageUpdate, view },
given(pageUpdate(staffed(), Message.ReadClock({ nowMs: ONCALL_NOW + 45 * 60_000 })).model),
expectView(text("Now, Tue 10 Nov, 12:15")).toExist(),
expectView(text("Now, Tue 10 Nov, 11:30")).toBeAbsent(),
)
})
Loading
Loading