refactor: cleanup

This commit is contained in:
zhom
2026-08-03 18:44:24 +04:00
parent 7d82a25107
commit 29cb83d063
32 changed files with 4227 additions and 551 deletions
+18
View File
@@ -237,6 +237,24 @@ export function preflight(profile: BrowserProfile): PreflightResult {
return ELIGIBLE;
}
/**
* Whether this profile could be launched on a remote host.
*
* A strict subset of {@link preflight}: a remote session needs the profile to
* exist in cloud storage in a form a host can read, and nothing more. The bot's
* extra requirement — an exit node — exists because a night of unattended
* traffic from a datacenter address is worse for the profile than not warming
* it, and that reasoning does not apply to a session the user is driving.
*
* Mirrors `remote_launch_profile_rules` in `api_server.rs`, which is
* authoritative; this only avoids offering an action that would be refused.
*/
export function canLaunchRemotely(profile: BrowserProfile): boolean {
const syncMode = profile.sync_mode ?? "Disabled";
if (syncMode === "Disabled" || syncMode === "Encrypted") return false;
return resolvedOs(profile) !== null;
}
export function preflightReason(t: TFunction, result: PreflightResult): string {
switch (result.code) {
case "syncOff":
+44 -8
View File
@@ -101,6 +101,7 @@ import { useBrowserState } from "@/hooks/use-browser-state";
import { useCloudAuth } from "@/hooks/use-cloud-auth";
import { cookieBotScopeFor, useCookieBot } from "@/hooks/use-cookie-bot";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useRemoteHandoff } from "@/hooks/use-remote-handoff";
import { useScrollFade } from "@/hooks/use-scroll-fade";
import { useTableSorting } from "@/hooks/use-table-sorting";
import { useTeamLocks } from "@/hooks/use-team-locks";
@@ -121,6 +122,7 @@ import {
import { DNS_BLOCKLIST_LEVELS } from "@/lib/dns-blocklist-levels";
import { canUseCookieBot } from "@/lib/entitlements";
import { formatRelativeTime } from "@/lib/flag-utils";
import type { RemoteHandoffState } from "@/lib/remote-sessions";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { cn } from "@/lib/utils";
import type {
@@ -273,6 +275,15 @@ interface TableMeta {
isProfileLockedByAnother: (profileId: string) => boolean;
getProfileLockEmail: (profileId: string) => string | undefined;
// Remote execution.
//
// `getRemoteHandoff` is the authoritative answer to "can this be opened
// here", read from the same store the backend gate reads. The team-lock cache
// above cannot serve it: it refreshes on a 30-second poll and says nothing at
// all about a session that has finished but whose work has not been pulled
// back yet.
getRemoteHandoff: (profileId: string) => RemoteHandoffState | null;
// Synchronizer
getProfileSyncInfo: (profileId: string) =>
| {
@@ -1585,6 +1596,10 @@ export function ProfilesDataTable({
const { vpnConfigs } = useVpnEvents();
const { user } = useCloudAuth();
const { isProfileLocked, getLockInfo } = useTeamLocks(user?.id);
// Which profiles cannot be opened on this computer, and why. Event-driven and
// read from the backend's own gate, so the button state and the refusal the
// backend would give can never disagree.
const { handoffFor } = useRemoteHandoff();
// Cookie Bot. Enrolments and live runs both live server-side, so the table
// reads them from the shared store rather than from BrowserProfile.
@@ -2445,6 +2460,9 @@ export function ProfilesDataTable({
getProfileLockEmail: (profileId: string) =>
getLockInfo(profileId)?.lockedByEmail,
// Remote execution
getRemoteHandoff: handoffFor,
// Synchronizer
getProfileSyncInfo: getProfileSyncInfo ?? (() => undefined),
onLaunchWithSync:
@@ -2524,6 +2542,7 @@ export function ProfilesDataTable({
handleCreateCountryProxy,
isProfileLocked,
getLockInfo,
handoffFor,
getProfileSyncInfo,
onLaunchWithSync,
cookieBotUnlocked,
@@ -2725,20 +2744,37 @@ export function ProfilesDataTable({
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const handoff = meta.getRemoteHandoff(profile.id);
// A profile open on the fleet IS running, and the button has to say
// so: it is the control that stops it, and stopping now reaches the
// remote browser rather than looking for a local process that was
// never there.
const isRunningRemotely = handoff === "running";
const isPendingRemotePull = handoff === "pending_sync";
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
(meta.isClient && meta.runningProfiles.has(profile.id)) ||
isRunningRemotely;
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isLockedByAnother = meta.isProfileLockedByAnother(profile.id);
const isSyncing = meta.syncStatuses[profile.id]?.status === "syncing";
const canLaunch =
meta.browserState.canLaunchProfile(profile) &&
!isLockedByAnother &&
!isSyncing;
// A remote session holds the profile lock under its own holder id, so
// `isLockedByAnother` is true for the user's OWN fleet session. That
// must not disable the control that stops it.
const canLaunch = isRunningRemotely
? true
: meta.browserState.canLaunchProfile(profile) &&
!isPendingRemotePull &&
!isLockedByAnother &&
!isSyncing;
const lockEmail = meta.getProfileLockEmail(profile.id);
const tooltipContent = isLockedByAnother
? meta.t("sync.team.cannotLaunchLocked", { email: lockEmail })
: meta.browserState.getLaunchTooltipContent(profile);
const tooltipContent = isRunningRemotely
? meta.t("profiles.remote.runningTooltip")
: isPendingRemotePull
? meta.t("profiles.remote.pendingSyncTooltip")
: isLockedByAnother
? meta.t("sync.team.cannotLaunchLocked", { email: lockEmail })
: meta.browserState.getLaunchTooltipContent(profile);
const handleProfileStop = async (profile: BrowserProfile) => {
meta.setStoppingProfiles((prev: Set<string>) =>
+51
View File
@@ -0,0 +1,51 @@
import { useCallback, useEffect, useState } from "react";
import {
getRemoteHandoffStates,
onRemoteHandoffChanged,
type RemoteHandoffState,
} from "@/lib/remote-sessions";
/**
* Which profiles cannot be opened on this computer right now.
*
* Reads the same store the backend launch gate reads, so the button this
* disables and the refusal the backend would produce can never disagree. That
* matters more than it sounds: the previous signal was the profile-lock cache,
* which refreshes on a 30-second server poll and only refetches on this
* device's own lock events. A profile running on the fleet therefore looked
* launchable for up to half a minute, and a profile whose finished session had
* not been pulled back looked launchable indefinitely.
*
* Updates arrive as an event rather than a poll because every transition that
* can change this already emits one.
*/
export function useRemoteHandoff() {
const [states, setStates] = useState<Record<string, RemoteHandoffState>>({});
const refresh = useCallback(async () => {
try {
setStates(await getRemoteHandoffStates());
} catch (error) {
// Not signed in, or the app is still starting. The backend gate still
// applies; the button is simply not pre-disabled.
console.warn("Could not read remote handoff state:", error);
}
}, []);
useEffect(() => {
void refresh();
const unlisten = onRemoteHandoffChanged(setStates);
return () => {
void unlisten.then((off) => {
off();
});
};
}, [refresh]);
const handoffFor = useCallback(
(profileId: string): RemoteHandoffState | null => states[profileId] ?? null,
[states],
);
return { handoffStates: states, handoffFor, refreshHandoff: refresh };
}
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Name (ZA)",
"newest": "Newest first",
"oldest": "Oldest first"
},
"remote": {
"runningTooltip": "Running on a remote machine. Stop it to bring the profile back here.",
"pendingSyncTooltip": "Downloading what the remote session changed. Available again when it finishes."
}
},
"createProfile": {
@@ -1880,7 +1884,12 @@
"cookieBotUnsupportedPlatform": "The cookie bot cannot run {{platform}} profiles. Only Windows and macOS profiles are supported.",
"cookieBotRequiresExitNode": "Attach a proxy or VPN first. Without one the run would come from a datacenter address, which damages the profile's identity.",
"unknownCode": "Something went wrong: {{code}}",
"cookieBotTouchFingerprintUnsupported": "This profile claims a touch device, which the bot cannot drive. Use a desktop fingerprint."
"cookieBotTouchFingerprintUnsupported": "This profile claims a touch device, which the bot cannot drive. Use a desktop fingerprint.",
"profileRunningRemotely": "This profile is running on a remote machine. Stop the remote session first.",
"profileRemoteSyncPending": "A remote session just finished. Waiting for its changes to download before this profile can open here.",
"profileLockedByMember": "This profile is in use by {{email}}.",
"profileLockedElsewhere": "This profile is in use on another device.",
"profileLockUnavailable": "Could not check whether this profile is in use elsewhere. Check your connection and try again."
},
"rail": {
"profiles": "Profiles",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Nombre (ZA)",
"newest": "Más recientes primero",
"oldest": "Más antiguos primero"
},
"remote": {
"runningTooltip": "Ejecutándose en una máquina remota. Deténlo para recuperar el perfil aquí.",
"pendingSyncTooltip": "Descargando lo que cambió la sesión remota. Disponible de nuevo cuando termine."
}
},
"createProfile": {
@@ -1887,7 +1891,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot no puede ejecutar perfiles de {{platform}}. Solo se admiten perfiles de Windows y macOS.",
"cookieBotRequiresExitNode": "Asigna primero un proxy o una VPN. Sin ninguno, la ejecución saldría desde una dirección de centro de datos, lo que daña la identidad del perfil.",
"unknownCode": "Algo salió mal: {{code}}",
"cookieBotTouchFingerprintUnsupported": "Este perfil declara un dispositivo táctil, que el bot no puede controlar. Usa una huella de escritorio."
"cookieBotTouchFingerprintUnsupported": "Este perfil declara un dispositivo táctil, que el bot no puede controlar. Usa una huella de escritorio.",
"profileRunningRemotely": "Este perfil se está ejecutando en una máquina remota. Detén primero la sesión remota.",
"profileRemoteSyncPending": "Una sesión remota acaba de terminar. Esperando a que se descarguen sus cambios antes de abrir este perfil aquí.",
"profileLockedByMember": "Este perfil está siendo usado por {{email}}.",
"profileLockedElsewhere": "Este perfil está en uso en otro dispositivo.",
"profileLockUnavailable": "No se pudo comprobar si este perfil está en uso en otro lugar. Revisa tu conexión e inténtalo de nuevo."
},
"rail": {
"profiles": "Perfiles",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Nom (ZA)",
"newest": "Plus récents dabord",
"oldest": "Plus anciens dabord"
},
"remote": {
"runningTooltip": "En cours d'exécution sur une machine distante. Arrêtez-la pour récupérer le profil ici.",
"pendingSyncTooltip": "Téléchargement des modifications de la session distante. De nouveau disponible une fois terminé."
}
},
"createProfile": {
@@ -1887,7 +1891,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot ne peut pas exécuter de profils {{platform}}. Seuls les profils Windows et macOS sont pris en charge.",
"cookieBotRequiresExitNode": "Associez d'abord un proxy ou un VPN. Sans cela, l'exécution proviendrait d'une adresse de centre de données, ce qui abîme l'identité du profil.",
"unknownCode": "Une erreur est survenue : {{code}}",
"cookieBotTouchFingerprintUnsupported": "Ce profil déclare un appareil tactile, que le bot ne peut pas piloter. Utilisez une empreinte de bureau."
"cookieBotTouchFingerprintUnsupported": "Ce profil déclare un appareil tactile, que le bot ne peut pas piloter. Utilisez une empreinte de bureau.",
"profileRunningRemotely": "Ce profil s'exécute sur une machine distante. Arrêtez d'abord la session distante.",
"profileRemoteSyncPending": "Une session distante vient de se terminer. Ses modifications doivent être téléchargées avant d'ouvrir ce profil ici.",
"profileLockedByMember": "Ce profil est utilisé par {{email}}.",
"profileLockedElsewhere": "Ce profil est utilisé sur un autre appareil.",
"profileLockUnavailable": "Impossible de vérifier si ce profil est utilisé ailleurs. Vérifiez votre connexion et réessayez."
},
"rail": {
"profiles": "Profils",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "名前 (Z→A)",
"newest": "新しい順",
"oldest": "古い順"
},
"remote": {
"runningTooltip": "リモートマシンで実行中です。停止するとプロファイルがここに戻ります。",
"pendingSyncTooltip": "リモートセッションの変更をダウンロード中です。完了すると再び使用できます。"
}
},
"createProfile": {
@@ -1880,7 +1884,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot は {{platform}} のプロファイルを実行できません。対応しているのは Windows と macOS のプロファイルのみです。",
"cookieBotRequiresExitNode": "先にプロキシまたは VPN を設定してください。設定しないと通信がデータセンターのアドレスから出て、プロファイルの信頼性を損ないます。",
"unknownCode": "エラーが発生しました: {{code}}",
"cookieBotTouchFingerprintUnsupported": "このプロファイルはタッチ端末を名乗っており、ボットは操作できません。デスクトップのフィンガープリントをお使いください。"
"cookieBotTouchFingerprintUnsupported": "このプロファイルはタッチ端末を名乗っており、ボットは操作できません。デスクトップのフィンガープリントをお使いください。",
"profileRunningRemotely": "このプロファイルはリモートマシンで実行中です。先にリモートセッションを停止してください。",
"profileRemoteSyncPending": "リモートセッションが終了しました。この profile をここで開く前に、変更のダウンロードを待っています。",
"profileLockedByMember": "このプロファイルは {{email}} が使用中です。",
"profileLockedElsewhere": "このプロファイルは別のデバイスで使用中です。",
"profileLockUnavailable": "このプロファイルが他で使用中か確認できませんでした。接続を確認して再試行してください。"
},
"rail": {
"profiles": "プロファイル",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "이름 (Z→A)",
"newest": "최신순",
"oldest": "오래된순"
},
"remote": {
"runningTooltip": "원격 머신에서 실행 중입니다. 중지하면 프로필이 여기로 돌아옵니다.",
"pendingSyncTooltip": "원격 세션이 변경한 내용을 내려받는 중입니다. 완료되면 다시 사용할 수 있습니다."
}
},
"createProfile": {
@@ -1880,7 +1884,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot은 {{platform}} 프로필을 실행할 수 없습니다. Windows와 macOS 프로필만 지원합니다.",
"cookieBotRequiresExitNode": "먼저 프록시나 VPN을 연결하세요. 없으면 실행 트래픽이 데이터센터 주소에서 나가 프로필 신뢰도를 해칩니다.",
"unknownCode": "문제가 발생했습니다: {{code}}",
"cookieBotTouchFingerprintUnsupported": "이 프로필은 터치 기기를 표방하며, 봇이 조작할 수 없습니다. 데스크톱 지문을 사용하세요."
"cookieBotTouchFingerprintUnsupported": "이 프로필은 터치 기기를 표방하며, 봇이 조작할 수 없습니다. 데스크톱 지문을 사용하세요.",
"profileRunningRemotely": "이 프로필은 원격 머신에서 실행 중입니다. 먼저 원격 세션을 중지하세요.",
"profileRemoteSyncPending": "원격 세션이 방금 끝났습니다. 이 프로필을 여기서 열기 전에 변경 사항을 내려받는 중입니다.",
"profileLockedByMember": "이 프로필은 {{email}} 님이 사용 중입니다.",
"profileLockedElsewhere": "이 프로필은 다른 기기에서 사용 중입니다.",
"profileLockUnavailable": "이 프로필이 다른 곳에서 사용 중인지 확인할 수 없습니다. 연결을 확인한 뒤 다시 시도하세요."
},
"rail": {
"profiles": "프로필",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Nome (ZA)",
"newest": "Mais recentes primeiro",
"oldest": "Mais antigos primeiro"
},
"remote": {
"runningTooltip": "Em execução numa máquina remota. Pare-a para trazer o perfil de volta para aqui.",
"pendingSyncTooltip": "A transferir o que a sessão remota alterou. Disponível novamente quando terminar."
}
},
"createProfile": {
@@ -1887,7 +1891,12 @@
"cookieBotUnsupportedPlatform": "O Cookie Bot não pode executar perfis de {{platform}}. Somente perfis Windows e macOS são suportados.",
"cookieBotRequiresExitNode": "Anexe primeiro um proxy ou VPN. Sem isso, a execução sairia de um endereço de data center, o que prejudica a identidade do perfil.",
"unknownCode": "Algo deu errado: {{code}}",
"cookieBotTouchFingerprintUnsupported": "Este perfil declara um dispositivo de toque, que o bot não consegue controlar. Use uma impressão digital de computador."
"cookieBotTouchFingerprintUnsupported": "Este perfil declara um dispositivo de toque, que o bot não consegue controlar. Use uma impressão digital de computador.",
"profileRunningRemotely": "Este perfil está em execução numa máquina remota. Pare primeiro a sessão remota.",
"profileRemoteSyncPending": "Uma sessão remota acabou de terminar. A aguardar a transferência das alterações antes de abrir este perfil aqui.",
"profileLockedByMember": "Este perfil está a ser utilizado por {{email}}.",
"profileLockedElsewhere": "Este perfil está a ser utilizado noutro dispositivo.",
"profileLockUnavailable": "Não foi possível verificar se este perfil está a ser utilizado noutro local. Verifique a ligação e tente novamente."
},
"rail": {
"profiles": "Perfis",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Имя (Я–А)",
"newest": "Сначала новые",
"oldest": "Сначала старые"
},
"remote": {
"runningTooltip": "Выполняется на удалённой машине. Остановите, чтобы вернуть профиль сюда.",
"pendingSyncTooltip": "Загружаются изменения удалённого сеанса. Профиль снова будет доступен по завершении."
}
},
"createProfile": {
@@ -1894,7 +1898,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot не может запускать профили {{platform}}. Поддерживаются только профили Windows и macOS.",
"cookieBotRequiresExitNode": "Сначала назначьте прокси или VPN. Без них трафик пойдёт с адреса дата-центра, а это вредит репутации профиля.",
"unknownCode": "Что-то пошло не так: {{code}}",
"cookieBotTouchFingerprintUnsupported": "Этот профиль выдаёт себя за сенсорное устройство, которым бот управлять не может. Используйте настольный отпечаток."
"cookieBotTouchFingerprintUnsupported": "Этот профиль выдаёт себя за сенсорное устройство, которым бот управлять не может. Используйте настольный отпечаток.",
"profileRunningRemotely": "Этот профиль запущен на удалённой машине. Сначала остановите удалённый сеанс.",
"profileRemoteSyncPending": "Удалённый сеанс только что завершился. Дождитесь загрузки его изменений, прежде чем открывать профиль здесь.",
"profileLockedByMember": "Этот профиль используется пользователем {{email}}.",
"profileLockedElsewhere": "Этот профиль используется на другом устройстве.",
"profileLockUnavailable": "Не удалось проверить, используется ли профиль где-то ещё. Проверьте подключение и попробуйте снова."
},
"rail": {
"profiles": "Профили",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Ad (ZA)",
"newest": "Önce en yeni",
"oldest": "Önce en eski"
},
"remote": {
"runningTooltip": "Uzak bir makinede çalışıyor. Profili buraya geri getirmek için durdurun.",
"pendingSyncTooltip": "Uzak oturumun değiştirdikleri indiriliyor. Bittiğinde yeniden kullanılabilir olacak."
}
},
"createProfile": {
@@ -1880,7 +1884,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot, {{platform}} profillerini çalıştıramaz. Yalnızca Windows ve macOS profilleri desteklenir.",
"cookieBotRequiresExitNode": "Önce bir proxy veya VPN ekleyin. Aksi hâlde çalışma bir veri merkezi adresinden çıkar ve bu, profilin kimliğine zarar verir.",
"unknownCode": "Bir sorun oluştu: {{code}}",
"cookieBotTouchFingerprintUnsupported": "Bu profil dokunmatik bir cihaz olduğunu bildiriyor ve bot bunu süremez. Masaüstü parmak izi kullanın."
"cookieBotTouchFingerprintUnsupported": "Bu profil dokunmatik bir cihaz olduğunu bildiriyor ve bot bunu süremez. Masaüstü parmak izi kullanın.",
"profileRunningRemotely": "Bu profil uzak bir makinede çalışıyor. Önce uzak oturumu durdurun.",
"profileRemoteSyncPending": "Uzak oturum az önce bitti. Bu profili burada açmadan önce değişikliklerinin inmesi bekleniyor.",
"profileLockedByMember": "Bu profil {{email}} tarafından kullanılıyor.",
"profileLockedElsewhere": "Bu profil başka bir cihazda kullanılıyor.",
"profileLockUnavailable": "Bu profilin başka bir yerde kullanılıp kullanılmadığı denetlenemedi. Bağlantınızı kontrol edip yeniden deneyin."
},
"rail": {
"profiles": "Profiller",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "Tên (ZA)",
"newest": "Mới nhất trước",
"oldest": "Cũ nhất trước"
},
"remote": {
"runningTooltip": "Đang chạy trên máy từ xa. Dừng lại để đưa hồ sơ về đây.",
"pendingSyncTooltip": "Đang tải về những gì phiên từ xa đã thay đổi. Sẽ dùng lại được khi hoàn tất."
}
},
"createProfile": {
@@ -1880,7 +1884,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot không chạy được hồ sơ {{platform}}. Chỉ hỗ trợ hồ sơ Windows và macOS.",
"cookieBotRequiresExitNode": "Hãy gán proxy hoặc VPN trước. Nếu không, lần chạy sẽ đi ra từ địa chỉ trung tâm dữ liệu, gây hại cho danh tính hồ sơ.",
"unknownCode": "Đã xảy ra lỗi: {{code}}",
"cookieBotTouchFingerprintUnsupported": "Hồ sơ này khai báo là thiết bị cảm ứng, bot không điều khiển được. Hãy dùng vân tay máy tính để bàn."
"cookieBotTouchFingerprintUnsupported": "Hồ sơ này khai báo là thiết bị cảm ứng, bot không điều khiển được. Hãy dùng vân tay máy tính để bàn.",
"profileRunningRemotely": "Hồ sơ này đang chạy trên máy từ xa. Hãy dừng phiên từ xa trước.",
"profileRemoteSyncPending": "Một phiên từ xa vừa kết thúc. Đang chờ tải các thay đổi về trước khi mở hồ sơ này tại đây.",
"profileLockedByMember": "Hồ sơ này đang được {{email}} sử dụng.",
"profileLockedElsewhere": "Hồ sơ này đang được sử dụng trên thiết bị khác.",
"profileLockUnavailable": "Không thể kiểm tra hồ sơ này có đang được dùng ở nơi khác hay không. Hãy kiểm tra kết nối và thử lại."
},
"rail": {
"profiles": "Profile",
+10 -1
View File
@@ -345,6 +345,10 @@
"nameDesc": "名称 (ZA)",
"newest": "最新优先",
"oldest": "最早优先"
},
"remote": {
"runningTooltip": "正在远程计算机上运行。停止后配置文件会回到本机。",
"pendingSyncTooltip": "正在下载远程会话所做的更改。完成后即可再次使用。"
}
},
"createProfile": {
@@ -1880,7 +1884,12 @@
"cookieBotUnsupportedPlatform": "Cookie Bot 无法运行 {{platform}} 配置文件。仅支持 Windows 和 macOS 配置文件。",
"cookieBotRequiresExitNode": "请先绑定代理或 VPN。否则运行会从数据中心地址发出,损害配置文件的身份。",
"unknownCode": "出现问题: {{code}}",
"cookieBotTouchFingerprintUnsupported": "该配置文件声称是触摸设备,机器人无法操作。请使用桌面端指纹。"
"cookieBotTouchFingerprintUnsupported": "该配置文件声称是触摸设备,机器人无法操作。请使用桌面端指纹。",
"profileRunningRemotely": "该配置文件正在远程计算机上运行。请先停止远程会话。",
"profileRemoteSyncPending": "远程会话刚刚结束。正在等待其更改下载完成后才能在此打开该配置文件。",
"profileLockedByMember": "该配置文件正在被 {{email}} 使用。",
"profileLockedElsewhere": "该配置文件正在另一台设备上使用。",
"profileLockUnavailable": "无法检查该配置文件是否正在别处使用。请检查网络连接后重试。"
},
"rail": {
"profiles": "配置文件",
+17
View File
@@ -78,6 +78,11 @@ export type BackendErrorCode =
| "REMOTE_SESSION_CONFLICT"
| "REMOTE_SYNC_IN_PROGRESS"
| "REMOTE_HOURS_EXHAUSTED"
| "PROFILE_RUNNING_REMOTELY"
| "PROFILE_REMOTE_SYNC_PENDING"
| "PROFILE_LOCKED_BY_MEMBER"
| "PROFILE_LOCKED_ELSEWHERE"
| "PROFILE_LOCK_UNAVAILABLE"
| "NOT_TEAM_MEMBER"
| "COOKIE_BOT_NOT_ENTITLED"
| "COOKIE_BOT_NOT_ENROLLED"
@@ -314,6 +319,18 @@ export function translateBackendError(t: TFunction, err: unknown): string {
granted: parsed.params?.granted ?? "0",
used: parsed.params?.used ?? "0",
});
case "PROFILE_RUNNING_REMOTELY":
return t("backendErrors.profileRunningRemotely");
case "PROFILE_REMOTE_SYNC_PENDING":
return t("backendErrors.profileRemoteSyncPending");
case "PROFILE_LOCKED_BY_MEMBER":
return t("backendErrors.profileLockedByMember", {
email: parsed.params?.email ?? "",
});
case "PROFILE_LOCKED_ELSEWHERE":
return t("backendErrors.profileLockedElsewhere");
case "PROFILE_LOCK_UNAVAILABLE":
return t("backendErrors.profileLockUnavailable");
case "NOT_TEAM_MEMBER":
return t("backendErrors.notTeamMember");
case "COOKIE_BOT_NOT_ENTITLED":
+36
View File
@@ -47,6 +47,19 @@ export interface RemoteSessionEnded {
billed_seconds: number;
}
/**
* Why a profile cannot be opened on this computer right now.
*
* - `running`: a browser is open on the fleet holding this profile.
* - `pending_sync`: a session has finished and what it wrote is still being
* pulled down. Opening the local copy now would make the local files look
* newer than the host's push, and the next sync would then upload the stale
* copy over the session's work and delete the rest of it.
*
* Both states are temporary and neither is an error.
*/
export type RemoteHandoffState = "running" | "pending_sync";
/**
* States a session cannot leave under its own steam.
*
@@ -83,8 +96,31 @@ export const REMOTE_SESSION_EVENTS = {
snapshot: "remote-session-snapshot",
/** Stream connectivity. Payload: `RemoteSessionStreamStatus`. */
stream: "remote-session-stream",
/**
* The set of profiles that cannot be launched locally changed.
* Payload: `Record<profileId, RemoteHandoffState>`.
*/
handoff: "remote-handoff-changed",
} as const;
/** Which profiles are blocked from launching locally, and why. */
export function getRemoteHandoffStates(): Promise<
Record<string, RemoteHandoffState>
> {
return invoke<Record<string, RemoteHandoffState>>(
"get_remote_handoff_states",
);
}
export function onRemoteHandoffChanged(
handler: (states: Record<string, RemoteHandoffState>) => void,
): Promise<UnlistenFn> {
return listen<Record<string, RemoteHandoffState>>(
REMOTE_SESSION_EVENTS.handoff,
(event) => handler(event.payload),
);
}
export function listRemoteSessions(): Promise<RemoteSessionState[]> {
return invoke<RemoteSessionState[]>("list_remote_sessions");
}