Skip to content

Commit 4827866

Browse files
authored
v0.5.38: snap to grid, copilot ux improvements, billing line items
2 parents 3e697d9 + 2146326 commit 4827866

File tree

44 files changed

+26265
-152
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+26265
-152
lines changed

apps/sim/app/api/billing/update-cost/route.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { userStats } from '@sim/db/schema'
33
import { eq, sql } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
6+
import { logModelUsage } from '@/lib/billing/core/usage-log'
67
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
78
import { checkInternalApiKey } from '@/lib/copilot/utils'
89
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
@@ -14,6 +15,9 @@ const logger = createLogger('BillingUpdateCostAPI')
1415
const UpdateCostSchema = z.object({
1516
userId: z.string().min(1, 'User ID is required'),
1617
cost: z.number().min(0, 'Cost must be a non-negative number'),
18+
model: z.string().min(1, 'Model is required'),
19+
inputTokens: z.number().min(0).default(0),
20+
outputTokens: z.number().min(0).default(0),
1721
})
1822

1923
/**
@@ -71,11 +75,12 @@ export async function POST(req: NextRequest) {
7175
)
7276
}
7377

74-
const { userId, cost } = validation.data
78+
const { userId, cost, model, inputTokens, outputTokens } = validation.data
7579

7680
logger.info(`[${requestId}] Processing cost update`, {
7781
userId,
7882
cost,
83+
model,
7984
})
8085

8186
// Check if user stats record exists (same as ExecutionLogger)
@@ -107,6 +112,16 @@ export async function POST(req: NextRequest) {
107112
addedCost: cost,
108113
})
109114

115+
// Log usage for complete audit trail
116+
await logModelUsage({
117+
userId,
118+
source: 'copilot',
119+
model,
120+
inputTokens,
121+
outputTokens,
122+
cost,
123+
})
124+
110125
// Check if user has hit overage threshold and bill incrementally
111126
await checkAndBillOverageThreshold(userId)
112127

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'crypto'
22
import { db } from '@sim/db'
3-
import { chat } from '@sim/db/schema'
3+
import { chat, workflow } from '@sim/db/schema'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
@@ -94,6 +94,21 @@ export async function POST(
9494
if (!deployment.isActive) {
9595
logger.warn(`[${requestId}] Chat is not active: ${identifier}`)
9696

97+
const [workflowRecord] = await db
98+
.select({ workspaceId: workflow.workspaceId })
99+
.from(workflow)
100+
.where(eq(workflow.id, deployment.workflowId))
101+
.limit(1)
102+
103+
const workspaceId = workflowRecord?.workspaceId
104+
if (!workspaceId) {
105+
logger.warn(`[${requestId}] Cannot log: workflow ${deployment.workflowId} has no workspace`)
106+
return addCorsHeaders(
107+
createErrorResponse('This chat is currently unavailable', 403),
108+
request
109+
)
110+
}
111+
97112
const executionId = randomUUID()
98113
const loggingSession = new LoggingSession(
99114
deployment.workflowId,
@@ -104,7 +119,7 @@ export async function POST(
104119

105120
await loggingSession.safeStart({
106121
userId: deployment.userId,
107-
workspaceId: '', // Will be resolved if needed
122+
workspaceId,
108123
variables: {},
109124
})
110125

@@ -169,7 +184,14 @@ export async function POST(
169184

170185
const { actorUserId, workflowRecord } = preprocessResult
171186
const workspaceOwnerId = actorUserId!
172-
const workspaceId = workflowRecord?.workspaceId || ''
187+
const workspaceId = workflowRecord?.workspaceId
188+
if (!workspaceId) {
189+
logger.error(`[${requestId}] Workflow ${deployment.workflowId} has no workspaceId`)
190+
return addCorsHeaders(
191+
createErrorResponse('Workflow has no associated workspace', 500),
192+
request
193+
)
194+
}
173195

174196
try {
175197
const selectedOutputs: string[] = []

apps/sim/app/api/logs/export/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export async function GET(request: NextRequest) {
5757
workflowName: workflow.name,
5858
}
5959

60-
let conditions: SQL | undefined = eq(workflow.workspaceId, params.workspaceId)
60+
let conditions: SQL | undefined = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
6161

6262
if (params.level && params.level !== 'all') {
6363
const levels = params.level.split(',').filter(Boolean)
@@ -134,7 +134,7 @@ export async function GET(request: NextRequest) {
134134
permissions,
135135
and(
136136
eq(permissions.entityType, 'workspace'),
137-
eq(permissions.entityId, workflow.workspaceId),
137+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
138138
eq(permissions.userId, userId)
139139
)
140140
)

apps/sim/app/api/logs/route.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export async function GET(request: NextRequest) {
130130
deploymentVersionName: sql<null>`NULL`,
131131
}
132132

133+
const workspaceFilter = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
134+
133135
const baseQuery = db
134136
.select(selectColumns)
135137
.from(workflowExecutionLogs)
@@ -141,18 +143,12 @@ export async function GET(request: NextRequest) {
141143
workflowDeploymentVersion,
142144
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
143145
)
144-
.innerJoin(
145-
workflow,
146-
and(
147-
eq(workflowExecutionLogs.workflowId, workflow.id),
148-
eq(workflow.workspaceId, params.workspaceId)
149-
)
150-
)
146+
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
151147
.innerJoin(
152148
permissions,
153149
and(
154150
eq(permissions.entityType, 'workspace'),
155-
eq(permissions.entityId, workflow.workspaceId),
151+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
156152
eq(permissions.userId, userId)
157153
)
158154
)
@@ -300,7 +296,7 @@ export async function GET(request: NextRequest) {
300296
}
301297

302298
const logs = await baseQuery
303-
.where(conditions)
299+
.where(and(workspaceFilter, conditions))
304300
.orderBy(desc(workflowExecutionLogs.startedAt))
305301
.limit(params.limit)
306302
.offset(params.offset)
@@ -312,22 +308,16 @@ export async function GET(request: NextRequest) {
312308
pausedExecutions,
313309
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
314310
)
315-
.innerJoin(
316-
workflow,
317-
and(
318-
eq(workflowExecutionLogs.workflowId, workflow.id),
319-
eq(workflow.workspaceId, params.workspaceId)
320-
)
321-
)
311+
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
322312
.innerJoin(
323313
permissions,
324314
and(
325315
eq(permissions.entityType, 'workspace'),
326-
eq(permissions.entityId, workflow.workspaceId),
316+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
327317
eq(permissions.userId, userId)
328318
)
329319
)
330-
.where(conditions)
320+
.where(and(eq(workflowExecutionLogs.workspaceId, params.workspaceId), conditions))
331321

332322
const countResult = await countQuery
333323

apps/sim/app/api/logs/triggers/route.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
2+
import { permissions, workflowExecutionLogs } from '@sim/db/schema'
33
import { and, eq, isNotNull, sql } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
@@ -42,23 +42,17 @@ export async function GET(request: NextRequest) {
4242
trigger: workflowExecutionLogs.trigger,
4343
})
4444
.from(workflowExecutionLogs)
45-
.innerJoin(
46-
workflow,
47-
and(
48-
eq(workflowExecutionLogs.workflowId, workflow.id),
49-
eq(workflow.workspaceId, params.workspaceId)
50-
)
51-
)
5245
.innerJoin(
5346
permissions,
5447
and(
5548
eq(permissions.entityType, 'workspace'),
56-
eq(permissions.entityId, workflow.workspaceId),
49+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
5750
eq(permissions.userId, userId)
5851
)
5952
)
6053
.where(
6154
and(
55+
eq(workflowExecutionLogs.workspaceId, params.workspaceId),
6256
isNotNull(workflowExecutionLogs.trigger),
6357
sql`${workflowExecutionLogs.trigger} NOT IN ('api', 'manual', 'webhook', 'chat', 'schedule')`
6458
)

apps/sim/app/api/users/me/settings/route.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ const SettingsSchema = z.object({
2626
showTrainingControls: z.boolean().optional(),
2727
superUserModeEnabled: z.boolean().optional(),
2828
errorNotificationsEnabled: z.boolean().optional(),
29+
snapToGridSize: z.number().min(0).max(50).optional(),
2930
})
3031

31-
// Default settings values
3232
const defaultSettings = {
3333
theme: 'system',
3434
autoConnect: true,
@@ -38,6 +38,7 @@ const defaultSettings = {
3838
showTrainingControls: false,
3939
superUserModeEnabled: false,
4040
errorNotificationsEnabled: true,
41+
snapToGridSize: 0,
4142
}
4243

4344
export async function GET() {
@@ -46,7 +47,6 @@ export async function GET() {
4647
try {
4748
const session = await getSession()
4849

49-
// Return default settings for unauthenticated users instead of 401 error
5050
if (!session?.user?.id) {
5151
logger.info(`[${requestId}] Returning default settings for unauthenticated user`)
5252
return NextResponse.json({ data: defaultSettings }, { status: 200 })
@@ -72,13 +72,13 @@ export async function GET() {
7272
showTrainingControls: userSettings.showTrainingControls ?? false,
7373
superUserModeEnabled: userSettings.superUserModeEnabled ?? true,
7474
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
75+
snapToGridSize: userSettings.snapToGridSize ?? 0,
7576
},
7677
},
7778
{ status: 200 }
7879
)
7980
} catch (error: any) {
8081
logger.error(`[${requestId}] Settings fetch error`, error)
81-
// Return default settings on error instead of error response
8282
return NextResponse.json({ data: defaultSettings }, { status: 200 })
8383
}
8484
}
@@ -89,7 +89,6 @@ export async function PATCH(request: Request) {
8989
try {
9090
const session = await getSession()
9191

92-
// Return success for unauthenticated users instead of error
9392
if (!session?.user?.id) {
9493
logger.info(
9594
`[${requestId}] Settings update attempted by unauthenticated user - acknowledged without saving`
@@ -103,7 +102,6 @@ export async function PATCH(request: Request) {
103102
try {
104103
const validatedData = SettingsSchema.parse(body)
105104

106-
// Store the settings
107105
await db
108106
.insert(settings)
109107
.values({
@@ -135,7 +133,6 @@ export async function PATCH(request: Request) {
135133
}
136134
} catch (error: any) {
137135
logger.error(`[${requestId}] Settings update error`, error)
138-
// Return success on error instead of error response
139136
return NextResponse.json({ success: true }, { status: 200 })
140137
}
141138
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
3+
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
5+
import { createLogger } from '@/lib/logs/console/logger'
6+
7+
const logger = createLogger('UsageLogsAPI')
8+
9+
const QuerySchema = z.object({
10+
source: z.enum(['workflow', 'wand', 'copilot']).optional(),
11+
workspaceId: z.string().optional(),
12+
period: z.enum(['1d', '7d', '30d', 'all']).optional().default('30d'),
13+
limit: z.coerce.number().min(1).max(100).optional().default(50),
14+
cursor: z.string().optional(),
15+
})
16+
17+
/**
18+
* GET /api/users/me/usage-logs
19+
* Get usage logs for the authenticated user
20+
*/
21+
export async function GET(req: NextRequest) {
22+
try {
23+
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
24+
25+
if (!auth.success || !auth.userId) {
26+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
27+
}
28+
29+
const userId = auth.userId
30+
31+
const { searchParams } = new URL(req.url)
32+
const queryParams = {
33+
source: searchParams.get('source') || undefined,
34+
workspaceId: searchParams.get('workspaceId') || undefined,
35+
period: searchParams.get('period') || '30d',
36+
limit: searchParams.get('limit') || '50',
37+
cursor: searchParams.get('cursor') || undefined,
38+
}
39+
40+
const validation = QuerySchema.safeParse(queryParams)
41+
42+
if (!validation.success) {
43+
return NextResponse.json(
44+
{
45+
error: 'Invalid query parameters',
46+
details: validation.error.issues,
47+
},
48+
{ status: 400 }
49+
)
50+
}
51+
52+
const { source, workspaceId, period, limit, cursor } = validation.data
53+
54+
let startDate: Date | undefined
55+
const endDate = new Date()
56+
57+
if (period !== 'all') {
58+
startDate = new Date()
59+
switch (period) {
60+
case '1d':
61+
startDate.setDate(startDate.getDate() - 1)
62+
break
63+
case '7d':
64+
startDate.setDate(startDate.getDate() - 7)
65+
break
66+
case '30d':
67+
startDate.setDate(startDate.getDate() - 30)
68+
break
69+
}
70+
}
71+
72+
const result = await getUserUsageLogs(userId, {
73+
source: source as UsageLogSource | undefined,
74+
workspaceId,
75+
startDate,
76+
endDate,
77+
limit,
78+
cursor,
79+
})
80+
81+
logger.debug('Retrieved usage logs', {
82+
userId,
83+
source,
84+
period,
85+
logCount: result.logs.length,
86+
hasMore: result.pagination.hasMore,
87+
})
88+
89+
return NextResponse.json({
90+
success: true,
91+
...result,
92+
})
93+
} catch (error) {
94+
logger.error('Failed to get usage logs', {
95+
error: error instanceof Error ? error.message : String(error),
96+
})
97+
98+
return NextResponse.json(
99+
{
100+
error: 'Failed to retrieve usage logs',
101+
},
102+
{ status: 500 }
103+
)
104+
}
105+
}

0 commit comments

Comments
 (0)