Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1cd90dc
feat(cost-insights) planning
jeanduplessis Jun 25, 2026
832786b
feat(cost-insights): add page routes
jeanduplessis Jun 25, 2026
783059c
docs(cost-insights): clarify suggestion examples
jeanduplessis Jun 25, 2026
9a4f874
feat(cost-insights): add spend rollup data layer
jeanduplessis Jun 25, 2026
be830c4
feat(cost-insight): implement cost insights UI
jeanduplessis Jun 25, 2026
05d0083
fix(cost-insights): address review findings
jeanduplessis Jun 26, 2026
ff1f1fc
feat(cost-insights): show review item counts
jeanduplessis Jun 26, 2026
dfb7fcf
style(sidebar): use mono review count badge
jeanduplessis Jun 26, 2026
69bcd6d
feat(cost-insights): add admin rollout and multi-window alerts
jeanduplessis Jun 26, 2026
db622a3
fix(cost-insights): address PR review feedback
jeanduplessis Jun 26, 2026
9d6091e
fix(cost-insights): harden hourly evaluation
jeanduplessis Jun 26, 2026
9877d9d
fix(cost-insights): reduce evaluation database impact
jeanduplessis Jul 2, 2026
dd8d4e9
fix(cost-insights): hide ask kilo entrypoints
jeanduplessis Jul 2, 2026
81fd754
fix(db): regenerate cost insights migration
jeanduplessis Jul 2, 2026
1b1530d
fix(cost-insights): polish seeded dashboard state
jeanduplessis Jul 3, 2026
31e238c
feat(cost-insights): refine spend evidence presentation
jeanduplessis Jul 3, 2026
56b60a1
refactor(cost-insights): align alert driver rows
jeanduplessis Jul 3, 2026
5f4ddc4
chore(plans): remove cost insights planning docs
jeanduplessis Jul 3, 2026
3d90cd2
fix(cost-insights): resolve review follow-ups
jeanduplessis Jul 3, 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
252 changes: 252 additions & 0 deletions .specs/cost-insights.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Business-rule specs live in `.specs/`. Before making **any** changes to a domain
| `.specs/kiloclaw-controller.md` | KiloClaw controller/machine lifecycle, bootstrap, Docker image |
| `.specs/kiloclaw-datamodel.md` | KiloClaw data model — instance/subscription tables, invariants |
| `.specs/model-experiments.md` | Model experiment routing, bucketing, lifecycle, prompt retention, and reporting rules |
| `.specs/cost-insights.md` | Cost Insights and Spend Alerts owner scope, anomaly alerts, threshold alerts, and alert acknowledgments |
| `.specs/security-agent.md` | Security Agent Auto Remediation and finding/SLA notification guarantees |
| `.specs/subscription-center.md` | Subscription Center ownership, states, and user-facing behavior |
| `.specs/team-enterprise-seat-billing.md` | Team and Enterprise seat billing, subscription management |
Expand Down
117 changes: 116 additions & 1 deletion CONTEXT.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions apps/storybook/stories/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Building2,
Cable,
ChartColumnIncreasing,
ChartLine,
ChevronLeft,
ChevronRight,
Cloud,
Expand Down Expand Up @@ -109,6 +110,11 @@ const dashboardItems: SidebarStoryItem[] = [
icon: ChartColumnIncreasing,
url: '/usage',
},
{
title: 'Cost Insights',
icon: ChartLine,
url: '/cost-insights',
},
];

