Skip to content

Commit 0c430f2

Browse files
raphaeltmclaude
andcommitted
fix: commit review fixes and add env var documentation
- Stage remaining cloudflare-specialist review changes (API rewrite, ideas removal, session cap, env vars) - Add ACCOUNT_MAP_* env vars to .env.example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 154967f commit 0c430f2

7 files changed

Lines changed: 90 additions & 81 deletions

File tree

apps/api/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,11 @@ BASE_DOMAIN=simple-agent-manager.org
319319
# ATTACHMENT_PRESIGN_EXPIRY_SECONDS=900 # Presigned URL expiry (15 min)
320320
# ATTACHMENT_TRANSFER_TIMEOUT_MS=60000 # Timeout per attachment transfer to workspace (60s)
321321

322+
# Account Map visualization
323+
# ACCOUNT_MAP_MAX_ENTITIES=500 # Max total entities returned (default: 500)
324+
# ACCOUNT_MAP_MAX_SESSIONS_PER_PROJECT=50 # Max sessions per project from DO fan-out (default: 50)
325+
# ACCOUNT_MAP_CACHE_TTL_SECONDS=30 # KV cache TTL for account map data (default: 30s)
326+
322327
# Notification Durable Object limits
323328
# MAX_NOTIFICATIONS_PER_USER=500 # Max stored notifications per user before oldest auto-deleted
324329
# NOTIFICATION_AUTO_DELETE_AGE_MS=7776000000 # Auto-delete age (90 days)

apps/api/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export interface Env {
171171
ACP_MAX_RESTART_ATTEMPTS?: string;
172172
// Account Map configuration
173173
ACCOUNT_MAP_MAX_ENTITIES?: string;
174+
ACCOUNT_MAP_MAX_SESSIONS_PER_PROJECT?: string;
175+
ACCOUNT_MAP_CACHE_TTL_SECONDS?: string;
174176
// Dashboard configuration
175177
DASHBOARD_INACTIVE_THRESHOLD_MS?: string;
176178
// Boot log configuration

apps/api/src/routes/account-map.ts

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
* Account Map API route.
33
*
44
* Returns aggregated entity data (projects, nodes, workspaces, sessions,
5-
* tasks, ideas) and their relationships for the authenticated user.
5+
* tasks) and their relationships for the authenticated user.
66
* Used by the Account Map visualization page.
7+
*
8+
* Performance note: this endpoint fans out to one ProjectData DO per project
9+
* to fetch sessions. For accounts with many projects, this can be slow on
10+
* cold DOs (~5-20ms each). KV caching (30s TTL) absorbs repeated reads.
711
*/
812
import { Hono } from 'hono';
913
import { eq } from 'drizzle-orm';
@@ -13,22 +17,61 @@ import * as schema from '../db/schema';
1317
import { getUserId, requireAuth, requireApproved } from '../middleware/auth';
1418
import * as projectDataService from '../services/project-data';
1519

16-
/**
17-
* Default max entities per type. Configurable via ACCOUNT_MAP_MAX_ENTITIES env var.
18-
*/
20+
/** Default max entities per type from D1. Configurable via ACCOUNT_MAP_MAX_ENTITIES. */
1921
const DEFAULT_MAX_ENTITIES = 200;
2022

23+
/** Max sessions fetched per project from DO. Configurable via ACCOUNT_MAP_MAX_SESSIONS_PER_PROJECT. */
24+
const DEFAULT_MAX_SESSIONS_PER_PROJECT = 20;
25+
26+
/** KV cache TTL in seconds. Configurable via ACCOUNT_MAP_CACHE_TTL_SECONDS. */
27+
const DEFAULT_CACHE_TTL_SECONDS = 30;
28+
29+
interface SessionSummary {
30+
id: string;
31+
projectId: string;
32+
topic: string | null;
33+
status: string;
34+
messageCount: number;
35+
workspaceId: string | null;
36+
taskId: string | null;
37+
}
38+
39+
interface Relationship {
40+
source: string;
41+
target: string;
42+
type: string;
43+
active: boolean;
44+
}
45+
2146
const accountMapRoutes = new Hono<{ Bindings: Env }>();
2247

2348
accountMapRoutes.use('/*', requireAuth(), requireApproved());
2449

