feat: batch profile launch/stop for paid users

This commit is contained in:
zhom
2026-06-24 00:07:43 +04:00
parent 50d2834634
commit 4007dedcf0
14 changed files with 753 additions and 10 deletions
+119
View File
@@ -228,6 +228,10 @@ export default function Home() {
// Cloud auth for cross-OS unlock
const { user: cloudUser } = useCloudAuth();
const crossOsUnlocked = getEntitlements(cloudUser).crossOsFingerprints;
// Bulk run/stop is a paid (browser automation) feature, matching the
// /v1/profiles/batch/run API gate. Free/starter users see the bulk Run/Stop
// actions disabled with a Pro badge.
const automationUnlocked = getEntitlements(cloudUser).browserAutomation;
const [selfHostedSyncConfigured, setSelfHostedSyncConfigured] =
useState(false);
@@ -1128,6 +1132,75 @@ export default function Home() {
setCookieCopyDialogOpen(true);
}, [selectedProfiles, profiles, t]);
const [pendingBulkAction, setPendingBulkAction] = useState<{
action: "run" | "stop";
profiles: BrowserProfile[];
} | null>(null);
const [isBulkActing, setIsBulkActing] = useState(false);
const executeBulkRun = useCallback(
async (targets: BrowserProfile[]) => {
setIsBulkActing(true);
try {
await Promise.allSettled(targets.map((p) => launchProfile(p)));
setSelectedProfiles([]);
} finally {
setIsBulkActing(false);
setPendingBulkAction(null);
}
},
[launchProfile],
);
const executeBulkStop = useCallback(
async (targets: BrowserProfile[]) => {
setIsBulkActing(true);
try {
await Promise.allSettled(targets.map((p) => handleKillProfile(p)));
setSelectedProfiles([]);
} finally {
setIsBulkActing(false);
setPendingBulkAction(null);
}
},
[handleKillProfile],
);
// Bulk run/stop only touch eligible profiles (run: not already running;
// stop: currently running). An empty result shows a toast instead of a silent
// no-op (guard), and 10+ targets require confirmation before launching/stopping.
const handleBulkRun = useCallback(() => {
if (selectedProfiles.length === 0) return;
const targets = profiles.filter(
(p) => selectedProfiles.includes(p.id) && !runningProfiles.has(p.id),
);
if (targets.length === 0) {
showErrorToast(t("profiles.bulkRun.noneToRun"));
return;
}
if (targets.length >= 10) {
setPendingBulkAction({ action: "run", profiles: targets });
return;
}
void executeBulkRun(targets);
}, [selectedProfiles, profiles, runningProfiles, executeBulkRun, t]);
const handleBulkStop = useCallback(() => {
if (selectedProfiles.length === 0) return;
const targets = profiles.filter(
(p) => selectedProfiles.includes(p.id) && runningProfiles.has(p.id),
);
if (targets.length === 0) {
showErrorToast(t("profiles.bulkStop.noneToStop"));
return;
}
if (targets.length >= 10) {
setPendingBulkAction({ action: "stop", profiles: targets });
return;
}
void executeBulkStop(targets);
}, [selectedProfiles, profiles, runningProfiles, executeBulkStop, t]);
const handleCopyCookiesToProfile = useCallback((profile: BrowserProfile) => {
setSelectedProfilesForCookies([profile.id]);
setCookieCopyDialogOpen(true);
@@ -1569,6 +1642,9 @@ export default function Home() {
onBulkGroupAssignment={handleBulkGroupAssignment}
onBulkProxyAssignment={handleBulkProxyAssignment}
onBulkCopyCookies={handleBulkCopyCookies}
onBulkRun={handleBulkRun}
onBulkStop={handleBulkStop}
bulkActionsUnlocked={automationUnlocked}
onBulkExtensionGroupAssignment={
handleBulkExtensionGroupAssignment
}
@@ -1868,6 +1944,49 @@ export default function Home() {
profile={currentProfileForCookieManagement}
/>
<DeleteConfirmationDialog
isOpen={pendingBulkAction !== null}
onClose={() => {
setPendingBulkAction(null);
}}
onConfirm={() => {
if (!pendingBulkAction) return;
if (pendingBulkAction.action === "run") {
void executeBulkRun(pendingBulkAction.profiles);
} else {
void executeBulkStop(pendingBulkAction.profiles);
}
}}
title={
pendingBulkAction?.action === "stop"
? t("profiles.bulkStop.confirmTitle", {
count: pendingBulkAction?.profiles.length ?? 0,
})
: t("profiles.bulkRun.confirmTitle", {
count: pendingBulkAction?.profiles.length ?? 0,
})
}
description={
pendingBulkAction?.action === "stop"
? t("profiles.bulkStop.confirmDescription", {
count: pendingBulkAction?.profiles.length ?? 0,
})
: t("profiles.bulkRun.confirmDescription", {
count: pendingBulkAction?.profiles.length ?? 0,
})
}
confirmButtonText={
pendingBulkAction?.action === "stop"
? t("profiles.bulkStop.confirmButton", {
count: pendingBulkAction?.profiles.length ?? 0,
})
: t("profiles.bulkRun.confirmButton", {
count: pendingBulkAction?.profiles.length ?? 0,
})
}
confirmButtonVariant="default"
isLoading={isBulkActing}
/>
<DeleteConfirmationDialog
isOpen={showBulkDeleteConfirmation}
onClose={() => {
@@ -19,6 +19,12 @@ interface DeleteConfirmationDialogProps {
title: string;
description: string;
confirmButtonText?: string;
confirmButtonVariant?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost";
isLoading?: boolean;
profileIds?: string[];
profiles?: { id: string; name: string }[];
@@ -31,6 +37,7 @@ export function DeleteConfirmationDialog({
title,
description,
confirmButtonText,
confirmButtonVariant = "destructive",
isLoading = false,
profileIds,
profiles = [],
@@ -79,7 +86,7 @@ export function DeleteConfirmationDialog({
{t("common.buttons.cancel")}
</RippleButton>
<LoadingButton
variant="destructive"
variant={confirmButtonVariant}
onClick={() => void handleConfirm()}
isLoading={isLoading}
>
+45
View File
@@ -56,6 +56,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ProBadge } from "@/components/ui/pro-badge";
import {
Table,
TableBody,
@@ -1134,6 +1135,9 @@ interface ProfilesDataTableProps {
onBulkGroupAssignment?: () => void;
onBulkProxyAssignment?: () => void;
onBulkCopyCookies?: () => void;
onBulkRun?: () => void;
onBulkStop?: () => void;
bulkActionsUnlocked?: boolean;
onBulkExtensionGroupAssignment?: () => void;
onAssignExtensionGroup?: (profileIds: string[]) => void;
onOpenProfileSyncDialog?: (profile: BrowserProfile) => void;
@@ -1179,6 +1183,9 @@ export function ProfilesDataTable({
onBulkGroupAssignment,
onBulkProxyAssignment,
onBulkCopyCookies,
onBulkRun,
onBulkStop,
bulkActionsUnlocked = false,
onBulkExtensionGroupAssignment,
onAssignExtensionGroup,
onOpenProfileSyncDialog,
@@ -3223,6 +3230,44 @@ export function ProfilesDataTable({
})()}
<DataTableActionBar table={table}>
<DataTableActionBarSelection table={table} />
{onBulkRun && (
<span className="relative inline-flex">
<DataTableActionBarAction
tooltip={
bulkActionsUnlocked
? t("profiles.actionBar.runSelected")
: t("profiles.actionBar.proRequired")
}
onClick={bulkActionsUnlocked ? onBulkRun : undefined}
disabled={!bulkActionsUnlocked}
size="icon"
>
<LuPlay className="fill-current" />
</DataTableActionBarAction>
{!bulkActionsUnlocked && (
<ProBadge className="absolute -top-2 -right-2 pointer-events-none" />
)}
</span>
)}
{onBulkStop && (
<span className="relative inline-flex">
<DataTableActionBarAction
tooltip={
bulkActionsUnlocked
? t("profiles.actionBar.stopSelected")
: t("profiles.actionBar.proRequired")
}
onClick={bulkActionsUnlocked ? onBulkStop : undefined}
disabled={!bulkActionsUnlocked}
size="icon"
>
<LuSquare className="fill-current" />
</DataTableActionBarAction>
{!bulkActionsUnlocked && (
<ProBadge className="absolute -top-2 -right-2 pointer-events-none" />
)}
</span>
)}
{onBulkGroupAssignment && (
<DataTableActionBarAction
tooltip={t("profiles.actionBar.assignToGroup")}
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "Assign to Group",
"assignProxy": "Assign Proxy",
"assignExtensionGroup": "Assign Extension Group",
"copyCookies": "Copy Cookies"
"copyCookies": "Copy Cookies",
"runSelected": "Run selected",
"stopSelected": "Stop selected",
"proRequired": "Pro plan required for bulk run/stop"
},
"passwordProtectedBadge": "Password Protected",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "Run {{count}} profiles?",
"confirmDescription": "Launching {{count}} profiles at once can use a lot of system resources. Continue?",
"confirmButton": "Run {{count}}",
"noneToRun": "No selected profiles to run."
},
"bulkStop": {
"confirmTitle": "Stop {{count}} profiles?",
"confirmDescription": "Stop {{count}} running profiles?",
"confirmButton": "Stop {{count}}",
"noneToStop": "No selected profiles to stop."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "Asignar a grupo",
"assignProxy": "Asignar proxy",
"assignExtensionGroup": "Asignar grupo de extensiones",
"copyCookies": "Copiar cookies"
"copyCookies": "Copiar cookies",
"runSelected": "Ejecutar seleccionados",
"stopSelected": "Detener seleccionados",
"proRequired": "Se requiere el plan Pro para ejecución/parada masiva"
},
"passwordProtectedBadge": "Protegido por Contraseña",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "¿Ejecutar {{count}} perfiles?",
"confirmDescription": "Iniciar {{count}} perfiles a la vez puede consumir muchos recursos del sistema. ¿Continuar?",
"confirmButton": "Ejecutar {{count}}",
"noneToRun": "No hay perfiles seleccionados para ejecutar."
},
"bulkStop": {
"confirmTitle": "¿Detener {{count}} perfiles?",
"confirmDescription": "¿Detener {{count}} perfiles en ejecución?",
"confirmButton": "Detener {{count}}",
"noneToStop": "No hay perfiles seleccionados para detener."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "Assigner à un groupe",
"assignProxy": "Assigner un proxy",
"assignExtensionGroup": "Assigner un groupe dextensions",
"copyCookies": "Copier les cookies"
"copyCookies": "Copier les cookies",
"runSelected": "Lancer la sélection",
"stopSelected": "Arrêter la sélection",
"proRequired": "Plan Pro requis pour le lancement/arrêt groupé"
},
"passwordProtectedBadge": "Protégé par mot de passe",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "Lancer {{count}} profils ?",
"confirmDescription": "Lancer {{count}} profils à la fois peut consommer beaucoup de ressources système. Continuer ?",
"confirmButton": "Lancer {{count}}",
"noneToRun": "Aucun profil sélectionné à lancer."
},
"bulkStop": {
"confirmTitle": "Arrêter {{count}} profils ?",
"confirmDescription": "Arrêter {{count}} profils en cours dexécution ?",
"confirmButton": "Arrêter {{count}}",
"noneToStop": "Aucun profil sélectionné à arrêter."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "グループに割り当て",
"assignProxy": "プロキシを割り当て",
"assignExtensionGroup": "拡張機能グループを割り当て",
"copyCookies": "Cookieをコピー"
"copyCookies": "Cookieをコピー",
"runSelected": "選択を実行",
"stopSelected": "選択を停止",
"proRequired": "一括実行・停止には Pro プランが必要です"
},
"passwordProtectedBadge": "パスワード保護",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "{{count}} 個のプロファイルを実行しますか?",
"confirmDescription": "{{count}} 個のプロファイルを一度に起動すると、システムリソースを大量に消費する可能性があります。続行しますか?",
"confirmButton": "{{count}} 個を実行",
"noneToRun": "実行する選択中のプロファイルがありません。"
},
"bulkStop": {
"confirmTitle": "{{count}} 個のプロファイルを停止しますか?",
"confirmDescription": "実行中の {{count}} 個のプロファイルを停止しますか?",
"confirmButton": "{{count}} 個を停止",
"noneToStop": "停止する選択中のプロファイルがありません。"
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "그룹에 할당",
"assignProxy": "프록시 할당",
"assignExtensionGroup": "확장 프로그램 그룹 할당",
"copyCookies": "쿠키 복사"
"copyCookies": "쿠키 복사",
"runSelected": "선택 실행",
"stopSelected": "선택 중지",
"proRequired": "대량 실행/중지하려면 Pro 플랜이 필요합니다"
},
"passwordProtectedBadge": "비밀번호 보호됨",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "{{count}}개의 프로필을 실행할까요?",
"confirmDescription": "{{count}}개의 프로필을 한 번에 실행하면 시스템 리소스를 많이 사용할 수 있습니다. 계속할까요?",
"confirmButton": "{{count}}개 실행",
"noneToRun": "실행할 선택된 프로필이 없습니다."
},
"bulkStop": {
"confirmTitle": "{{count}}개의 프로필을 중지할까요?",
"confirmDescription": "실행 중인 {{count}}개의 프로필을 중지할까요?",
"confirmButton": "{{count}}개 중지",
"noneToStop": "중지할 선택된 프로필이 없습니다."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "Atribuir a grupo",
"assignProxy": "Atribuir proxy",
"assignExtensionGroup": "Atribuir grupo de extensões",
"copyCookies": "Copiar cookies"
"copyCookies": "Copiar cookies",
"runSelected": "Executar selecionados",
"stopSelected": "Parar selecionados",
"proRequired": "Plano Pro necessário para execução/parada em massa"
},
"passwordProtectedBadge": "Protegido por Senha",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "Executar {{count}} perfis?",
"confirmDescription": "Iniciar {{count}} perfis de uma vez pode consumir muitos recursos do sistema. Continuar?",
"confirmButton": "Executar {{count}}",
"noneToRun": "Nenhum perfil selecionado para executar."
},
"bulkStop": {
"confirmTitle": "Parar {{count}} perfis?",
"confirmDescription": "Parar {{count}} perfis em execução?",
"confirmButton": "Parar {{count}}",
"noneToStop": "Nenhum perfil selecionado para parar."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "Назначить группе",
"assignProxy": "Назначить прокси",
"assignExtensionGroup": "Назначить группу расширений",
"copyCookies": "Копировать cookies"
"copyCookies": "Копировать cookies",
"runSelected": "Запустить выбранные",
"stopSelected": "Остановить выбранные",
"proRequired": "Для массового запуска/остановки требуется план Pro"
},
"passwordProtectedBadge": "Защищено паролем",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "Запустить {{count}} профилей?",
"confirmDescription": "Одновременный запуск {{count}} профилей может потребовать много системных ресурсов. Продолжить?",
"confirmButton": "Запустить {{count}}",
"noneToRun": "Нет выбранных профилей для запуска."
},
"bulkStop": {
"confirmTitle": "Остановить {{count}} профилей?",
"confirmDescription": "Остановить {{count}} запущенных профилей?",
"confirmButton": "Остановить {{count}}",
"noneToStop": "Нет выбранных профилей для остановки."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "Gán vào nhóm",
"assignProxy": "Gán Proxy",
"assignExtensionGroup": "Gán nhóm tiện ích",
"copyCookies": "Sao chép Cookie"
"copyCookies": "Sao chép Cookie",
"runSelected": "Chạy mục đã chọn",
"stopSelected": "Dừng mục đã chọn",
"proRequired": "Cần gói Pro để chạy/dừng hàng loạt"
},
"passwordProtectedBadge": "Được bảo vệ bằng mật khẩu",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "Chạy {{count}} hồ sơ?",
"confirmDescription": "Khởi chạy {{count}} hồ sơ cùng lúc có thể tiêu tốn nhiều tài nguyên hệ thống. Tiếp tục?",
"confirmButton": "Chạy {{count}}",
"noneToRun": "Không có hồ sơ nào được chọn để chạy."
},
"bulkStop": {
"confirmTitle": "Dừng {{count}} hồ sơ?",
"confirmDescription": "Dừng {{count}} hồ sơ đang chạy?",
"confirmButton": "Dừng {{count}}",
"noneToStop": "Không có hồ sơ nào được chọn để dừng."
}
},
"createProfile": {
+16 -1
View File
@@ -305,11 +305,26 @@
"assignToGroup": "分配到分组",
"assignProxy": "分配代理",
"assignExtensionGroup": "分配扩展分组",
"copyCookies": "复制 Cookie"
"copyCookies": "复制 Cookie",
"runSelected": "运行所选",
"stopSelected": "停止所选",
"proRequired": "批量运行/停止需要 Pro 套餐"
},
"passwordProtectedBadge": "密码保护",
"launchHook": {
"placeholder": "https://example.com/track-launch"
},
"bulkRun": {
"confirmTitle": "运行 {{count}} 个配置文件?",
"confirmDescription": "一次启动 {{count}} 个配置文件可能会占用大量系统资源。是否继续?",
"confirmButton": "运行 {{count}} 个",
"noneToRun": "没有选中可运行的配置文件。"
},
"bulkStop": {
"confirmTitle": "停止 {{count}} 个配置文件?",
"confirmDescription": "停止 {{count}} 个正在运行的配置文件?",
"confirmButton": "停止 {{count}} 个",
"noneToStop": "没有选中可停止的配置文件。"
}
},
"createProfile": {