const kiloClawItems: SidebarStoryItem[] = [
Expand Down
28 changes: 28 additions & 0 deletions apps/storybook/stories/cost-insights/AskKilo.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/nextjs';
import { CostInsightsAskKiloView, CostInsightsShellView } from '@/components/cost-insights';
import { personalOwner } from './costInsightsFixtures';

const meta: Meta<typeof CostInsightsAskKiloView> = {
title: 'Cost Insights/Ask Kilo',
component: CostInsightsAskKiloView,
parameters: { layout: 'fullscreen' },
};

export default meta;
type Story = StoryObj<typeof CostInsightsAskKiloView>;

function AskKiloStory({ initialQuestion }: { initialQuestion?: string }) {
return (
<CostInsightsShellView owner={personalOwner} activePage="ask">
<CostInsightsAskKiloView initialQuestion={initialQuestion} />
</CostInsightsShellView>
);
}

export const DisabledPreview: Story = {
render: () => <AskKiloStory />,
};

export const DisabledPreviewWithQuestion: Story = {
render: () => <AskKiloStory initialQuestion="Show my spend for the last 30 days" />,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/nextjs';
import { CostInsightsAlertBar } from '@/components/cost-insights';
import { organizationOwner } from './costInsightsFixtures';

const meta = {
title: 'Cost Insights/In-App Alert Bar',
component: CostInsightsAlertBar,
parameters: { layout: 'fullscreen' },
args: {
owner: organizationOwner,
alertCount: 2,
reviewHref: '/organizations/4f2fc143-4b30-4c8a-878b-df89c89c6790/cost-insights',
},
decorators: [
Story => (
<div className="bg-background min-h-screen">
<header className="border-border bg-surface-raised flex h-14 items-center border-b px-4 type-body font-semibold md:px-6">
Kilo Cloud
</header>
<Story />
<main className="mx-auto max-w-[1140px] p-4 md:p-6">
<div className="type-heading">Account overview</div>
</main>
</div>
),
],
} satisfies Meta<typeof CostInsightsAlertBar>;

export default meta;
type Story = StoryObj<typeof meta>;

export const AlertsNeedReview: Story = {};
129 changes: 129 additions & 0 deletions apps/storybook/stories/cost-insights/EventHistory.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/nextjs';
import {
CostInsightsEventHistoryView,
CostInsightsShellView,
type ActivityFilter,
type CostInsightsOwner,
type CostInsightEvent,
} from '@/components/cost-insights';
import {
allEvents,
longLabelEvents,
organizationOwner,
personalOwner,
threshold7DayEvent,
} from './costInsightsFixtures';

const paginatedEvents = Array.from({ length: 23 }, (_, index): CostInsightEvent => {
const event = allEvents[index % allEvents.length];
if (!event) throw new Error('Activity fixture requires at least one event');
return {
...event,
id: `${event.id}-${index}`,
occurredAt:
index < 5
? event.occurredAt
: new Date(
new Date(event.occurredAt).getTime() - (Math.floor(index / 5) + 1) * 24 * 60 * 60 * 1000
).toISOString(),
};
});

const meta: Meta<typeof CostInsightsEventHistoryView> = {
title: 'Cost Insights/Activity',
component: CostInsightsEventHistoryView,
parameters: { layout: 'fullscreen' },
};

export default meta;
type Story = StoryObj<typeof CostInsightsEventHistoryView>;

function ActivityStory({
events,
owner,
empty,
}: {
events: CostInsightEvent[];
owner: CostInsightsOwner;
empty: boolean;
}) {
const [filter, setFilter] = useState<ActivityFilter>('all');
const [page, setPage] = useState(1);
const filteredEvents = events.filter(event => {
if (filter === 'alerts') return ['anomaly_alert', 'threshold_crossed'].includes(event.type);
if (filter === 'suggestions')
return ['suggestion_created', 'suggestion_dismissed'].includes(event.type);
if (filter === 'reviews') return event.type === 'reviewed';
if (filter === 'settings') return ['config_changed', 'disabled'].includes(event.type);
return true;
});
const pageCount = Math.max(1, Math.ceil(filteredEvents.length / 10));
const currentPage = Math.min(page, pageCount);
const pageEvents = filteredEvents.slice((currentPage - 1) * 10, currentPage * 10);
const basePath =
owner.type === 'organization'
? '/organizations/acme-cost-insights/cost-insights'
: '/cost-insights';

return (
<CostInsightsShellView owner={owner} activePage="events" basePath={basePath}>
<CostInsightsEventHistoryView
events={pageEvents}
empty={empty}
filter={filter}
page={currentPage}
pageCount={pageCount}
totalCount={filteredEvents.length}
onFilterChange={nextFilter => {
setFilter(nextFilter);
setPage(1);
}}
onPageChange={setPage}
/>
</CostInsightsShellView>
);
}

function renderActivity(
events: CostInsightEvent[],
owner: CostInsightsOwner = personalOwner,
empty = false
) {
return <ActivityStory events={events} owner={owner} empty={empty} />;
}

export const ActivityHistory: Story = {
render: () => renderActivity(paginatedEvents, organizationOwner),
};

export const SevenDayThresholdActivity: Story = {
render: () => renderActivity([threshold7DayEvent], organizationOwner),
};

export const Empty: Story = {
render: () => renderActivity([], personalOwner, true),
};

export const Loading: Story = {
render: () => (
<CostInsightsShellView owner={personalOwner} activePage="events">
<CostInsightsEventHistoryView events={[]} isLoading />
</CostInsightsShellView>
),
};

export const LoadError: Story = {
render: () => (
<CostInsightsShellView owner={personalOwner} activePage="events">
<CostInsightsEventHistoryView events={[]} isError />
</CostInsightsShellView>
),
};

export const Mobile: Story = {
render: () => renderActivity(longLabelEvents, organizationOwner),
globals: {
viewport: { value: 'mobile2', isRotated: false },
},
};
Loading
Loading