mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-25 14:02:22 +02:00
[eric] signin: live port hand-off, stalled-wait retry, openswarm:// registered (ENG-381)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01En8dRGsJPLrJCQBEkTH4Mp
This commit is contained in:
co-authored by
Claude Fable 5
parent
3c24c10cbd
commit
fa9196c8af
@@ -31,6 +31,14 @@
|
||||
"electron-builder-squirrel-windows": "^26.8.1"
|
||||
},
|
||||
"build": {
|
||||
"protocols": [
|
||||
{
|
||||
"name": "OpenSwarm",
|
||||
"schemes": [
|
||||
"openswarm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"appId": "com.clusterlabs.openswarm",
|
||||
"productName": "OpenSwarm",
|
||||
"afterPack": "./build/after-pack.js",
|
||||
|
||||
@@ -5,7 +5,7 @@ import EmailIcon from '@mui/icons-material/Email';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchSettings } from '@/shared/state/settingsSlice';
|
||||
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
|
||||
import { OPENSWARM_DEFAULT_PROXY_URL, getBackendPort } from '@/shared/config';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import SignInDialog from '@/app/components/overlays/SignInDialog';
|
||||
@@ -17,6 +17,8 @@ import { googleStartUrl } from '@/shared/googleStartUrl';
|
||||
// setup are tied to a real account. Google hands off through the external browser and lands out-of-band
|
||||
// (the cloud page POSTs the bearer to the local backend), so we poll settings until user_id appears.
|
||||
// Email reuses the proven SignInDialog (magic-link 6-digit code) on top of the beat.
|
||||
const GOOGLE_STALL_MS = 60_000;
|
||||
|
||||
const BeatSignIn: React.FC<{
|
||||
c: ClaudeTokens;
|
||||
onNext: () => void;
|
||||
@@ -28,6 +30,7 @@ const BeatSignIn: React.FC<{
|
||||
const proxyUrl = useAppSelector((s) => s.settings.data.openswarm_proxy_url || OPENSWARM_DEFAULT_PROXY_URL);
|
||||
const installId = useAppSelector((s) => s.settings.data.installation_id ?? '');
|
||||
const [waitingGoogle, setWaitingGoogle] = useState(false);
|
||||
const [stalled, setStalled] = useState(false);
|
||||
const [notReady, setNotReady] = useState(false);
|
||||
const [emailOpen, setEmailOpen] = useState(false);
|
||||
const signedIn = !!userId;
|
||||
@@ -38,11 +41,17 @@ const BeatSignIn: React.FC<{
|
||||
return () => window.clearInterval(id);
|
||||
}, [signedIn, waitingGoogle, dispatch]);
|
||||
|
||||
// The hand-off can die silently (browser blocked the localhost POST, wrong port, the tab was closed), and "Waiting for your browser" forever was the whole bug report. Past a minute, say so, offer the retry, and record it.
|
||||
useEffect(() => {
|
||||
if (signedIn || !waitingGoogle || stalled) return undefined;
|
||||
const id = window.setTimeout(() => { setStalled(true); report('signin', 'google_handoff_stalled'); }, GOOGLE_STALL_MS);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [signedIn, waitingGoogle, stalled]);
|
||||
|
||||
const onGoogle = (): void => {
|
||||
if (signedIn) return;
|
||||
report('signin', 'google_clicked');
|
||||
const localPort = (window as unknown as { __OPENSWARM_PORT__?: number }).__OPENSWARM_PORT__ || 8324;
|
||||
const startUrl = googleStartUrl(proxyUrl, installId, localPort);
|
||||
report('signin', stalled ? 'google_retry_clicked' : 'google_clicked');
|
||||
const startUrl = googleStartUrl(proxyUrl, installId, getBackendPort());
|
||||
// First run is exactly when settings may not have landed yet, so this beat is the likeliest
|
||||
// place to catch a not-ready install. Say so on the row instead of opening a broken page.
|
||||
if (!startUrl) { setNotReady(true); return; }
|
||||
@@ -50,11 +59,12 @@ const BeatSignIn: React.FC<{
|
||||
const api = (window as unknown as { openswarm?: { openExternal?: (u: string) => void } }).openswarm;
|
||||
if (api?.openExternal) api.openExternal(startUrl);
|
||||
else window.open(startUrl, '_blank');
|
||||
setStalled(false);
|
||||
setWaitingGoogle(true);
|
||||
};
|
||||
|
||||
const rows: Array<{ id: string; name: string; icon: React.ReactNode; onClick: () => void; hint?: string }> = [
|
||||
{ id: 'google', name: 'Continue with Google', icon: <GoogleIcon sx={{ fontSize: 20, color: '#4285F4' }} />, onClick: onGoogle, hint: notReady ? 'Still starting up, try again in a second' : (waitingGoogle && !signedIn ? 'Waiting for your browser...' : undefined) },
|
||||
{ id: 'google', name: 'Continue with Google', icon: <GoogleIcon sx={{ fontSize: 20, color: '#4285F4' }} />, onClick: onGoogle, hint: notReady ? 'Still starting up, try again in a second' : stalled ? 'Still waiting. Click to open the sign-in page again' : (waitingGoogle && !signedIn ? 'Waiting for your browser...' : undefined) },
|
||||
{ id: 'email', name: 'Continue with email', icon: <EmailIcon sx={{ fontSize: 20, color: '#6f6e6a' }} />, onClick: () => { if (!signedIn) setEmailOpen(true); } },
|
||||
];
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { activateSignin, fetchSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
|
||||
import { OPENSWARM_DEFAULT_PROXY_URL, getBackendPort } from '@/shared/config';
|
||||
import { report } from '@/shared/serviceClient';
|
||||
import { googleStartUrl } from '@/shared/googleStartUrl';
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function SignInDialog({ onClose, initialStage = 'choose', mandato
|
||||
|
||||
const onGoogle = () => {
|
||||
report('signin', 'google_clicked');
|
||||
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
|
||||
const localPort = getBackendPort();
|
||||
const startUrl = googleStartUrl(cloudBase, installId, localPort);
|
||||
if (!startUrl) {
|
||||
setErrMsg('Still finishing startup. Give it a second and try again.');
|
||||
@@ -116,7 +116,7 @@ export default function SignInDialog({ onClose, initialStage = 'choose', mandato
|
||||
setBusy(true);
|
||||
try {
|
||||
report('signin', 'email_verify_submitted');
|
||||
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
|
||||
const localPort = getBackendPort();
|
||||
const res = await fetch(`${cloudBase}/api/auth/email/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -5,7 +5,7 @@ import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { signOut } from '@/shared/state/settingsSlice';
|
||||
import { OPENSWARM_DEFAULT_PROXY_URL } from '@/shared/config';
|
||||
import { OPENSWARM_DEFAULT_PROXY_URL, getBackendPort } from '@/shared/config';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import SignInDialog from '@/app/components/overlays/SignInDialog';
|
||||
import { googleStartUrl } from '@/shared/googleStartUrl';
|
||||
@@ -46,7 +46,7 @@ const AccountCard: React.FC = () => {
|
||||
|
||||
const onSignIn = () => {
|
||||
// Pass local_port so the bearer-handoff page POSTs to the right backend (Electron binds in 8324..8424).
|
||||
const localPort = (window as any).__OPENSWARM_PORT__ || 8324;
|
||||
const localPort = getBackendPort();
|
||||
const startUrl = googleStartUrl(proxyUrl, installId, localPort);
|
||||
// No error surface on this card, so the honest move is to do nothing visible rather than send
|
||||
// them to a black cloud error page. Settings land within a second and the click then works.
|
||||
|
||||
@@ -10,6 +10,13 @@ const port =
|
||||
8324;
|
||||
const host = window.location.hostname || 'localhost';
|
||||
|
||||
/** The backend port as of NOW. Prefer this over the boot-time snapshot when the number leaves the app (the Google sign-in hand-off POSTs the bearer back to it): preload captures `__OPENSWARM_PORT__` once, and a slow first-run port scan leaves it null, which used to send the cloud to a bare 8324 nobody was listening on. */
|
||||
export function getBackendPort(): number {
|
||||
const ow = _w.openswarm;
|
||||
const live = ow && typeof ow.getBackendPortLive === 'function' ? ow.getBackendPortLive() : null;
|
||||
return typeof live === 'number' && live > 0 ? live : port;
|
||||
}
|
||||
|
||||
export const API_BASE = `http://${host}:${port}/api`;
|
||||
export const WS_BASE = `ws://${host}:${port}`;
|
||||
// Must match openswarm-cloud's PUBLIC_BASE_URL (fly.toml) and the Google OAuth redirect URI.
|
||||
|
||||
Reference in New Issue
Block a user