Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f972b97
feat(settings): move auto-triage to the AI tab with provider cards an…
Makisuo Oct 17, 2026
fd51f1f
feat(digest): send digests on the subscriber's day, hour and time zone
Makisuo Oct 17, 2026
f5e0600
feat(account): rebuild email preferences with a schedule and save bar
Makisuo Oct 17, 2026
82d86f9
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
49abaca
fix(settings): guard digest claims on the send window, not UTC midnight
Makisuo Oct 17, 2026
8a90cb7
fix(settings): prefill the digest zone from the browser and note when…
Makisuo Oct 17, 2026
166c63f
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
0acb5a1
fix(settings): keep saved AI limits and email schedule on screen unti…
Makisuo Oct 17, 2026
ee8126e
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
eb6fa94
style(settings): format the investigations limit link
Makisuo Oct 17, 2026
9ec013e
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
1db1160
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
7f5cd8e
fix(settings): release the AI limits draft when the save normalized it
Makisuo Oct 17, 2026
46dcbf9
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
205a573
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
145c05c
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
6834531
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
1d054b7
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
b956913
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
d9e2501
fix(settings): commit AI limits and the email schedule with the draft…
Makisuo Oct 17, 2026
a59fa18
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
7b666d6
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
6021fc2
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
e395167
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
55229ea
fix(settings): track AI limits and email schedule saves as in flight …
Makisuo Oct 17, 2026
0c6d017
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
a05c12f
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
2d24089
fix(settings): let admins remove a saved OpenRouter key after the add…
Makisuo Oct 17, 2026
6ce2206
feat(settings): show auto-triage as coming soon outside the rollout, …
Makisuo Oct 17, 2026
17659d1
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
97f68a4
fix(settings): give both model provider cards the same height
Makisuo Oct 17, 2026
359022d
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 2026
a06d415
Merge branch 'feat/settings-mcp' into feat/settings-ai-email-prefs
Makisuo Oct 17, 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/api/src/routes/internal/digest.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const HttpDigestLive = HttpApiBuilder.group(MapleInternalApi, "digest", (
email: payload.email,
enabled: payload.enabled,
dayOfWeek: payload.dayOfWeek,
sendHour: payload.sendHour,
timezone: payload.timezone,
namespaces: payload.namespaces,
environments: payload.environments,
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/components/account/digest-preferences.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest"

import { digestTimeZoneOptions, newDigestDraft, sameDigestDraft } from "./digest-preferences"

const base = {
dayOfWeek: 1,
sendHour: 9,
timezone: "Europe/Berlin",
namespaces: ["payments", "checkout"],
environments: ["production"],
}

describe("sameDigestDraft", () => {
it("ignores the order scope values were picked in", () => {
expect(sameDigestDraft(base, { ...base, namespaces: ["checkout", "payments"] })).toBe(true)
})

it("sees a change to the day, hour, zone or scope", () => {
expect(sameDigestDraft(base, { ...base, dayOfWeek: 0 })).toBe(false)
expect(sameDigestDraft(base, { ...base, sendHour: 10 })).toBe(false)
expect(sameDigestDraft(base, { ...base, timezone: "UTC" })).toBe(false)
expect(sameDigestDraft(base, { ...base, environments: [] })).toBe(false)
})
})

describe("digestTimeZoneOptions", () => {
it("keeps the saved and browser zones selectable and puts UTC first", () => {
const zones = digestTimeZoneOptions(["Europe/Berlin", "Asia/Tokyo"], "Etc/GMT+5", "America/Denver")
expect(zones[0]).toBe("UTC")
expect(zones).toEqual(["UTC", "America/Denver", "Asia/Tokyo", "Etc/GMT+5", "Europe/Berlin"])
})

it("lists each zone once", () => {
const zones = digestTimeZoneOptions(["UTC", "Europe/Berlin"], "Europe/Berlin", "Europe/Berlin")
expect(zones).toEqual(["UTC", "Europe/Berlin"])
})
})

describe("newDigestDraft", () => {
it("schedules a first-time subscriber in the browser's zone", () => {
expect(newDigestDraft("America/Los_Angeles")).toEqual({
dayOfWeek: 1,
sendHour: 9,
timezone: "America/Los_Angeles",
namespaces: [],
environments: [],
})
})

it("falls back to UTC when the browser reports no zone", () => {
expect(newDigestDraft(undefined).timezone).toBe("UTC")
expect(newDigestDraft("").timezone).toBe("UTC")
})
})
66 changes: 66 additions & 0 deletions apps/web/src/components/account/digest-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { isSameDraft } from "@/components/settings/settings-draft"

/** Monday first, as people read a week; values are `Date#getDay` numbers (0 = Sunday). */
export const DIGEST_DAYS = [
{ value: 1, label: "Monday" },
{ value: 2, label: "Tuesday" },
{ value: 3, label: "Wednesday" },
{ value: 4, label: "Thursday" },
{ value: 5, label: "Friday" },
{ value: 6, label: "Saturday" },
{ value: 0, label: "Sunday" },
] as const

export const DIGEST_HOURS = Array.from({ length: 24 }, (_, hour) => ({
value: hour,
label: `${String(hour).padStart(2, "0")}:00`,
}))

/** What the server applies to a subscriber who never saved a schedule. */
const DEFAULT_DIGEST_SCHEDULE = { dayOfWeek: 1, sendHour: 9, timezone: "UTC" } as const

export interface DigestPreferencesDraft {
readonly dayOfWeek: number
readonly sendHour: number
readonly timezone: string
readonly namespaces: ReadonlyArray<string>
readonly environments: ReadonlyArray<string>
}

/**
* The draft for someone with no subscription yet: the server's day and hour, but in the
* browser's own zone, so the schedule reads in local time before the first save.
*/
export function newDigestDraft(browserZone: string | undefined): DigestPreferencesDraft {
return {
...DEFAULT_DIGEST_SCHEDULE,
timezone:
browserZone !== undefined && browserZone !== "" ? browserZone : DEFAULT_DIGEST_SCHEDULE.timezone,
namespaces: [],
environments: [],
}
}

const sorted = (values: ReadonlyArray<string>) => [...values].sort()

/** Scopes are sets: picking the same namespaces in another order is not an edit. */
export function sameDigestDraft(a: DigestPreferencesDraft, b: DigestPreferencesDraft): boolean {
return isSameDraft(
{ ...a, namespaces: sorted(a.namespaces), environments: sorted(a.environments) },
{ ...b, namespaces: sorted(b.namespaces), environments: sorted(b.environments) },
)
}

/**
* Zones for the picker: every zone the browser knows, plus UTC, the saved zone and the
* browser's own zone, so the current value is always selectable even when the list lacks it.
*/
export function digestTimeZoneOptions(
supported: ReadonlyArray<string>,
saved: string,
browserZone: string | undefined,
): ReadonlyArray<string> {
const zones = new Set([...supported, "UTC", saved])
if (browserZone) zones.add(browserZone)
return [...zones].sort((a, b) => (a === "UTC" ? -1 : b === "UTC" ? 1 : a.localeCompare(b)))
}
Loading
Loading