Skip to content

Commit 51cc1d4

Browse files
authored
Merge pull request #2497 from trycompai/main
[comp] Production Deploy
2 parents 87fc64e + cfee2f2 commit 51cc1d4

13 files changed

Lines changed: 373 additions & 25 deletions

File tree

apps/api/src/admin-organizations/admin-organizations.controller.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,29 @@ export class AdminOrganizationsController {
4242
});
4343
}
4444

45+
@Get('activity')
46+
@ApiOperation({ summary: 'Organization activity report - shows last session per org (platform admin)' })
47+
@ApiQuery({ name: 'inactiveDays', required: false, description: 'Filter orgs with no session in N days (default: 90)' })
48+
@ApiQuery({ name: 'hasAccess', required: false, description: 'Filter by hasAccess (true/false)' })
49+
@ApiQuery({ name: 'onboarded', required: false, description: 'Filter by onboardingCompleted (true/false)' })
50+
@ApiQuery({ name: 'page', required: false })
51+
@ApiQuery({ name: 'limit', required: false })
52+
async activity(
53+
@Query('inactiveDays') inactiveDays?: string,
54+
@Query('hasAccess') hasAccess?: string,
55+
@Query('onboarded') onboarded?: string,
56+
@Query('page') page?: string,
57+
@Query('limit') limit?: string,
58+
) {
59+
return this.service.getOrgActivity({
60+
inactiveDays: Math.max(0, Number.isFinite(parseInt(inactiveDays ?? '90', 10)) ? parseInt(inactiveDays ?? '90', 10) : 90),
61+
hasAccess: hasAccess === 'true' ? true : hasAccess === 'false' ? false : undefined,
62+
onboarded: onboarded === 'true' ? true : onboarded === 'false' ? false : undefined,
63+
page: Math.max(1, parseInt(page || '1', 10) || 1),
64+
limit: Math.min(100, Math.max(1, parseInt(limit || '50', 10) || 50)),
65+
});
66+
}
67+
4568
@Get(':id')
4669
@ApiOperation({ summary: 'Get organization details (platform admin)' })
4770
async get(@Param('id') id: string) {

apps/api/src/admin-organizations/admin-organizations.service.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,119 @@ export class AdminOrganizationsService {
100100
};
101101
}
102102

