From 1a4fdd4d7bb4ca65f669bec163e4de5eb43949fd Mon Sep 17 00:00:00 2001 From: Alex Lider Date: Wed, 30 Sep 2026 20:21:28 +0200 Subject: [PATCH 01/11] fix(navigation): show pending invite dot on the organization row The pending invite indicator in the account menu moves from the project row to the organization row, and the pending invite list moves from the project switcher to the organization switcher. An invite joins an organization, so the indicator now sits where the invite is accepted. Generated-By: PostHog Desktop Task-Id: 3c469151-6420-43b0-8fb4-d290680020ab --- .../lib/components/Account/NewAccountMenu.tsx | 2 +- .../Account/OrgSwitcher.stories.tsx | 58 +++++++++++++++ .../lib/components/Account/OrgSwitcher.tsx | 72 +++++++++++++++++- .../Account/ProjectSwitcher.stories.tsx | 23 ++---- .../components/Account/ProjectSwitcher.tsx | 73 +------------------ 5 files changed, 136 insertions(+), 92 deletions(-) create mode 100644 frontend/src/lib/components/Account/OrgSwitcher.stories.tsx diff --git a/frontend/src/lib/components/Account/NewAccountMenu.tsx b/frontend/src/lib/components/Account/NewAccountMenu.tsx index c7b3e89a22da..fc7c1c5371f0 100644 --- a/frontend/src/lib/components/Account/NewAccountMenu.tsx +++ b/frontend/src/lib/components/Account/NewAccountMenu.tsx @@ -183,7 +183,6 @@ export function NewAccountMenu({ isLayoutNavCollapsed }: AccountMenuProps): JSX. {currentTeam ? projectNameWithoutFirstEmoji : 'Select project'} - {hasPendingInvites && } } @@ -293,6 +292,7 @@ export function NewAccountMenu({ isLayoutNavCollapsed }: AccountMenuProps): JSX. ? currentOrganization.name : 'Select organization'} + {hasPendingInvites && } } diff --git a/frontend/src/lib/components/Account/OrgSwitcher.stories.tsx b/frontend/src/lib/components/Account/OrgSwitcher.stories.tsx new file mode 100644 index 000000000000..1c30f51f069d --- /dev/null +++ b/frontend/src/lib/components/Account/OrgSwitcher.stories.tsx @@ -0,0 +1,58 @@ +import { MOCK_DEFAULT_ORGANIZATION, MOCK_DEFAULT_TEAM, MOCK_DEFAULT_USER } from 'lib/api.mock' + +import type { Meta, StoryObj } from '@storybook/react' + +import { useStorybookMocks } from '~/mocks/browser' + +import { OrgSwitcher } from './OrgSwitcher' + +type StoryProps = { hasPendingInvite: boolean } + +const PENDING_INVITE = { + id: '018f0000-0000-0000-0000-000000000001', + target_email: MOCK_DEFAULT_USER.email, + organization_id: '018f0000-0000-0000-0000-00000000abcd', + organization_name: 'Acme Corp', + created_at: '2026-04-17T12:00:00Z', +} + +const meta: Meta<(props: StoryProps) => JSX.Element> = { + title: 'Components/Account/Org Switcher', + parameters: { + layout: 'centered', + viewMode: 'story', + }, + render: ({ hasPendingInvite }: StoryProps) => { + useStorybookMocks({ + get: { + '/api/users/@me/': () => [ + 200, + { + ...MOCK_DEFAULT_USER, + pending_invites: hasPendingInvite ? [PENDING_INVITE] : [], + }, + ], + '/api/organizations/@current/': () => [200, MOCK_DEFAULT_ORGANIZATION], + '/api/environments/@current/': () => [200, MOCK_DEFAULT_TEAM], + '/api/projects/@current/': () => [200, MOCK_DEFAULT_TEAM], + }, + }) + + return ( +
+ +
+ ) + }, +} +export default meta + +type Story = StoryObj<(props: StoryProps) => JSX.Element> + +export const NoPendingInvite: Story = { + args: { hasPendingInvite: false }, +} + +export const WithPendingInvite: Story = { + args: { hasPendingInvite: true }, +} diff --git a/frontend/src/lib/components/Account/OrgSwitcher.tsx b/frontend/src/lib/components/Account/OrgSwitcher.tsx index c8b0ad89177d..07e7ffd20033 100644 --- a/frontend/src/lib/components/Account/OrgSwitcher.tsx +++ b/frontend/src/lib/components/Account/OrgSwitcher.tsx @@ -2,7 +2,7 @@ import { Combobox } from '@base-ui/react/combobox' import { useActions, useValues } from 'kea' import { useCallback, useMemo, useRef, useState } from 'react' -import { IconCheck, IconPlusSmall, IconSearch, IconX } from '@posthog/icons' +import { IconCheck, IconLetter, IconPlusSmall, IconSearch, IconX } from '@posthog/icons' import { KeyboardShortcut } from 'lib/components/KeyboardShortcut/KeyboardShortcut' import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' @@ -13,6 +13,7 @@ import { ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives' import { MenuSeparator } from 'lib/ui/Menus/Menus' import { cn } from 'lib/utils/css-classes' import { organizationLogic } from 'scenes/organizationLogic' +import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { globalModalsLogic } from '~/layout/globalModalsLogic' @@ -21,6 +22,7 @@ import { AvailableFeature, OrganizationBasicType } from '~/types' import { ScrollableShadows } from '../ScrollableShadows/ScrollableShadows' import { newAccountMenuLogic } from './newAccountMenuLogic' +import { pendingInvitesLogic, PendingInviteForCurrentUser } from './pendingInvitesLogic' interface OrgListItem { type: 'org' @@ -31,13 +33,19 @@ interface OrgListItem { disabledReason?: string } +interface PendingInviteListItem { + type: 'pending-invite' + id: string + invite: PendingInviteForCurrentUser +} + interface CreateOrgItem { type: 'create' id: 'create-new-org' label: string } -type ListItem = OrgListItem | CreateOrgItem +type ListItem = OrgListItem | PendingInviteListItem | CreateOrgItem export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Element { const { preflight } = useValues(preflightLogic) @@ -45,6 +53,7 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen const { showCreateOrganizationModal } = useActions(globalModalsLogic) const { currentOrganization } = useValues(organizationLogic) const { otherOrganizations } = useValues(userLogic) + const { pendingInvites } = useValues(pendingInvitesLogic) const { updateCurrentOrganization } = useActions(userLogic) const { closeOrgSwitcher, setAccountMenuOpen } = useActions(newAccountMenuLogic) const [searchValue, setSearchValue] = useState('') @@ -77,6 +86,16 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen return items }, [currentOrganization, otherOrganizations]) + const allPendingInviteItems: PendingInviteListItem[] = useMemo( + () => + pendingInvites.map((invite) => ({ + type: 'pending-invite' as const, + id: invite.id, + invite, + })), + [pendingInvites] + ) + const filteredItems = useMemo(() => { const searchLower = searchValue.trim().toLowerCase() @@ -85,6 +104,10 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen ? allOrgItems.filter((item) => item.org.name.toLowerCase().includes(searchLower)) : allOrgItems + const filteredInvites = searchLower + ? allPendingInviteItems.filter((item) => item.invite.organization_name.toLowerCase().includes(searchLower)) + : allPendingInviteItems + // Create the "create" item - show different label based on search const createItem: CreateOrgItem = { type: 'create', @@ -94,13 +117,16 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen // label: searchValue.trim() ? `Create '${searchValue.trim()}'` : 'New organization', } - return [...filteredOrgs, createItem] as ListItem[] - }, [allOrgItems, searchValue]) + return [...filteredOrgs, ...filteredInvites, createItem] as ListItem[] + }, [allOrgItems, allPendingInviteItems, searchValue]) const currentOrgItem = filteredItems.find((o): o is OrgListItem => o.type === 'org' && o.isCurrent) const otherOrgItems = filteredItems .filter((o): o is OrgListItem => o.type === 'org' && !o.isCurrent) .sort((a, b) => a.org.name.localeCompare(b.org.name)) + const pendingInviteItems = filteredItems + .filter((o): o is PendingInviteListItem => o.type === 'pending-invite') + .sort((a, b) => a.invite.organization_name.localeCompare(b.invite.organization_name)) const createItem = filteredItems.find((o): o is CreateOrgItem => o.type === 'create') const handleItemClick = useCallback( @@ -115,6 +141,9 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen { guardOnCloud: false } ) closeOrgSwitcher() + } else if (item.type === 'pending-invite') { + closeOrgSwitcher() + window.location.href = urls.inviteSignup(item.invite.id) } else if (!item.isCurrent && !item.isDisabled) { closeOrgSwitcher() updateCurrentOrganization(item.org.id) @@ -136,6 +165,9 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen if (item.type === 'create') { return item.label } + if (item.type === 'pending-invite') { + return item.invite.organization_name + } return item.org.name }, []) @@ -271,6 +303,38 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen )} + {/* Pending Invitations */} + {pendingInviteItems.length > 0 && ( + + + {(item: PendingInviteListItem) => ( + handleItemClick(item)} + render={(props) => ( + + + + {item.invite.organization_name} + + + Pending invite + + + )} + /> + )} + + + )} + {/* Create New Organization */} diff --git a/frontend/src/lib/components/Account/ProjectSwitcher.stories.tsx b/frontend/src/lib/components/Account/ProjectSwitcher.stories.tsx index de82ae88c739..20aaf1a87041 100644 --- a/frontend/src/lib/components/Account/ProjectSwitcher.stories.tsx +++ b/frontend/src/lib/components/Account/ProjectSwitcher.stories.tsx @@ -8,15 +8,7 @@ import { useStorybookMocks } from '~/mocks/browser' import { ProjectSwitcher } from './ProjectSwitcher' -type StoryProps = { hasPendingInvite: boolean; hasDataFreshness?: boolean } - -const PENDING_INVITE = { - id: '018f0000-0000-0000-0000-000000000001', - target_email: MOCK_DEFAULT_USER.email, - organization_id: '018f0000-0000-0000-0000-00000000abcd', - organization_name: 'Acme Corp', - created_at: '2026-04-17T12:00:00Z', -} +type StoryProps = { hasDataFreshness?: boolean } // The case this feature exists for: a pile of similarly named leftovers around one product, // where only one is real and the names alone can't tell you which. @@ -79,7 +71,7 @@ const meta: Meta<(props: StoryProps) => JSX.Element> = { layout: 'centered', viewMode: 'story', }, - render: ({ hasPendingInvite, hasDataFreshness }: StoryProps) => { + render: ({ hasDataFreshness }: StoryProps) => { const organization = hasDataFreshness ? { ...MOCK_DEFAULT_ORGANIZATION, teams: [MOCK_DEFAULT_TEAM, ...FRESHNESS_TEAMS] } : MOCK_DEFAULT_ORGANIZATION @@ -91,7 +83,6 @@ const meta: Meta<(props: StoryProps) => JSX.Element> = { { ...MOCK_DEFAULT_USER, organization, - pending_invites: hasPendingInvite ? [PENDING_INVITE] : [], }, ], '/api/organizations/@current/': () => [200, organization], @@ -115,14 +106,10 @@ export default meta type Story = StoryObj<(props: StoryProps) => JSX.Element> -export const NoPendingInvite: Story = { - args: { hasPendingInvite: false }, -} - -export const WithPendingInvite: Story = { - args: { hasPendingInvite: true }, +export const Default: Story = { + args: { hasDataFreshness: false }, } export const WithDataFreshness: Story = { - args: { hasPendingInvite: false, hasDataFreshness: true }, + args: { hasDataFreshness: true }, } diff --git a/frontend/src/lib/components/Account/ProjectSwitcher.tsx b/frontend/src/lib/components/Account/ProjectSwitcher.tsx index ce88fb62800d..1b61bebbac88 100644 --- a/frontend/src/lib/components/Account/ProjectSwitcher.tsx +++ b/frontend/src/lib/components/Account/ProjectSwitcher.tsx @@ -2,7 +2,7 @@ import { Combobox } from '@base-ui/react/combobox' import { useActions, useValues } from 'kea' import { useCallback, useMemo, useRef, useState } from 'react' -import { IconCheck, IconLetter, IconPlusSmall, IconSearch, IconX } from '@posthog/icons' +import { IconCheck, IconPlusSmall, IconSearch, IconX } from '@posthog/icons' import { KeyboardShortcut } from 'lib/components/KeyboardShortcut/KeyboardShortcut' import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' @@ -14,14 +14,12 @@ import { cn } from 'lib/utils/css-classes' import { getProjectSwitchTargetUrl } from 'lib/utils/kea-router' import { organizationLogic } from 'scenes/organizationLogic' import { isAuthenticatedTeam, teamLogic } from 'scenes/teamLogic' -import { urls } from 'scenes/urls' import { globalModalsLogic } from '~/layout/globalModalsLogic' import { AvailableFeature, TeamBasicType } from '~/types' import { ScrollableShadows } from '../ScrollableShadows/ScrollableShadows' import { newAccountMenuLogic } from './newAccountMenuLogic' -import { pendingInvitesLogic, PendingInviteForCurrentUser } from './pendingInvitesLogic' import { ProjectFreshnessIndicator } from './ProjectFreshnessIndicator' import { ProjectName } from './ProjectMenu' @@ -32,19 +30,13 @@ interface ProjectListItem { isCurrent: boolean } -interface PendingInviteListItem { - type: 'pending-invite' - id: string - invite: PendingInviteForCurrentUser -} - interface CreateProjectItem { type: 'create' id: 'create-new-project' label: string } -type ListItem = ProjectListItem | PendingInviteListItem | CreateProjectItem +type ListItem = ProjectListItem | CreateProjectItem export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Element | null { const { preflight } = useValues(preflightLogic) @@ -52,7 +44,6 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El const { showCreateProjectModal } = useActions(globalModalsLogic) const { currentTeam } = useValues(teamLogic) const { currentOrganization, projectCreationForbiddenReason } = useValues(organizationLogic) - const { pendingInvites } = useValues(pendingInvitesLogic) const { closeProjectSwitcher, setAccountMenuOpen } = useActions(newAccountMenuLogic) const [searchValue, setSearchValue] = useState('') const inputRef = useRef(null!) @@ -74,16 +65,6 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El return items }, [currentOrganization?.teams, currentTeam?.id]) - const allPendingInviteItems: PendingInviteListItem[] = useMemo( - () => - pendingInvites.map((invite) => ({ - type: 'pending-invite' as const, - id: invite.id, - invite, - })), - [pendingInvites] - ) - const filteredItems = useMemo(() => { const searchLower = searchValue.trim().toLowerCase() @@ -92,10 +73,6 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El ? allProjectItems.filter((item) => item.team.name.toLowerCase().includes(searchLower)) : allProjectItems - const filteredInvites = searchLower - ? allPendingInviteItems.filter((item) => item.invite.organization_name.toLowerCase().includes(searchLower)) - : allPendingInviteItems - // Create the "create" item - show different label based on search const createItem: CreateProjectItem = { type: 'create', @@ -105,16 +82,13 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El // label: searchValue.trim() ? `Create '${searchValue.trim()}'` : 'New project', } - return [...filteredProjects, ...filteredInvites, createItem] as ListItem[] - }, [allProjectItems, allPendingInviteItems, searchValue]) + return [...filteredProjects, createItem] as ListItem[] + }, [allProjectItems, searchValue]) const currentProject = filteredItems.find((p): p is ProjectListItem => p.type === 'project' && p.isCurrent) const otherProjects = filteredItems .filter((p): p is ProjectListItem => p.type === 'project' && !p.isCurrent) .sort((a, b) => a.team.name.localeCompare(b.team.name)) - const pendingInviteItems = filteredItems - .filter((p): p is PendingInviteListItem => p.type === 'pending-invite') - .sort((a, b) => a.invite.organization_name.localeCompare(b.invite.organization_name)) const createItem = filteredItems.find((p): p is CreateProjectItem => p.type === 'create') const canCreateProject = preflight?.can_create_org !== false && !projectCreationForbiddenReason @@ -138,9 +112,6 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El } ) closeProjectSwitcher() - } else if (item.type === 'pending-invite') { - closeProjectSwitcher() - window.location.href = urls.inviteSignup(item.invite.id) } else if (!item.isCurrent) { const targetUrl = getProjectSwitchTargetUrl( location.pathname, @@ -170,9 +141,6 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El if (item.type === 'create') { return item.label } - if (item.type === 'pending-invite') { - return item.invite.organization_name - } return item.team.name }, []) @@ -294,39 +262,6 @@ export function ProjectSwitcher({ dialog = true }: { dialog?: boolean }): JSX.El )} - {/* Pending Invitations */} - {pendingInviteItems.length > 0 && ( - - - {(item: PendingInviteListItem) => ( - handleItemClick(item)} - render={(props) => ( - - - - {item.invite.organization_name} - - - Pending invite - - - )} - /> - )} - - - )} - {/* Create New Project */} From be100121850b86779781a1e2ffbd150d96149fc6 Mon Sep 17 00:00:00 2001 From: Alex Lider Date: Wed, 30 Sep 2026 20:25:41 +0200 Subject: [PATCH 02/11] fix(navigation): explain the pending invite dot on hover The dot now carries a tooltip that names the organization that sent the invite and says where to accept it. The old project button no longer sets its own tooltip, so a single tooltip appears on hover. Generated-By: PostHog Desktop Task-Id: 3c469151-6420-43b0-8fb4-d290680020ab --- .../lib/components/Account/ProjectMenu.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/Account/ProjectMenu.tsx b/frontend/src/lib/components/Account/ProjectMenu.tsx index 4e31700d28da..845738ce29fe 100644 --- a/frontend/src/lib/components/Account/ProjectMenu.tsx +++ b/frontend/src/lib/components/Account/ProjectMenu.tsx @@ -2,6 +2,7 @@ import { useValues } from 'kea' import { LemonSnack } from '@posthog/lemon-ui' +import { Tooltip } from 'lib/lemon-ui/Tooltip' import { ButtonPrimitive, ButtonPrimitiveProps } from 'lib/ui/Button/ButtonPrimitives' import { MenuOpenIndicator } from 'lib/ui/Menus/Menus' import { @@ -27,14 +28,23 @@ export function ProjectName({ team, className }: { team: TeamBasicType; classNam } export function PendingInviteDot({ className }: { className?: string }): JSX.Element { + const { pendingInvites } = useValues(pendingInvitesLogic) + const organizationNames = pendingInvites.map((invite) => invite.organization_name) + const title = + organizationNames.length === 1 + ? `You have a pending invite to join ${organizationNames[0]}. Open the organization menu to accept it.` + : `You have pending invites to join ${organizationNames.length} organizations. Open the organization menu to accept them.` + return ( - - - - + + + + + + ) } @@ -61,7 +71,6 @@ export function ProjectMenu({ iconOnly ? 'min-w-auto' : '', buttonProps.className )} - tooltip={hasPendingInvites ? 'You have a pending invitation' : buttonProps.tooltip} > {iconOnly ? (
From 0b3058b0a10b0fa2e0e44815f45f54eb2fdc5824 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 1 Oct 2026 19:01:30 +0200 Subject: [PATCH 03/11] fix(navigation): polish the pending invite dot and organization row The dot halo no longer spills onto the project name, the tooltip is one sentence, and the pending invite row in the organization switcher uses the same logo, alignment and tag as the other organization rows. Co-Authored-By: Claude Fable 5.1 --- .../lib/components/Account/NewAccountMenu.tsx | 2 +- .../lib/components/Account/OrgSwitcher.tsx | 20 +++++++++++-------- .../lib/components/Account/ProjectMenu.tsx | 13 ++++++------ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/Account/NewAccountMenu.tsx b/frontend/src/lib/components/Account/NewAccountMenu.tsx index fc7c1c5371f0..f6bff8beec7c 100644 --- a/frontend/src/lib/components/Account/NewAccountMenu.tsx +++ b/frontend/src/lib/components/Account/NewAccountMenu.tsx @@ -121,7 +121,7 @@ export function NewAccountMenu({ isLayoutNavCollapsed }: AccountMenuProps): JSX. )} {hasPendingInvites && ( )} diff --git a/frontend/src/lib/components/Account/OrgSwitcher.tsx b/frontend/src/lib/components/Account/OrgSwitcher.tsx index 07e7ffd20033..f7960f8d65bf 100644 --- a/frontend/src/lib/components/Account/OrgSwitcher.tsx +++ b/frontend/src/lib/components/Account/OrgSwitcher.tsx @@ -2,11 +2,12 @@ import { Combobox } from '@base-ui/react/combobox' import { useActions, useValues } from 'kea' import { useCallback, useMemo, useRef, useState } from 'react' -import { IconCheck, IconLetter, IconPlusSmall, IconSearch, IconX } from '@posthog/icons' +import { IconCheck, IconPlusSmall, IconSearch, IconX } from '@posthog/icons' import { KeyboardShortcut } from 'lib/components/KeyboardShortcut/KeyboardShortcut' import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic' import { IconBlank } from 'lib/lemon-ui/icons' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo' import { preflightLogic } from 'lib/logic/preflightLogic' import { ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives' @@ -320,13 +321,16 @@ export function OrgSwitcher({ dialog = true }: { dialog?: boolean }): JSX.Elemen tooltip={`Accept pending invitation to ${item.invite.organization_name}`} tooltipPlacement="right" > - - - {item.invite.organization_name} - - - Pending invite - + + + {item.invite.organization_name} +
+ pending invite +
)} /> diff --git a/frontend/src/lib/components/Account/ProjectMenu.tsx b/frontend/src/lib/components/Account/ProjectMenu.tsx index 845738ce29fe..e2341f7df340 100644 --- a/frontend/src/lib/components/Account/ProjectMenu.tsx +++ b/frontend/src/lib/components/Account/ProjectMenu.tsx @@ -32,16 +32,17 @@ export function PendingInviteDot({ className }: { className?: string }): JSX.Ele const organizationNames = pendingInvites.map((invite) => invite.organization_name) const title = organizationNames.length === 1 - ? `You have a pending invite to join ${organizationNames[0]}. Open the organization menu to accept it.` - : `You have pending invites to join ${organizationNames.length} organizations. Open the organization menu to accept them.` + ? `You have a pending invite to join ${organizationNames[0]}.` + : `You have pending invites to join ${organizationNames.length} organizations.` return ( - + {/* The ping halo scales to twice the dot, so the box is twice the dot to keep it off the neighbors */} + @@ -79,9 +80,7 @@ export function ProjectMenu({ ) : ( {currentTeam.name ?? 'Project'} )} - {hasPendingInvites && ( - - )} + {hasPendingInvites && } {!iconOnly && } From 0fc3d0e9dce9bb72b9485ee5e6584dff6efd02f5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 1 Oct 2026 19:33:42 +0200 Subject: [PATCH 04/11] fix(navigation): keep pending invites reachable on project pending deletion The pending deletion page showed the organization switcher only to users with other organizations. Pending invites now live in that switcher, so show it when the user has a pending invite too. Co-Authored-By: Claude Fable 5.1 --- frontend/src/scenes/project/PendingDeletion.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/scenes/project/PendingDeletion.tsx b/frontend/src/scenes/project/PendingDeletion.tsx index cd181c897014..e47adc9ed5b3 100644 --- a/frontend/src/scenes/project/PendingDeletion.tsx +++ b/frontend/src/scenes/project/PendingDeletion.tsx @@ -5,6 +5,7 @@ import { LemonButton, LemonCard } from '@posthog/lemon-ui' import { newAccountMenuLogic } from 'lib/components/Account/newAccountMenuLogic' import { OrgSwitcher } from 'lib/components/Account/OrgSwitcher' +import { pendingInvitesLogic } from 'lib/components/Account/pendingInvitesLogic' import { ProjectSwitcher } from 'lib/components/Account/ProjectSwitcher' import { HogWelder } from 'lib/components/hedgehogs' import { dayjs } from 'lib/dayjs' @@ -26,7 +27,9 @@ export function ProjectPendingDeletion(): JSX.Element { const { isProjectSwitcherOpen, isOrgSwitcherOpen } = useValues(newAccountMenuLogic) const { openProjectSwitcher, closeProjectSwitcher, openOrgSwitcher, closeOrgSwitcher } = useActions(newAccountMenuLogic) - const hasOtherOrgs = otherOrganizations.length > 0 + const { pendingInvites } = useValues(pendingInvitesLogic) + // Pending invites are accepted from the organization switcher, so show it for them too + const showOrgSwitcher = otherOrganizations.length > 0 || pendingInvites.length > 0 return (
@@ -73,7 +76,7 @@ export function ProjectPendingDeletion(): JSX.Element { Switch project - {hasOtherOrgs && ( + {showOrgSwitcher && ( Date: Thu, 1 Oct 2026 20:12:05 +0000 Subject: [PATCH 05/11] chore(visual): update storybook baselines 6 updated, 4 removed Run: feb50236-da32-4eb9-abfc-f9b4d0f6a0c2 Co-authored-by: a-lider <221966567+a-lider@users.noreply.github.com> --- frontend/snapshots.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/snapshots.yml b/frontend/snapshots.yml index 9a2623194814..191c0f467ee8 100644 --- a/frontend/snapshots.yml +++ b/frontend/snapshots.yml @@ -124,18 +124,22 @@ snapshots: hash: v1.k794b7964.c18261f8a8a79a5e0902ddce51785b490a0989efa6795851640fc3652f196e80.YIgWf5MpUsQDOzGjlEXNDlIrEyg9GEBXU5HZn8A3GNA components-account-assignment-filter--unassigned-only--light: hash: v1.k794b7964.4144ad0cfd0b53bd92cb7360bc27c693863338b5d393d69ddd900c542bc6f382.nk0C9Ks32zxldf1ffLBSBoB2xP_PQEKlyBbBOiiNP1c - components-account-project-switcher--no-pending-invite--dark: - hash: v1.k794b7964.ddbb5d27187cb1eb9341734e0b780c762cdfbc09aae81833c970369bd7ad4f32.3HNFMfb4Lflcg3uKD1-KNl09d3leMhmfuhR2WVLjLRE - components-account-project-switcher--no-pending-invite--light: - hash: v1.k794b7964.3f8d66ba04469cdf3e92d78483c58125e84531888eedde628469fde5e9e39a08.MhOzPyXhl7CeOLA5Em19cktkdgffO_BL7CojWkngtMQ + components-account-org-switcher--no-pending-invite--dark: + hash: v1.k794b7964.4388a2607493bd36d9bc8960e2db7720df10102c5e2675bd7b5043f32519aed2.vrUvJ1500oMMFj1Gdc8hWdOaDEMfJRLhHNevwJW_ShQ + components-account-org-switcher--no-pending-invite--light: + hash: v1.k794b7964.db6c1002f011be274c4d274319f422724d8ef4b9c56f7f2f0f29ce23268b1f9b.G8SN1NyxYZSFc-HXe2Jhp1J-2szjpnPFrcuzD_UDLxw + components-account-org-switcher--with-pending-invite--dark: + hash: v1.k794b7964.7e7973392507a753037f4f094c2610038922d5f7c24f53cb34e5eb382847f834.byvdYZq4qgwYwUrcp82ijCWn4wDtvKVHr5qjj0LN3Os + components-account-org-switcher--with-pending-invite--light: + hash: v1.k794b7964.a9b3da4207480a66210bba2a464ba36ec9088faba11571c76a3836bb8ec3224e.pynJGDeHW4VhZJ_1FDVwCNbJc7rqGK17NwwSmkRqg2I + components-account-project-switcher--default--dark: + hash: v1.k794b7964.ddbb5d27187cb1eb9341734e0b780c762cdfbc09aae81833c970369bd7ad4f32.3WklDJAVVmzyrW0Qg1WBXMN0EamqUuc7HcYoGYBjMec + components-account-project-switcher--default--light: + hash: v1.k794b7964.3f8d66ba04469cdf3e92d78483c58125e84531888eedde628469fde5e9e39a08.yksh25-a4q1Q-SmXQSq_IMBe7LNW0d2waKZw_tDj0_A components-account-project-switcher--with-data-freshness--dark: hash: v1.k794b7964.33311041a0feedba12322f5c99b05b91db51c7f26793b837fce7c0666c5ef6f5.EPYhVoP42oDcDI9m3qloUEuvND0yjchkisUylND2Y-s components-account-project-switcher--with-data-freshness--light: hash: v1.k794b7964.825e1ea3745d1f526c46a4d7b365778ce2d3307933461503fca588f68916a3a9.6Bpu71WRfMaGy8N4X06ixaXTIYkj-4jg44eVXhgWkCw - components-account-project-switcher--with-pending-invite--dark: - hash: v1.k794b7964.b0c1c0b0b8d1a0c77cd873a1cc0b391924cd64ff7de46b1b4d309a50d98eca8a.ddWvJC0f1_fBpYhfDKtUrKeuZSrY5xR8ViHWVdiIfqk - components-account-project-switcher--with-pending-invite--light: - hash: v1.k794b7964.769ba5491bf515d4c18234cbb1b0965e4af8f190b5823914c8955194bb28d356.5QmxWM1jcZVZCAYm6oCJrI2XXnzoIHP19CyfFNvmiog components-action-step-description--step-variants--dark: hash: v1.k794b7964.19f9bee33652dbf2425e6df03ae732ed625840c249efc00589e0a1c0457b45d4.rVZ9Ek86XyAHV2eXwI_PDDTdDVv0QnEup4nMHmBjRKk components-action-step-description--step-variants--light: From 02bcd3c2a78a7628ba0a7f4c49ffa1ecc9e2cda9 Mon Sep 17 00:00:00 2001 From: Sandy Spicer Date: Thu, 1 Oct 2026 23:42:01 -0700 Subject: [PATCH 06/11] chore(today): pin the clock in the first-open briefing test The test opens the briefing once with a Prague timezone and once with the project's UTC, and expects the same briefing. A briefing day starts at eight local time, so between 06:00 and 08:00 UTC the two calls fall on different days and the test fails for every pull request. Co-Authored-By: Claude Fable 5.1 --- products/today/backend/tests/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/products/today/backend/tests/test_api.py b/products/today/backend/tests/test_api.py index c26f4074cd4c..9a067b85bf2e 100644 --- a/products/today/backend/tests/test_api.py +++ b/products/today/backend/tests/test_api.py @@ -47,7 +47,8 @@ def test_no_briefing_and_no_row_for_people_who_may_not_get_one( def test_first_open_creates_one_briefing_and_starts_generation_once(self, sync_connect: MagicMock) -> None: sync_connect.return_value.start_workflow = AsyncMock() - with self._flag(True): + # At noon UTC it is past eight in Prague and in the project's UTC, so both calls read the same briefing day. + with self._flag(True), time_machine.travel(datetime(2026, 9, 30, 12, 0, tzinfo=UTC), tick=False): first = self.client.get(f"/api/projects/{self.team.id}/today/briefing/?timezone=Europe/Prague") # An MCP call sends no timezone; it must not move the person's mornings to the project's. second = self.client.get(f"/api/projects/{self.team.id}/today/briefing/") From 4415adbb8f9e543005e486b48c5e6fb71b00702f Mon Sep 17 00:00:00 2001 From: pawel-cebula Date: Mon, 28 Sep 2026 10:38:01 +0200 Subject: [PATCH 07/11] fix(billing): default usage and spend to complete days --- .../engineering/ai/implementing-mcp-tools.md | 3 ++ ee/api/billing.py | 26 ++++++++-- ee/api/test/test_billing.py | 44 +++++++++++++++-- .../billing/frontend/generated/api.schemas.ts | 12 +++++ products/billing/mcp/tools.yaml | 32 ------------- services/mcp/src/api/generated.ts | 12 +++++ services/mcp/src/generated/billing/api.ts | 48 +++++++++++++++---- services/mcp/src/tools/generated/billing.ts | 24 ---------- .../tool-schemas/billing-spend-get.json | 4 +- .../tool-schemas/billing-usage-get.json | 4 +- 10 files changed, 132 insertions(+), 77 deletions(-) diff --git a/docs/published/handbook/engineering/ai/implementing-mcp-tools.md b/docs/published/handbook/engineering/ai/implementing-mcp-tools.md index 8ecf7e7a7906..8a1737a12682 100644 --- a/docs/published/handbook/engineering/ai/implementing-mcp-tools.md +++ b/docs/published/handbook/engineering/ai/implementing-mcp-tools.md @@ -142,6 +142,9 @@ a billing-specific tool wrapper. The billing usage/spend tools accept `usage_types` as an array of strings. Their field description lists the accepted identifiers from `ee/billing/billing_types.py`, through the generated API schema. The MCP client JSON-encodes the array for the HTTP API. +When both dates are omitted, the shared billing request serializer defaults usage/spend reads to the last 30 complete UTC days, ending yesterday. +Explicit date ranges are unchanged; a start date without an end date still ends today. +This also applies to the organization usage/spend time-series endpoints and CSV exports. The billing overview, usage, and spend tools do not need a rollout flag. API scopes and billing access checks still apply. diff --git a/ee/api/billing.py b/ee/api/billing.py index c24c3fd6e9e2..d619ab5bea7b 100644 --- a/ee/api/billing.py +++ b/ee/api/billing.py @@ -1,6 +1,7 @@ import re import json from collections.abc import Callable, Sequence +from datetime import timedelta from typing import Any, NoReturn, Optional, cast from zoneinfo import ZoneInfo @@ -351,8 +352,21 @@ class BillingUsageRequestSerializer(serializers.Serializer): Only responsible for parsing dates, passes through other params. """ - start_date = serializers.CharField(required=False, allow_blank=True, allow_null=True) - end_date = serializers.CharField(required=False, allow_blank=True, allow_null=True) + start_date = serializers.CharField( + required=False, + allow_blank=True, + allow_null=True, + help_text="Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.", + ) + end_date = serializers.CharField( + required=False, + allow_blank=True, + allow_null=True, + help_text=( + "End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, " + "or today if only start_date is provided." + ), + ) usage_types = serializers.CharField( required=False, allow_blank=True, @@ -436,8 +450,12 @@ def validate_end_date(self, value: Optional[str]) -> Optional[str]: return self._parse_date(value, "end_date") def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: - if attrs.get("start_date") and not attrs.get("end_date"): - attrs["end_date"] = timezone.now().date().isoformat() + today_utc = timezone.now().astimezone(ZoneInfo("UTC")).date() + if not attrs.get("start_date") and not attrs.get("end_date"): + attrs["start_date"] = (today_utc - timedelta(days=30)).isoformat() + attrs["end_date"] = (today_utc - timedelta(days=1)).isoformat() + elif attrs.get("start_date") and not attrs.get("end_date"): + attrs["end_date"] = today_utc.isoformat() return attrs def validate_usage_types(self, value: Optional[str]) -> Optional[str]: diff --git a/ee/api/test/test_billing.py b/ee/api/test/test_billing.py index 33b272d49d13..c40e8b811a03 100644 --- a/ee/api/test/test_billing.py +++ b/ee/api/test/test_billing.py @@ -1348,7 +1348,7 @@ def test_every_billing_action_is_partner_locked_or_explicitly_exempt(self) -> No assert unlocked == self.READ_ONLY_ACTIONS | self.UNLOCKED_WRITE_ACTIONS -class TestBillingUsageRequestSerializer(TestCase): +class TestBillingUsageRequestSerializer(SimpleTestCase): def test_valid_dates(self): serializer = BillingUsageRequestSerializer(data={"start_date": "2025-01-01", "end_date": "2025-01-31"}) self.assertTrue(serializer.is_valid(), serializer.errors) @@ -1423,11 +1423,30 @@ def test_accepts_every_breakdown_the_spend_read_serves(self, _case_name: str, va serializer = BillingUsageRequestSerializer(data={"breakdowns": value}) self.assertTrue(serializer.is_valid(), serializer.errors) - def test_empty_and_null_dates_are_valid(self): - serializer = BillingUsageRequestSerializer(data={"start_date": "", "end_date": None}) + @parameterized.expand( + [ + ("missing", {}), + ("empty", {"start_date": "", "end_date": ""}), + ("null", {"start_date": None, "end_date": None}), + ("empty_start", {"start_date": ""}), + ("null_end", {"end_date": None}), + ] + ) + @time_machine.travel("2025-02-15T00:30:00+14:00", tick=False) + def test_missing_empty_and_null_dates_default_to_last_30_complete_utc_days( + self, _case_name: str, data: dict[str, str | None] + ) -> None: + serializer = BillingUsageRequestSerializer(data=data) + self.assertTrue(serializer.is_valid(), serializer.errors) + self.assertEqual(serializer.validated_data["start_date"], "2025-01-15") + self.assertEqual(serializer.validated_data["end_date"], "2025-02-13") + + @time_machine.travel("2025-02-15", tick=False) + def test_end_date_without_start_date_is_preserved(self) -> None: + serializer = BillingUsageRequestSerializer(data={"end_date": "2025-02-14"}) self.assertTrue(serializer.is_valid(), serializer.errors) - self.assertIsNone(serializer.validated_data.get("start_date")) - self.assertIsNone(serializer.validated_data.get("end_date")) + self.assertNotIn("start_date", serializer.validated_data) + self.assertEqual(serializer.validated_data["end_date"], "2025-02-14") class TestBillingUpstreamValidationErrors(SimpleTestCase): @@ -1617,6 +1636,21 @@ def test_get_usage_success(self, mock_get_usage_data): # No teams_map: names are put into the response on the way out. self.assertNotIn("teams_map", passed_params) + @parameterized.expand([("usage",), ("spend",)]) + @time_machine.travel("2025-02-15T00:30:00+14:00", tick=False) + def test_usage_and_spend_default_date_range_is_sent_to_billing(self, endpoint: str) -> None: + manager_method = f"ee.billing.billing_manager.BillingManager.get_{endpoint}_data" + mock_data = self.MOCK_USAGE_DATA if endpoint == "usage" else self.MOCK_SPEND_DATA + + with patch(manager_method, return_value=mock_data) as mock_fetch: + response = self.client.get(f"/api/billing/{endpoint}/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + mock_fetch.assert_called_once() + passed_params = mock_fetch.call_args[0][1] + self.assertEqual(passed_params["start_date"], "2025-01-15") + self.assertEqual(passed_params["end_date"], "2025-02-13") + @staticmethod def _billing_refusal(upstream_status: int, body: object) -> Exception: # The shape handle_billing_service_error raises: the status in the message, the parsed body third. diff --git a/products/billing/frontend/generated/api.schemas.ts b/products/billing/frontend/generated/api.schemas.ts index 8405c01d11a9..435c10dbec3b 100644 --- a/products/billing/frontend/generated/api.schemas.ts +++ b/products/billing/frontend/generated/api.schemas.ts @@ -1262,6 +1262,7 @@ export type BillingSpendRetrieveParams = { */ breakdowns?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1277,6 +1278,7 @@ export type BillingSpendRetrieveParams = { */ page_size?: number | null /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1312,6 +1314,7 @@ export type BillingSpendExportRetrieveParams = { */ breakdowns?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1327,6 +1330,7 @@ export type BillingSpendExportRetrieveParams = { */ page_size?: number | null /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1362,6 +1366,7 @@ export type BillingUsageRetrieveParams = { */ breakdowns?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1377,6 +1382,7 @@ export type BillingUsageRetrieveParams = { */ page_size?: number | null /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1412,6 +1418,7 @@ export type BillingUsageExportRetrieveParams = { */ breakdowns?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1427,6 +1434,7 @@ export type BillingUsageExportRetrieveParams = { */ page_size?: number | null /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1568,6 +1576,7 @@ export type BillingSpendTimeseriesRetrieveParams = { */ cursor?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1583,6 +1592,7 @@ export type BillingSpendTimeseriesRetrieveParams = { */ limit?: number | null /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1655,6 +1665,7 @@ export type BillingUsageTimeseriesRetrieveParams = { */ cursor?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1670,6 +1681,7 @@ export type BillingUsageTimeseriesRetrieveParams = { */ limit?: number | null /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null diff --git a/products/billing/mcp/tools.yaml b/products/billing/mcp/tools.yaml index ebef462cd846..a2703c7c3cb6 100644 --- a/products/billing/mcp/tools.yaml +++ b/products/billing/mcp/tools.yaml @@ -264,14 +264,6 @@ tools: grant applies only to usage/spend, not billing-overview-get or billing admin actions. Use this to investigate cost spikes, compare spend across periods, or attribute spend to specific products or teams. param_overrides: - start_date: - description: > - Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the - last 30 days. If you use "all", also pass end_date. - end_date: - description: > - End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user - did not name one. team_ids: input_schema: BillingTeamIdsSchema usage_types: @@ -333,14 +325,6 @@ tools: billing-usage-timeseries-get. Beta: behind the organization-billing-api flag, and the output shape may change while it is. param_overrides: - start_date: - description: > - Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the - last 30 days. If you use "all", also pass end_date. - end_date: - description: > - End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user - did not name one. team_ids: description: > JSON-encoded array of numeric team (project) IDs to filter by, NOT a comma-separated string. Pass as @@ -432,14 +416,6 @@ tools: projects. For a single-snapshot aggregate view call billing-overview-get when the caller has full billing access; for cost (spend) breakdowns call billing-spend-get. param_overrides: - start_date: - description: > - Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the - last 30 days. If you use "all", also pass end_date. - end_date: - description: > - End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user - did not name one. team_ids: input_schema: BillingTeamIdsSchema usage_types: @@ -535,14 +511,6 @@ tools: billing-spend-timeseries-get. Beta: behind the organization-billing-api flag, and the output shape may change while it is. param_overrides: - start_date: - description: > - Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the - last 30 days. If you use "all", also pass end_date. - end_date: - description: > - End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user - did not name one. team_ids: description: > JSON-encoded array of numeric team (project) IDs to filter by, NOT a comma-separated string. Pass as diff --git a/services/mcp/src/api/generated.ts b/services/mcp/src/api/generated.ts index 31c210d14587..b04c71e7c156 100644 --- a/services/mcp/src/api/generated.ts +++ b/services/mcp/src/api/generated.ts @@ -109857,6 +109857,7 @@ export namespace Schemas { */ breakdowns?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -109872,6 +109873,7 @@ export namespace Schemas { */ page_size?: number | null; /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -109907,6 +109909,7 @@ export namespace Schemas { */ breakdowns?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -109922,6 +109925,7 @@ export namespace Schemas { */ page_size?: number | null; /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -109957,6 +109961,7 @@ export namespace Schemas { */ breakdowns?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -109972,6 +109977,7 @@ export namespace Schemas { */ page_size?: number | null; /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110007,6 +110013,7 @@ export namespace Schemas { */ breakdowns?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -110022,6 +110029,7 @@ export namespace Schemas { */ page_size?: number | null; /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110483,6 +110491,7 @@ export namespace Schemas { */ cursor?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -110498,6 +110507,7 @@ export namespace Schemas { */ limit?: number | null; /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110570,6 +110580,7 @@ export namespace Schemas { */ cursor?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -110585,6 +110596,7 @@ export namespace Schemas { */ limit?: number | null; /** + * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; diff --git a/services/mcp/src/generated/billing/api.ts b/services/mcp/src/generated/billing/api.ts index 39b522660834..b2c2fe3ace83 100644 --- a/services/mcp/src/generated/billing/api.ts +++ b/services/mcp/src/generated/billing/api.ts @@ -29,7 +29,12 @@ export const BillingSpendRetrieveQueryParams = () => zod.object({ .describe( 'JSON-encoded array of breakdown dimensions. Valid values are \"type\" and \"team\", for example [\"type\",\"team\"]. Omit for a single aggregate series.' ), - end_date: zod.string().nullish(), + end_date: zod + .string() + .nullish() + .describe( + 'End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided.' + ), interval: zod.string().nullish(), page_size: zod .number() @@ -39,7 +44,10 @@ export const BillingSpendRetrieveQueryParams = () => zod.object({ .describe( 'Return at most this many series, ranked by total, with a `next` cursor for the page after. A caller that pages never approaches the size this endpoint refuses oversized breakdowns at. Requires a project breakdown.' ), - start_date: zod.string().nullish(), + start_date: zod + .string() + .nullish() + .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), team_ids: zod .string() .nullish() @@ -80,7 +88,12 @@ export const BillingUsageRetrieveQueryParams = () => zod.object({ .describe( 'JSON-encoded array of breakdown dimensions. Valid values are \"type\" and \"team\", for example [\"type\",\"team\"]. Omit for a single aggregate series.' ), - end_date: zod.string().nullish(), + end_date: zod + .string() + .nullish() + .describe( + 'End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided.' + ), interval: zod.string().nullish(), page_size: zod .number() @@ -90,7 +103,10 @@ export const BillingUsageRetrieveQueryParams = () => zod.object({ .describe( 'Return at most this many series, ranked by total, with a `next` cursor for the page after. A caller that pages never approaches the size this endpoint refuses oversized breakdowns at. Requires a project breakdown.' ), - start_date: zod.string().nullish(), + start_date: zod + .string() + .nullish() + .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), team_ids: zod .string() .nullish() @@ -237,7 +253,12 @@ export const BillingSpendTimeseriesRetrieveQueryParams = () => zod.object({ .max(billingSpendTimeseriesRetrieveQueryCursorMax) .nullish() .describe("The cursor from a previous page's `next` link. Opaque. Ignored without `limit`."), - end_date: zod.string().nullish(), + end_date: zod + .string() + .nullish() + .describe( + 'End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided.' + ), interval: zod.string().nullish(), limit: zod .number() @@ -247,7 +268,10 @@ export const BillingSpendTimeseriesRetrieveQueryParams = () => zod.object({ .describe( 'Series per page, ranked by total, with a `next` link for the page after. Requires a project breakdown and is ignored without one. Omit it to get every series at once.' ), - start_date: zod.string().nullish(), + start_date: zod + .string() + .nullish() + .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), team_ids: zod .string() .nullish() @@ -336,7 +360,12 @@ export const BillingUsageTimeseriesRetrieveQueryParams = () => zod.object({ .max(billingUsageTimeseriesRetrieveQueryCursorMax) .nullish() .describe("The cursor from a previous page's `next` link. Opaque. Ignored without `limit`."), - end_date: zod.string().nullish(), + end_date: zod + .string() + .nullish() + .describe( + 'End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided.' + ), interval: zod.string().nullish(), limit: zod .number() @@ -346,7 +375,10 @@ export const BillingUsageTimeseriesRetrieveQueryParams = () => zod.object({ .describe( 'Series per page, ranked by total, with a `next` link for the page after. Requires a project breakdown and is ignored without one. Omit it to get every series at once.' ), - start_date: zod.string().nullish(), + start_date: zod + .string() + .nullish() + .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), team_ids: zod .string() .nullish() diff --git a/services/mcp/src/tools/generated/billing.ts b/services/mcp/src/tools/generated/billing.ts index 207b00a786de..5bc8f2507b18 100644 --- a/services/mcp/src/tools/generated/billing.ts +++ b/services/mcp/src/tools/generated/billing.ts @@ -170,12 +170,6 @@ const billingProjectsList = (): ToolBase< const BillingSpendGetSchema = () => { const BillingSpendRetrieveQueryParams = orvalSchemas.BillingSpendRetrieveQueryParams() return BillingSpendRetrieveQueryParams.extend({ - start_date: BillingSpendRetrieveQueryParams.shape['start_date'].describe( - 'Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the last 30 days. If you use "all", also pass end_date.' - ), - end_date: BillingSpendRetrieveQueryParams.shape['end_date'].describe( - "End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user did not name one." - ), team_ids: BillingTeamIdsSchema, usage_types: BillingUsageTypesSchema, breakdowns: BillingSpendBreakdownsSchema, @@ -239,12 +233,6 @@ const billingSpendSummaryGet = (): ToolBase< const BillingSpendTimeseriesGetSchema = () => { const BillingSpendTimeseriesRetrieveQueryParams = orvalSchemas.BillingSpendTimeseriesRetrieveQueryParams() return BillingSpendTimeseriesRetrieveQueryParams.extend({ - start_date: BillingSpendTimeseriesRetrieveQueryParams.shape['start_date'].describe( - 'Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the last 30 days. If you use "all", also pass end_date.' - ), - end_date: BillingSpendTimeseriesRetrieveQueryParams.shape['end_date'].describe( - "End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user did not name one." - ), team_ids: BillingSpendTimeseriesRetrieveQueryParams.shape['team_ids'].describe( "JSON-encoded array of numeric team (project) IDs to filter by, NOT a comma-separated string. Pass as e.g. `[1,2]`. Omit for every project this request can see: all org teams for full billing-access callers, or the member's visible/project-scoped teams for member read-only callers." ), @@ -315,12 +303,6 @@ const billingSubscriptionGet = (): ToolBase< const BillingUsageGetSchema = () => { const BillingUsageRetrieveQueryParams = orvalSchemas.BillingUsageRetrieveQueryParams() return BillingUsageRetrieveQueryParams.extend({ - start_date: BillingUsageRetrieveQueryParams.shape['start_date'].describe( - 'Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the last 30 days. If you use "all", also pass end_date.' - ), - end_date: BillingUsageRetrieveQueryParams.shape['end_date'].describe( - "End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user did not name one." - ), team_ids: BillingTeamIdsSchema, usage_types: BillingUsageTypesSchema, breakdowns: BillingUsageBreakdownsSchema, @@ -408,12 +390,6 @@ const billingUsageSummaryGet = (): ToolBase< const BillingUsageTimeseriesGetSchema = () => { const BillingUsageTimeseriesRetrieveQueryParams = orvalSchemas.BillingUsageTimeseriesRetrieveQueryParams() return BillingUsageTimeseriesRetrieveQueryParams.extend({ - start_date: BillingUsageTimeseriesRetrieveQueryParams.shape['start_date'].describe( - 'Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the last 30 days. If you use "all", also pass end_date.' - ), - end_date: BillingUsageTimeseriesRetrieveQueryParams.shape['end_date'].describe( - "End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user did not name one." - ), team_ids: BillingUsageTimeseriesRetrieveQueryParams.shape['team_ids'].describe( "JSON-encoded array of numeric team (project) IDs to filter by, NOT a comma-separated string. Pass as e.g. `[1,2]`. Omit for every project this request can see: all org teams for full billing-access callers, or the member's visible/project-scoped teams for member read-only callers." ), diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json index 2f16f5da6410..8e17e94015bd 100644 --- a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json @@ -37,7 +37,7 @@ "type": "null" } ], - "description": "End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user did not name one." + "description": "End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided." }, "interval": { "anyOf": [ @@ -72,7 +72,7 @@ "type": "null" } ], - "description": "Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the last 30 days. If you use \"all\", also pass end_date." + "description": "Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago." }, "team_ids": { "anyOf": [ diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json index 3266a9951df0..d25dd6b6a3fa 100644 --- a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json @@ -37,7 +37,7 @@ "type": "null" } ], - "description": "End date (YYYY-MM-DD), inclusive. Pass this whenever start_date is set; use today's date if the user did not name one." + "description": "End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided." }, "interval": { "anyOf": [ @@ -72,7 +72,7 @@ "type": "null" } ], - "description": "Start date (YYYY-MM-DD). For open-ended investigations, choose an explicit recent window such as the last 30 days. If you use \"all\", also pass end_date." + "description": "Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago." }, "team_ids": { "anyOf": [ From f5dd0d3ce8567f056444e6bbf974b84872b18900 Mon Sep 17 00:00:00 2001 From: pawel-cebula Date: Mon, 28 Sep 2026 11:22:44 +0200 Subject: [PATCH 08/11] fix(billing): restore all-date guidance in tool schemas --- ee/api/billing.py | 4 +++- .../billing/frontend/generated/api.schemas.ts | 12 ++++++------ services/mcp/src/api/generated.ts | 12 ++++++------ services/mcp/src/generated/billing/api.ts | 16 ++++++++++++---- .../tool-schemas/billing-spend-get.json | 2 +- .../tool-schemas/billing-usage-get.json | 2 +- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/ee/api/billing.py b/ee/api/billing.py index d619ab5bea7b..d1aabb45b28c 100644 --- a/ee/api/billing.py +++ b/ee/api/billing.py @@ -356,7 +356,9 @@ class BillingUsageRequestSerializer(serializers.Serializer): required=False, allow_blank=True, allow_null=True, - help_text="Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.", + help_text=( + 'Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago.' + ), ) end_date = serializers.CharField( required=False, diff --git a/products/billing/frontend/generated/api.schemas.ts b/products/billing/frontend/generated/api.schemas.ts index 435c10dbec3b..6a018e169c7e 100644 --- a/products/billing/frontend/generated/api.schemas.ts +++ b/products/billing/frontend/generated/api.schemas.ts @@ -1278,7 +1278,7 @@ export type BillingSpendRetrieveParams = { */ page_size?: number | null /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1330,7 +1330,7 @@ export type BillingSpendExportRetrieveParams = { */ page_size?: number | null /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1382,7 +1382,7 @@ export type BillingUsageRetrieveParams = { */ page_size?: number | null /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1434,7 +1434,7 @@ export type BillingUsageExportRetrieveParams = { */ page_size?: number | null /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1592,7 +1592,7 @@ export type BillingSpendTimeseriesRetrieveParams = { */ limit?: number | null /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1681,7 +1681,7 @@ export type BillingUsageTimeseriesRetrieveParams = { */ limit?: number | null /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null diff --git a/services/mcp/src/api/generated.ts b/services/mcp/src/api/generated.ts index b04c71e7c156..6fca42a84161 100644 --- a/services/mcp/src/api/generated.ts +++ b/services/mcp/src/api/generated.ts @@ -109873,7 +109873,7 @@ export namespace Schemas { */ page_size?: number | null; /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -109925,7 +109925,7 @@ export namespace Schemas { */ page_size?: number | null; /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -109977,7 +109977,7 @@ export namespace Schemas { */ page_size?: number | null; /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110029,7 +110029,7 @@ export namespace Schemas { */ page_size?: number | null; /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110507,7 +110507,7 @@ export namespace Schemas { */ limit?: number | null; /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110596,7 +110596,7 @@ export namespace Schemas { */ limit?: number | null; /** - * Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago. + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; diff --git a/services/mcp/src/generated/billing/api.ts b/services/mcp/src/generated/billing/api.ts index b2c2fe3ace83..e20d71de48ce 100644 --- a/services/mcp/src/generated/billing/api.ts +++ b/services/mcp/src/generated/billing/api.ts @@ -47,7 +47,9 @@ export const BillingSpendRetrieveQueryParams = () => zod.object({ start_date: zod .string() .nullish() - .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), + .describe( + 'Start date (YYYY-MM-DD, UTC), or \"all\" for 2020-01-01. If both dates are omitted, defaults to 30 days ago.' + ), team_ids: zod .string() .nullish() @@ -106,7 +108,9 @@ export const BillingUsageRetrieveQueryParams = () => zod.object({ start_date: zod .string() .nullish() - .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), + .describe( + 'Start date (YYYY-MM-DD, UTC), or \"all\" for 2020-01-01. If both dates are omitted, defaults to 30 days ago.' + ), team_ids: zod .string() .nullish() @@ -271,7 +275,9 @@ export const BillingSpendTimeseriesRetrieveQueryParams = () => zod.object({ start_date: zod .string() .nullish() - .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), + .describe( + 'Start date (YYYY-MM-DD, UTC), or \"all\" for 2020-01-01. If both dates are omitted, defaults to 30 days ago.' + ), team_ids: zod .string() .nullish() @@ -378,7 +384,9 @@ export const BillingUsageTimeseriesRetrieveQueryParams = () => zod.object({ start_date: zod .string() .nullish() - .describe('Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago.'), + .describe( + 'Start date (YYYY-MM-DD, UTC), or \"all\" for 2020-01-01. If both dates are omitted, defaults to 30 days ago.' + ), team_ids: zod .string() .nullish() diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json index 8e17e94015bd..f8a5cc9e0729 100644 --- a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-spend-get.json @@ -72,7 +72,7 @@ "type": "null" } ], - "description": "Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago." + "description": "Start date (YYYY-MM-DD, UTC), or \"all\" for 2020-01-01. If both dates are omitted, defaults to 30 days ago." }, "team_ids": { "anyOf": [ diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json index d25dd6b6a3fa..06f53e9652e3 100644 --- a/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/billing-usage-get.json @@ -72,7 +72,7 @@ "type": "null" } ], - "description": "Start date (YYYY-MM-DD, UTC). If both dates are omitted, defaults to 30 days ago." + "description": "Start date (YYYY-MM-DD, UTC), or \"all\" for 2020-01-01. If both dates are omitted, defaults to 30 days ago." }, "team_ids": { "anyOf": [ From fb3261b472eb34c8f03c918c88107ac2d1833d5b Mon Sep 17 00:00:00 2001 From: "scheduled-actions-posthog[bot]" <250428249+scheduled-actions-posthog[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:48:48 +0000 Subject: [PATCH 09/11] chore(tasks): bump sandbox @posthog/agent to 2.4.253 --- products/tasks/backend/sandbox/images/Dockerfile.sandbox-base | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/products/tasks/backend/sandbox/images/Dockerfile.sandbox-base b/products/tasks/backend/sandbox/images/Dockerfile.sandbox-base index 5ecd4c08dfbb..86ce0a1dcc04 100644 --- a/products/tasks/backend/sandbox/images/Dockerfile.sandbox-base +++ b/products/tasks/backend/sandbox/images/Dockerfile.sandbox-base @@ -161,8 +161,8 @@ RUN set -eux; \ # every sandbox image with no PostHog commit or review. Bump this deliberately # in a reviewed PR when picking up a new agent release. COMMIT_HASH invalidates # the layer for PostHog-side image changes that keep the agent version unchanged. -ARG AGENT_VERSION=2.4.252 -ARG AGENT_TARBALL_SHA256="18e3c7c3afa46519f35df3f3248823c94df28ee1349acbde5af5a85c0bf368b2" +ARG AGENT_VERSION=2.4.253 +ARG AGENT_TARBALL_SHA256="190a7488288f0579eaadd7ff10fe5270c4f9982219568711c5dabec2d1dd0f5b" ARG AGENT_TARBALL_URL=https://github.com/PostHog/posthog/releases/download/agent-v${AGENT_VERSION}/posthog-agent-${AGENT_VERSION}.tgz ARG COMMIT_HASH RUN mkdir -p /scripts && \ From 5b5a95b3b43d4dd4a59b999a0408d72e25008e1d Mon Sep 17 00:00:00 2001 From: Sandy Spicer Date: Thu, 1 Oct 2026 23:48:50 -0700 Subject: [PATCH 10/11] test(today): cover a call without a timezone on a different briefing day Between 06:00 and 08:00 UTC a call without a timezone reads the project's briefing day, which is the day before for a person in Prague. The day boundary test now asserts that at a pinned time. Co-Authored-By: Claude Fable 5.1 --- products/today/backend/tests/test_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/products/today/backend/tests/test_api.py b/products/today/backend/tests/test_api.py index 9a067b85bf2e..90dcb806f399 100644 --- a/products/today/backend/tests/test_api.py +++ b/products/today/backend/tests/test_api.py @@ -88,10 +88,13 @@ def test_the_briefing_day_starts_at_eight(self, sync_connect: MagicMock) -> None early = self.client.get(url).json() with time_machine.travel(datetime(2026, 9, 30, 7, 0, tzinfo=UTC), tick=False): today = self.client.get(url).json() + # A call without a timezone uses the project's UTC, where it is still before eight. + without_timezone = self.client.get(f"/api/projects/{self.team.id}/today/briefing/").json() assert early["local_day"] == "2026-09-29" assert today["local_day"] == "2026-09-30" assert early["id"] != today["id"] + assert without_timezone["id"] == early["id"] assert sync_connect.return_value.start_workflow.call_count == 2 def test_a_refresh_while_one_is_being_written_starts_nothing_new(self, sync_connect: MagicMock) -> None: From d4a8664d7001b74acadb557b5b11eb7e023ee0eb Mon Sep 17 00:00:00 2001 From: pawel-cebula Date: Fri, 2 Oct 2026 08:51:52 +0200 Subject: [PATCH 11/11] chore(billing): regenerate export date guidance after rebase --- products/billing/frontend/generated/api.schemas.ts | 4 ++++ services/mcp/src/api/generated.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/products/billing/frontend/generated/api.schemas.ts b/products/billing/frontend/generated/api.schemas.ts index 6a018e169c7e..f023453c7ee5 100644 --- a/products/billing/frontend/generated/api.schemas.ts +++ b/products/billing/frontend/generated/api.schemas.ts @@ -1533,6 +1533,7 @@ export type BillingSpendExportDownloadParams = { */ breakdowns?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1541,6 +1542,7 @@ export type BillingSpendExportDownloadParams = { */ interval?: string | null /** + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null @@ -1622,6 +1624,7 @@ export type BillingUsageExportDownloadParams = { */ breakdowns?: string | null /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null @@ -1630,6 +1633,7 @@ export type BillingUsageExportDownloadParams = { */ interval?: string | null /** + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null diff --git a/services/mcp/src/api/generated.ts b/services/mcp/src/api/generated.ts index 6fca42a84161..36c91ae9687e 100644 --- a/services/mcp/src/api/generated.ts +++ b/services/mcp/src/api/generated.ts @@ -110448,6 +110448,7 @@ export namespace Schemas { */ breakdowns?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -110456,6 +110457,7 @@ export namespace Schemas { */ interval?: string | null; /** + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null; @@ -110537,6 +110539,7 @@ export namespace Schemas { */ breakdowns?: string | null; /** + * End date (YYYY-MM-DD, UTC), inclusive. Defaults to yesterday if both dates are omitted, or today if only start_date is provided. * @nullable */ end_date?: string | null; @@ -110545,6 +110548,7 @@ export namespace Schemas { */ interval?: string | null; /** + * Start date (YYYY-MM-DD, UTC), or "all" for 2020-01-01. If both dates are omitted, defaults to 30 days ago. * @nullable */ start_date?: string | null;