Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions backend/__tests__/unit/routes/stats.public.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ const express = require('express');

const mockPgQuery = jest.fn();
const mockMessageCountDocuments = jest.fn().mockResolvedValue(88);
const mockUserCountDocuments = jest.fn().mockResolvedValue(262);

jest.mock('../../../models/Pod', () => ({
countDocuments: jest.fn().mockResolvedValue(12),
}));
jest.mock('../../../models/User', () => ({
countDocuments: jest.fn().mockResolvedValue(42),
countDocuments: mockUserCountDocuments,
}));
jest.mock('../../../models/Message', () => ({
countDocuments: mockMessageCountDocuments,
Expand Down Expand Up @@ -42,13 +43,19 @@ describe('GET /api/stats/public', () => {
activePods: 12,
activeAgents: 3,
messageCount24h: 1234,
registeredUsers: 42,
agentCount: 262,
});
expect(mockPgQuery).toHaveBeenCalledWith(
'SELECT COUNT(*)::int AS count FROM messages WHERE created_at >= $1',
[expect.any(Date)],
);
expect(mockMessageCountDocuments).not.toHaveBeenCalled();
expect(mockUserCountDocuments).toHaveBeenCalledWith({
'botMetadata.agentName': { $exists: true },
});
expect(mockUserCountDocuments).toHaveBeenCalledTimes(1);
expect(res.body).not.toHaveProperty('humanCount');
expect(res.body).not.toHaveProperty('registeredUsers');
});

it('falls back to MongoDB when the PostgreSQL count fails', async () => {
Expand All @@ -61,7 +68,7 @@ describe('GET /api/stats/public', () => {
activePods: 12,
activeAgents: 3,
messageCount24h: 88,
registeredUsers: 42,
agentCount: 262,
});
expect(mockMessageCountDocuments).toHaveBeenCalledWith({
createdAt: { $gte: expect.any(Date) },
Expand Down
16 changes: 13 additions & 3 deletions backend/routes/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,27 @@ router.get('/public', async (_req: unknown, res: { json: (d: unknown) => void; s
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);

const [activePods, activeAgents, messageCount24h, registeredUsers] = await Promise.all([
const [
activePods,
activeAgents,
messageCount24h,
agentCount,
] = await Promise.all([
Pod.countDocuments({ updatedAt: { $gte: sevenDaysAgo } }),
AgentInstallation.distinct('agentName').then((names: string[]) => names.length),
pgMessageCount24h(oneDayAgo).catch((err: { message?: string }) => {
console.warn('stats: PG message count failed, falling back to Mongo:', err?.message);
return Message.countDocuments({ createdAt: { $gte: oneDayAgo } });
}),
User.countDocuments(),
User.countDocuments({ 'botMetadata.agentName': { $exists: true } }),
]);

res.json({ activePods, activeAgents, messageCount24h, registeredUsers });
res.json({
activePods,
activeAgents,
messageCount24h,
agentCount,
});
} catch {
res.status(500).json({ error: 'Failed to fetch stats' });
}
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/v2/__tests__/V2LandingPage.stats.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import axios from 'axios';
import { useAuth } from '../../context/AuthContext';
import V2LandingPage from '../landing/V2LandingPage';

jest.mock('axios');
jest.mock('../../context/AuthContext', () => ({
useAuth: jest.fn(),
}));

const mockAxiosGet = axios.get as jest.Mock;
const mockUseAuth = useAuth as jest.Mock;

const renderLanding = () => render(
<MemoryRouter>
<V2LandingPage />
</MemoryRouter>,
);

describe('V2LandingPage proof stats', () => {
let playSpy: jest.SpyInstance;

beforeAll(() => {
playSpy = jest.spyOn(window.HTMLMediaElement.prototype, 'play')
.mockResolvedValue(undefined);
});

afterAll(() => {
playSpy.mockRestore();
});

beforeEach(() => {
jest.clearAllMocks();
mockUseAuth.mockReturnValue({ isAuthenticated: false });
});

it('renders only agent and activity stats', async () => {
mockAxiosGet.mockResolvedValue({
data: {
activePods: 12,
activeAgents: 3,
messageCount24h: 1234,
agentCount: 262,
},
});

renderLanding();

const agentsLabel = await screen.findByText('agents');
const statsRow = agentsLabel.closest('.v2-landing__proof-stats');
expect(statsRow).not.toBeNull();

const stats = within(statsRow as HTMLElement);
expect(agentsLabel.previousElementSibling).toHaveTextContent('262');
expect(stats.getByText('messages / 24h').previousElementSibling).toHaveTextContent('1,234');
expect(stats.getByText('active pods').previousElementSibling).toHaveTextContent('12');
expect(stats.queryByText('builders')).not.toBeInTheDocument();
expect(stats.queryByText('people')).not.toBeInTheDocument();
expect(stats.queryByText('ADRs')).not.toBeInTheDocument();
expect(statsRow?.children).toHaveLength(3);
});
});
14 changes: 9 additions & 5 deletions frontend/src/v2/landing/V2LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const Mark: React.FC<{ size?: number }> = ({ size = 26 }) => (
interface Stats {
activePods?: number;
activeAgents?: number;
registeredUsers?: number;
messageCount24h?: number;
agentCount?: number;
}

const fmt = (n?: number): string => (typeof n === 'number' ? n.toLocaleString() : '—');
Expand Down Expand Up @@ -214,7 +215,11 @@ const V2LandingPage: React.FC = () => {
if (p && typeof p.catch === 'function') p.catch(() => { /* poster stays */ });
}, [motion]);

const hasStats = Boolean(stats && (stats.activePods || stats.activeAgents || stats.registeredUsers));
const hasStats = Boolean(stats && (
stats.activePods
|| stats.messageCount24h
|| stats.agentCount
));

return (
<div className={`v2-root v2-landing${motion ? ' v2-landing--motion' : ''}`}>
Expand Down Expand Up @@ -544,10 +549,9 @@ const V2LandingPage: React.FC = () => {
</p>
{hasStats && (
<div className="v2-landing__proof-stats">
<div className="v2-landing__proof-stat"><span className="v2-landing__proof-num">{fmt(stats?.agentCount)}</span><span className="v2-landing__proof-label">agents</span></div>
<div className="v2-landing__proof-stat"><span className="v2-landing__proof-num">{fmt(stats?.messageCount24h)}</span><span className="v2-landing__proof-label">messages / 24h</span></div>
<div className="v2-landing__proof-stat"><span className="v2-landing__proof-num">{fmt(stats?.activePods)}</span><span className="v2-landing__proof-label">active pods</span></div>
<div className="v2-landing__proof-stat"><span className="v2-landing__proof-num">{fmt(stats?.activeAgents)}</span><span className="v2-landing__proof-label">agents connected</span></div>
<div className="v2-landing__proof-stat"><span className="v2-landing__proof-num">{fmt(stats?.registeredUsers)}</span><span className="v2-landing__proof-label">people</span></div>
<div className="v2-landing__proof-stat"><span className="v2-landing__proof-num">{ADR_COUNT}</span><span className="v2-landing__proof-label">ADRs</span></div>
</div>
)}
</div>
Expand Down
Loading