103+
async getOrgActivity(options: {
104+
inactiveDays: number;
105+
hasAccess?: boolean;
106+
onboarded?: boolean;
107+
page: number;
108+
limit: number;
109+
}) {
110+
const { inactiveDays, hasAccess, onboarded, page, limit } = options;
111+
const skip = (page - 1) * limit;
112+
const cutoff = new Date();
113+
cutoff.setDate(cutoff.getDate() - inactiveDays);
114+
115+
const where: Record<string, unknown> = {};
116+
if (hasAccess !== undefined) where.hasAccess = hasAccess;
117+
if (onboarded !== undefined) where.onboardingCompleted = onboarded;
118+
119+
const [organizations, total] = await Promise.all([
120+
db.organization.findMany({
121+
where,
122+
select: {
123+
id: true,
124+
name: true,
125+
createdAt: true,
126+
hasAccess: true,
127+
onboardingCompleted: true,
128+
_count: { select: { members: true, tasks: true, policy: true, auditLog: true } },
129+
members: {
130+
where: { deactivated: false },
131+
select: {
132+
role: true,
133+
user: {
134+
select: {
135+
id: true,
136+
name: true,
137+
email: true,
138+
sessions: {
139+
orderBy: { updatedAt: 'desc' as const },
140+
take: 1,
141+
select: { updatedAt: true },
142+
},
143+
},
144+
},
145+
},
146+
},
147+
auditLog: {
148+
orderBy: { timestamp: 'desc' as const },
149+
take: 1,
150+
select: { timestamp: true },
151+
},
152+
},
153+
orderBy: { createdAt: 'desc' },
154+
skip,
155+
take: limit,
156+
}),
157+
db.organization.count({ where }),
158+
]);
159+
160+
// Post-process to find last activity per org
161+
const data = organizations.map((org) => {
162+
let lastSession: Date | null = null;
163+
let owner: { id: string; name: string; email: string } | null = null;
164+
165+
for (const member of org.members) {
166+
const sess = member.user?.sessions?.[0]?.updatedAt;
167+
if (sess && (!lastSession || sess > lastSession)) {
168+
lastSession = sess;
169+
}
170+
if (member.role?.includes('owner') && !owner) {
171+
owner = { id: member.user.id, name: member.user.name, email: member.user.email };
172+
}
173+
}
174+
175+
const lastAuditLog = org.auditLog?.[0]?.timestamp ?? null;
176+
const lastActivity = [lastSession, lastAuditLog]
177+
.filter(Boolean)
178+
.sort((a, b) => (b as Date).getTime() - (a as Date).getTime())[0] as Date | undefined;
179+
180+
const isActive = lastActivity ? lastActivity >= cutoff : false;
181+
182+
return {
183+
id: org.id,
184+
name: org.name,
185+
createdAt: org.createdAt,
186+
hasAccess: org.hasAccess,
187+
onboardingCompleted: org.onboardingCompleted,
188+
memberCount: org._count.members,
189+
taskCount: org._count.tasks,
190+
policyCount: org._count.policy,
191+
auditLogCount: org._count.auditLog,
192+
owner,
193+
lastSession: lastSession?.toISOString() ?? null,
194+
lastAuditLog: lastAuditLog ? (lastAuditLog as Date).toISOString() : null,
195+
lastActivity: lastActivity?.toISOString() ?? null,
196+
isActive,
197+
};
198+
});
199+
200+
const activeCount = data.filter((d) => d.isActive).length;
201+
const inactiveCount = data.filter((d) => !d.isActive).length;
202+
203+
return {
204+
data,
205+
total,
206+
page,
207+
limit,
208+
summary: {
209+
inactiveDays,
210+
activeInPage: activeCount,
211+
inactiveInPage: inactiveCount,
212+
},
213+
};
214+
}
215+
103216
async getOrganization(id: string) {
104217
const org = await db.organization.findUnique({
105218
where: { id },

apps/api/src/browserbase/browserbase.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
1515
const BROWSER_WIDTH = 1440;
1616
const BROWSER_HEIGHT = 900;
1717

18+
/** Stagehand v3 requires 'provider/model' format. */
19+
const STAGEHAND_MODEL = 'anthropic/claude-sonnet-4-6';
20+
const STAGEHAND_CUA_MODEL = 'anthropic/claude-sonnet-4-6';
21+
1822
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
1923

2024
const PENDING_CONTEXT_ID = '__PENDING__';
@@ -205,7 +209,7 @@ export class BrowserbaseService {
205209
projectId: this.getProjectId(),
206210
browserbaseSessionID: sessionId,
207211
model: {
208-
modelName: 'claude-3-7-sonnet-latest',
212+
modelName: STAGEHAND_MODEL,
209213
apiKey: process.env.ANTHROPIC_API_KEY,
210214
},
211215
verbose: 1,
@@ -784,7 +788,7 @@ export class BrowserbaseService {
784788
.agent({
785789
cua: true,
786790
model: {
787-
modelName: 'anthropic/claude-3-7-sonnet-latest',
791+
modelName: STAGEHAND_CUA_MODEL,
788792
apiKey: process.env.ANTHROPIC_API_KEY,
789793
},
790794
})

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/BrowserAutomations.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export function BrowserAutomations({ taskId, isManualTask = false }: BrowserAuto
130130
onRun={execution.runAutomation}
131131
onCreateClick={isManualTask ? undefined : () => setDialogState({ open: true, mode: 'create' })}
132132
onEditClick={(automation) => setDialogState({ open: true, mode: 'edit', automation })}
133+
onDelete={automations.deleteAutomation}
134+
onToggleEnabled={automations.toggleAutomation}
133135
/>
134136
<BrowserAutomationConfigDialog
135137
isOpen={dialogState.open}

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/browser-automations/AutomationItem.tsx

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@
22

33
import { cn } from '@/lib/utils';
44
import { Button } from '@trycompai/ui/button';
5-
import { ChevronDown, Loader2, MonitorPlay, Settings } from 'lucide-react';
5+
import {
6+
ChevronDown,
7+
Loader2,
8+
MonitorPlay,
9+
Pencil,
10+
Power,
11+
PowerOff,
12+
Trash2,
13+
} from 'lucide-react';
614
import { formatDistanceToNow } from 'date-fns';
15+
import { useState } from 'react';
716
import type { BrowserAutomation, BrowserAutomationRun } from '../../hooks/types';
817
import { RunHistory } from './RunHistory';
918

@@ -15,6 +24,8 @@ interface AutomationItemProps {
1524
onToggleExpand: () => void;
1625
onRun: () => void;
1726
onEdit: () => void;
27+
onDelete: () => void;
28+
onToggleEnabled: (enabled: boolean) => void;
1829
}
1930

2031
export function AutomationItem({
@@ -25,23 +36,30 @@ export function AutomationItem({
2536
onToggleExpand,
2637
onRun,
2738
onEdit,
39+
onDelete,
40+
onToggleEnabled,
2841
}: AutomationItemProps) {
42+
const [confirmDelete, setConfirmDelete] = useState(false);
2943
const runs: BrowserAutomationRun[] = automation.runs || [];
3044
const latestRun = runs[0];
3145

3246
// status dot
3347
const hasFailed = latestRun?.status === 'failed';
3448
const isCompleted = latestRun?.status === 'completed';
35-
const dotColor = hasFailed
36-
? 'bg-destructive shadow-[0_0_8px_rgba(255,0,0,0.3)]'
37-
: isCompleted
38-
? 'bg-primary shadow-[0_0_8px_rgba(0,77,64,0.4)]'
39-
: 'bg-muted-foreground';
49+
const isDisabled = !automation.isEnabled;
50+
const dotColor = isDisabled
51+
? 'bg-muted-foreground/40'
52+
: hasFailed
53+
? 'bg-destructive shadow-[0_0_8px_rgba(255,0,0,0.3)]'
54+
: isCompleted
55+
? 'bg-primary shadow-[0_0_8px_rgba(0,77,64,0.4)]'
56+
: 'bg-muted-foreground';
4057

4158
return (
4259
<div
4360
className={cn(
4461
'rounded-lg border transition-all duration-300',
62+
isDisabled && 'opacity-60',
4563
isExpanded
4664
? 'border-primary/30 shadow-sm bg-primary/2'
4765
: 'border-border/50 hover:border-border hover:shadow-sm',
@@ -51,9 +69,16 @@ export function AutomationItem({
5169
<div className={cn('h-2.5 w-2.5 rounded-full shrink-0', dotColor)} />
5270

5371
<div className="flex-1 min-w-0">
54-
<p className="font-semibold text-foreground text-sm tracking-tight">
55-
{automation.name}
56-
</p>
72+
<div className="flex items-center gap-2">
73+
<p className="font-semibold text-foreground text-sm tracking-tight">
74+
{automation.name}
75+
</p>
76+
{isDisabled && (
77+
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
78+
Paused
79+
</span>
80+
)}
81+
</div>
5782
{latestRun ? (
5883
<p className="text-xs text-muted-foreground mt-0.5">
5984
Last ran {formatDistanceToNow(new Date(latestRun.createdAt), { addSuffix: true })}
@@ -63,15 +88,75 @@ export function AutomationItem({
6388
)}
6489
</div>
6590

66-
<div className="flex items-center gap-2">
91+
<div className="flex items-center gap-1.5">
6792
{!readOnly && (
68-
<Button variant="ghost" size="icon" onClick={onEdit} aria-label="Edit automation">
69-
<Settings className="h-4 w-4" />
93+
<Button
94+
variant="ghost"
95+
size="icon"
96+
className="h-8 w-8"
97+
onClick={() => onToggleEnabled(!automation.isEnabled)}
98+
aria-label={automation.isEnabled ? 'Pause automation' : 'Enable automation'}
99+
>
100+
{automation.isEnabled ? (
101+
<Power className="h-3.5 w-3.5 text-primary" />
102+
) : (
103+
<PowerOff className="h-3.5 w-3.5 text-muted-foreground" />
104+
)}
70105
</Button>
71106
)}
72107

73108
{!readOnly && (
74-
<Button variant="outline" size="sm" onClick={onRun} disabled={isRunning}>
109+
<Button
110+
variant="ghost"
111+
size="icon"
112+
className="h-8 w-8"
113+
onClick={onEdit}
114+
aria-label="Edit automation"
115+
>
116+
<Pencil className="h-3.5 w-3.5" />
117+
</Button>
118+
)}
119+
120+
{!readOnly && (
121+
confirmDelete ? (
122+
<div className="flex items-center gap-1">
123+
<Button
124+
variant="destructive"
125+
size="sm"
126+
className="h-7 text-xs"
127+
onClick={() => { onDelete(); setConfirmDelete(false); }}
128+
>
129+
Confirm
130+
</Button>
131+
<Button
132+
variant="ghost"
133+
size="sm"
134+
className="h-7 text-xs"
135+
onClick={() => setConfirmDelete(false)}
136+
>
137+
Cancel
138+
</Button>
139+
</div>
140+
) : (
141+
<Button
142+
variant="ghost"
143+
size="icon"
144+
className="h-8 w-8"
145+
onClick={() => setConfirmDelete(true)}
146+
aria-label="Delete automation"
147+
>
148+
<Trash2 className="h-3.5 w-3.5 text-muted-foreground hover:text-destructive" />
149+
</Button>
150+
)
151+
)}
152+
153+
{!readOnly && (
154+
<Button
155+
variant="outline"
156+
size="sm"
157+
onClick={onRun}
158+
disabled={isRunning || isDisabled}
159+
>
75160
{isRunning ? (
76161
<>
77162
<Loader2 className="mr-1.5 h-3 w-3 animate-spin" />

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/browser-automations/BrowserAutomationsList.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const defaultProps = {
5555
onRun: vi.fn(),
5656
onCreateClick: vi.fn(),
5757
onEditClick: vi.fn(),
58+
onDelete: vi.fn(),
59+
onToggleEnabled: vi.fn(),
5860
};
5961

6062
describe('BrowserAutomationsList permission gating', () => {

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/browser-automations/BrowserAutomationsList.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ interface BrowserAutomationsListProps {
3333
/** When undefined, the create button is hidden (e.g., for manual tasks) */
3434
onCreateClick?: () => void;
3535
onEditClick: (automation: BrowserAutomation) => void;
36+
onDelete: (automationId: string) => void;
37+
onToggleEnabled: (automationId: string, enabled: boolean) => void;
3638
}
3739

3840
export function BrowserAutomationsList({
@@ -42,13 +44,16 @@ export function BrowserAutomationsList({
4244
onRun,
4345
onCreateClick,
4446
onEditClick,
47+
onDelete,
48+
onToggleEnabled,
4549
}: BrowserAutomationsListProps) {
4650
const [expandedId, setExpandedId] = useState<string | null>(null);
4751
const { hasPermission } = usePermissions();
4852
const canCreateIntegration = hasPermission('integration', 'create');
4953
const canUpdateIntegration = hasPermission('integration', 'update');
5054

51-
const nextRun = automations.length > 0 ? getNextScheduledRun() : null;
55+
const hasEnabledAutomations = automations.some((a) => a.isEnabled);
56+
const nextRun = hasEnabledAutomations ? getNextScheduledRun() : null;
5257

5358
return (
5459
<div className="rounded-lg border border-border bg-card overflow-hidden">
@@ -102,6 +107,8 @@ export function BrowserAutomationsList({
102107
}
103108
onRun={() => onRun(automation.id)}
104109
onEdit={() => onEditClick(automation)}
110+
onDelete={() => onDelete(automation.id)}
111+
onToggleEnabled={(enabled) => onToggleEnabled(automation.id, enabled)}
105112
/>
106113
))}
107114
</div>

0 commit comments

Comments
 (0)