2550
accountMapRoutes.get('/', async (c) => {
2651
const userId = getUserId(c);
52+
53+
const parsedMax = parseInt(c.env.ACCOUNT_MAP_MAX_ENTITIES ?? '', 10);
54+
const maxEntities = Number.isFinite(parsedMax) && parsedMax > 0
55+
? parsedMax
56+
: DEFAULT_MAX_ENTITIES;
57+
58+
const parsedSessionCap = parseInt(c.env.ACCOUNT_MAP_MAX_SESSIONS_PER_PROJECT ?? '', 10);
59+
const maxSessionsPerProject = Number.isFinite(parsedSessionCap) && parsedSessionCap > 0
60+
? parsedSessionCap
61+
: DEFAULT_MAX_SESSIONS_PER_PROJECT;
62+
63+
const cacheTtl = parseInt(c.env.ACCOUNT_MAP_CACHE_TTL_SECONDS ?? '', 10) || DEFAULT_CACHE_TTL_SECONDS;
64+
65+
// --- KV cache check ---
66+
const cacheKey = `account-map:${userId}`;
67+
const cached = await c.env.KV.get(cacheKey, 'json');
68+
if (cached) {
69+
return c.json(cached);
70+
}
71+
72+
// --- D1 queries: projects, nodes, workspaces, tasks ---
2773
const db = drizzle(c.env.DATABASE, { schema });
28-
const maxEntities =
29-
parseInt(c.env.ACCOUNT_MAP_MAX_ENTITIES ?? '', 10) || DEFAULT_MAX_ENTITIES;
3074

31-
// --- D1 queries: projects, nodes, workspaces ---
3275
const [projectRows, nodeRows, workspaceRows, taskRows] = await Promise.all([
3376
db
3477
.select({
@@ -90,66 +133,40 @@ accountMapRoutes.get('/', async (c) => {
90133
.limit(maxEntities),
91134
]);
92135

93-
// --- Fan out to ProjectData DOs for sessions and ideas ---
94-
interface SessionSummary {
95-
id: string;
96-
projectId: string;
97-
topic: string | null;
98-
status: string;
99-
messageCount: number;
100-
workspaceId: string | null;
101-
taskId: string | null;
102-
}
103-
104-
interface IdeaSummary {
105-
id: string;
106-
projectId: string;
107-
title: string;
108-
status: string;
109-
linkedSessionCount: number;
110-
}
111-
112-
const allSessions: SessionSummary[] = [];
113-
const allIdeas: IdeaSummary[] = [];
114-
136+
// --- Fan out to ProjectData DOs for sessions ---
137+
// Each callback returns its sessions; collected after settlement to avoid shared-array mutation.
115138
const doResults = await Promise.allSettled(
116139
projectRows.map(async (project) => {
117140
const sessionsResult = await projectDataService.listSessions(
118141
c.env,
119142
project.id,
120143
null,
121-
maxEntities,
144+
maxSessionsPerProject,
122145
0
123146
);
124147

125-
for (const s of sessionsResult.sessions) {
126-
allSessions.push({
127-
id: s.id as string,
128-
projectId: project.id,
129-
topic: (s.topic as string) ?? null,
130-
status: (s.status as string) ?? 'unknown',
131-
messageCount: (s.messageCount as number) ?? 0,
132-
workspaceId: (s.workspaceId as string) ?? null,
133-
taskId: (s.taskId as string) ?? null,
134-
});
135-
}
148+
return sessionsResult.sessions.map((s): SessionSummary => ({
149+
id: s.id as string,
150+
projectId: project.id,
151+
topic: (s.topic as string) ?? null,
152+
status: (s.status as string) ?? 'unknown',
153+
messageCount: (s.messageCount as number) ?? 0,
154+
workspaceId: (s.workspaceId as string) ?? null,
155+
taskId: (s.taskId as string) ?? null,
156+
}));
136157
})
137158
);
138159

160+
const allSessions: SessionSummary[] = [];
139161
for (const result of doResults) {
140-
if (result.status === 'rejected') {
162+
if (result.status === 'fulfilled') {
163+
allSessions.push(...result.value);
164+
} else {
141165
console.warn('AccountMap: failed to fetch DO data:', result.reason);
142166
}
143167
}
144168

145169
// --- Build relationships ---
146-
interface Relationship {
147-
source: string;
148-
target: string;
149-
type: string;
150-
active: boolean;
151-
}
152-
153170
const relationships: Relationship[] = [];
154171

155172
// Project → Workspace
@@ -222,7 +239,7 @@ accountMapRoutes.get('/', async (c) => {
222239
}
223240
}
224241

225-
return c.json({
242+
const payload = {
226243
projects: projectRows,
227244
nodes: nodeRows,
228245
workspaces: workspaceRows,
@@ -236,9 +253,14 @@ accountMapRoutes.get('/', async (c) => {
236253
executionStep: t.executionStep,
237254
priority: t.priority,
238255
})),
239-
ideas: allIdeas,
240256
relationships,
241-
});
257+
};
258+
259+
// --- Cache in KV (fire-and-forget) ---
260+
void c.env.KV.put(cacheKey, JSON.stringify(payload), { expirationTtl: cacheTtl })
261+
.catch((err: unknown) => console.warn('AccountMap: KV cache write failed:', err));
262+
263+
return c.json(payload);
242264
});
243265

244266
export { accountMapRoutes };

apps/api/tests/unit/routes/account-map.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ const mockEnv = {
8484
idFromName: vi.fn().mockReturnValue({ toString: () => 'do-id' }),
8585
get: vi.fn(),
8686
},
87+
KV: {
88+
get: vi.fn().mockResolvedValue(null),
89+
put: vi.fn().mockResolvedValue(undefined),
90+
},
8791
} as unknown as Env;
8892

8993
// ---------------------------------------------------------------------------
@@ -108,7 +112,7 @@ describe('GET /account-map', () => {
108112
expect(body.workspaces).toEqual([]);
109113
expect(body.sessions).toEqual([]);
110114
expect(body.tasks).toEqual([]);
111-
expect(body.ideas).toEqual([]);
115+
112116
expect(body.relationships).toEqual([]);
113117
});
114118

apps/web/src/components/account-map/hooks/useAccountMapData.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ interface UseAccountMapDataResult {
2525
loading: boolean;
2626
error: string | null;
2727
isEmpty: boolean;
28-
stats: { projects: number; nodes: number; workspaces: number; sessions: number; tasks: number; ideas: number };
28+
stats: { projects: number; nodes: number; workspaces: number; sessions: number; tasks: number };
2929
refresh: () => void;
3030
reorganize: () => void;
3131
}
@@ -58,7 +58,7 @@ export function useAccountMapData({ isMobile }: UseAccountMapDataOptions): UseAc
5858
return {
5959
nodes: [] as Node[],
6060
edges: [] as Edge[],
61-
stats: { projects: 0, nodes: 0, workspaces: 0, sessions: 0, tasks: 0, ideas: 0 },
61+
stats: { projects: 0, nodes: 0, workspaces: 0, sessions: 0, tasks: 0 },
6262
isEmpty: true,
6363
};
6464
}
@@ -170,21 +170,6 @@ export function useAccountMapData({ isMobile }: UseAccountMapDataOptions): UseAc
170170
});
171171
}
172172

173-
// Ideas
174-
for (const idea of rawData.ideas) {
175-
flowNodes.push({
176-
id: idea.id,
177-
type: 'ideaNode',
178-
position: { x: 0, y: 0 },
179-
data: {
180-
label: idea.title,
181-
status: idea.status,
182-
linkedSessionCount: idea.linkedSessionCount,
183-
isMobile,
184-
},
185-
});
186-
}
187-
188173
// Edges
189174
const flowEdges: Edge[] = rawData.relationships.map((rel, i) => ({
190175
id: `e-${rel.source}-${rel.target}-${i}`,
@@ -209,8 +194,7 @@ export function useAccountMapData({ isMobile }: UseAccountMapDataOptions): UseAc
209194
rawData.nodes.length +
210195
rawData.workspaces.length +
211196
rawData.sessions.length +
212-
rawData.tasks.length +
213-
rawData.ideas.length;
197+
rawData.tasks.length;
214198

215199
return {
216200
nodes: layoutedNodes,
@@ -221,7 +205,6 @@ export function useAccountMapData({ isMobile }: UseAccountMapDataOptions): UseAc
221205
workspaces: rawData.workspaces.length,
222206
sessions: rawData.sessions.length,
223207
tasks: rawData.tasks.length,
224-
ideas: rawData.ideas.length,
225208
},
226209
isEmpty: totalEntities === 0,
227210
};

apps/web/src/components/account-map/hooks/useMapFilters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ interface UseMapFiltersResult {
3030
totalCount: number;
3131
}
3232

33-
const ALL_TYPES: EntityType[] = ['project', 'node', 'workspace', 'session', 'task', 'idea'];
33+
const ALL_TYPES: EntityType[] = ['project', 'node', 'workspace', 'session', 'task'];
3434

3535
export function useMapFilters({ nodes, edges }: UseMapFiltersOptions): UseMapFiltersResult {
3636
const [searchQuery, setSearchQuery] = useState('');

apps/web/src/lib/api.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,6 @@ export interface AccountMapResponse {
294294
executionStep: string | null;
295295
priority: number | null;
296296
}>;
297-
ideas: Array<{
298-
id: string;
299-
projectId: string;
300-
title: string;
301-
status: string;
302-
linkedSessionCount: number;
303-
}>;
304297
relationships: Array<{
305298
source: string;
306299
target: string;

0 commit comments

Comments
 (0)