Skip to content

Commit ff7e823

Browse files
committed
simplify gateway UI: stop/start just works
Cleaned up the telegram card to three clear states: - No config: shows token input + Start - Stopped (has saved config): shows Start + trash to remove - Running: shows Pair Device + Stop Start after stop calls /gateway/restart which uses the saved token. No need to re-enter it.
1 parent e850b81 commit ff7e823

File tree

1 file changed

+96
-151
lines changed

1 file changed

+96
-151
lines changed

ui/desktop/src/components/settings/gateways/GatewaySettingsSection.tsx

Lines changed: 96 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ interface PairingCodeResponse {
3636
async function gatewayFetch(endpoint: string, options: globalThis.RequestInit = {}) {
3737
const secretKey = await window.electron.getSecretKey();
3838
const url = getApiUrl(endpoint);
39-
const response = await fetch(url, {
39+
return fetch(url, {
4040
...options,
4141
headers: {
4242
'Content-Type': 'application/json',
4343
'X-Secret-Key': secretKey,
4444
...options.headers,
4545
},
4646
});
47-
return response;
4847
}
4948

5049
export default function GatewaySettingsSection() {
@@ -59,8 +58,7 @@ export default function GatewaySettingsSection() {
5958
try {
6059
const response = await gatewayFetch('/gateway/status');
6160
if (response.ok) {
62-
const data: GatewayStatus[] = await response.json();
63-
setGateways(data);
61+
setGateways(await response.json());
6462
}
6563
} catch (err) {
6664
console.error('Failed to fetch gateway status:', err);
@@ -75,99 +73,20 @@ export default function GatewaySettingsSection() {
7573
return () => clearInterval(interval);
7674
}, [fetchStatus]);
7775

78-
const findGateway = (type: string) => gateways.find((g) => g.gateway_type === type);
79-
80-
const handleStopGateway = async (gatewayType: string) => {
81-
setError(null);
82-
try {
83-
const response = await gatewayFetch('/gateway/stop', {
84-
method: 'POST',
85-
body: JSON.stringify({ gateway_type: gatewayType }),
86-
});
87-
if (!response.ok) {
88-
const data = await response.json().catch(() => ({}));
89-
throw new Error(data.message || 'Failed to stop gateway');
90-
}
91-
await fetchStatus();
92-
} catch (err) {
93-
setError(err instanceof Error ? err.message : 'Failed to stop gateway');
94-
}
95-
};
96-
97-
const handleStartGateway = async (
98-
gatewayType: string,
99-
platformConfig: Record<string, unknown>
100-
) => {
76+
const doPost = async (endpoint: string, body: object, errorMsg: string) => {
10177
setError(null);
10278
try {
103-
const response = await gatewayFetch('/gateway/start', {
79+
const response = await gatewayFetch(endpoint, {
10480
method: 'POST',
105-
body: JSON.stringify({
106-
gateway_type: gatewayType,
107-
platform_config: platformConfig,
108-
max_sessions: 0,
109-
}),
81+
body: JSON.stringify(body),
11082
});
11183
if (!response.ok) {
11284
const data = await response.json().catch(() => ({}));
113-
throw new Error(data.message || 'Failed to start gateway');
85+
throw new Error(data.message || errorMsg);
11486
}
11587
await fetchStatus();
11688
} catch (err) {
117-
setError(err instanceof Error ? err.message : 'Failed to start gateway');
118-
}
119-
};
120-
121-
const handleRestartGateway = async (gatewayType: string) => {
122-
setError(null);
123-
try {
124-
const response = await gatewayFetch('/gateway/restart', {
125-
method: 'POST',
126-
body: JSON.stringify({ gateway_type: gatewayType }),
127-
});
128-
if (!response.ok) {
129-
const data = await response.json().catch(() => ({}));
130-
throw new Error(data.message || 'Failed to restart gateway');
131-
}
132-
await fetchStatus();
133-
} catch (err) {
134-
setError(err instanceof Error ? err.message : 'Failed to restart gateway');
135-
}
136-
};
137-
138-
const handleRemoveGateway = async (gatewayType: string) => {
139-
setError(null);
140-
try {
141-
const response = await gatewayFetch('/gateway/remove', {
142-
method: 'POST',
143-
body: JSON.stringify({ gateway_type: gatewayType }),
144-
});
145-
if (!response.ok) {
146-
const data = await response.json().catch(() => ({}));
147-
throw new Error(data.message || 'Failed to remove gateway');
148-
}
149-
await fetchStatus();
150-
} catch (err) {
151-
setError(err instanceof Error ? err.message : 'Failed to remove gateway');
152-
}
153-
};
154-
155-
const handleGeneratePairingCode = async (gatewayType: string) => {
156-
setError(null);
157-
try {
158-
const response = await gatewayFetch('/gateway/pair', {
159-
method: 'POST',
160-
body: JSON.stringify({ gateway_type: gatewayType }),
161-
});
162-
if (!response.ok) {
163-
const data = await response.json().catch(() => ({}));
164-
throw new Error(data.message || 'Failed to generate pairing code');
165-
}
166-
const data: PairingCodeResponse = await response.json();
167-
setPairingCode(data);
168-
setPairingGatewayType(gatewayType);
169-
} catch (err) {
170-
setError(err instanceof Error ? err.message : 'Failed to generate pairing code');
89+
setError(err instanceof Error ? err.message : errorMsg);
17190
}
17291
};
17392

@@ -206,6 +125,8 @@ export default function GatewaySettingsSection() {
206125
);
207126
}
208127

128+
const telegram = gateways.find((g) => g.gateway_type === 'telegram');
129+
209130
return (
210131
<>
211132
{error && (
@@ -215,12 +136,37 @@ export default function GatewaySettingsSection() {
215136
)}
216137

217138
<TelegramGatewayCard
218-
status={findGateway('telegram')}
219-
onStart={(config) => handleStartGateway('telegram', config)}
220-
onStop={() => handleStopGateway('telegram')}
221-
onRestart={() => handleRestartGateway('telegram')}
222-
onRemove={() => handleRemoveGateway('telegram')}
223-
onGenerateCode={() => handleGeneratePairingCode('telegram')}
139+
status={telegram}
140+
onStart={(config) =>
141+
doPost('/gateway/start', { gateway_type: 'telegram', platform_config: config, max_sessions: 0 }, 'Failed to start')
142+
}
143+
onRestart={() => doPost('/gateway/restart', { gateway_type: 'telegram' }, 'Failed to start')}
144+
onStop={() => doPost('/gateway/stop', { gateway_type: 'telegram' }, 'Failed to stop')}
145+
onRemove={() => doPost('/gateway/remove', { gateway_type: 'telegram' }, 'Failed to remove')}
146+
onGenerateCode={() =>
147+
doPost('/gateway/pair', { gateway_type: 'telegram' }, 'Failed to generate code').then(
148+
// re-fetch to get code — actually we need the response
149+
() => {}
150+
)
151+
}
152+
onGenerateCodeDirect={async () => {
153+
setError(null);
154+
try {
155+
const response = await gatewayFetch('/gateway/pair', {
156+
method: 'POST',
157+
body: JSON.stringify({ gateway_type: 'telegram' }),
158+
});
159+
if (!response.ok) {
160+
const data = await response.json().catch(() => ({}));
161+
throw new Error(data.message || 'Failed to generate pairing code');
162+
}
163+
const data: PairingCodeResponse = await response.json();
164+
setPairingCode(data);
165+
setPairingGatewayType('telegram');
166+
} catch (err) {
167+
setError(err instanceof Error ? err.message : 'Failed to generate pairing code');
168+
}
169+
}}
224170
onUnpairUser={handleUnpairUser}
225171
/>
226172

@@ -274,87 +220,86 @@ function PairedUsersList({
274220
);
275221
}
276222

277-
function RunningBadge() {
278-
return (
279-
<span className="inline-flex items-center gap-1 text-xs text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-2 py-0.5 rounded-full">
280-
Running
281-
</span>
282-
);
283-
}
284-
285-
function StoppedBadge() {
286-
return (
287-
<span className="inline-flex items-center gap-1 text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/30 px-2 py-0.5 rounded-full">
288-
Stopped
289-
</span>
290-
);
291-
}
292-
293223
function TelegramGatewayCard({
294224
status,
295225
onStart,
296-
onStop,
297226
onRestart,
227+
onStop,
298228
onRemove,
299-
onGenerateCode,
229+
onGenerateCodeDirect,
300230
onUnpairUser,
301231
}: {
302232
status: GatewayStatus | undefined;
303233
onStart: (config: Record<string, unknown>) => Promise<void>;
304-
onStop: () => void;
305-
onRestart: () => void;
306-
onRemove: () => void;
234+
onRestart: () => Promise<void>;
235+
onStop: () => Promise<void>;
236+
onRemove: () => Promise<void>;
307237
onGenerateCode: () => void;
238+
onGenerateCodeDirect: () => void;
308239
onUnpairUser: (platform: string, userId: string) => void;
309240
}) {
310241
const [botToken, setBotToken] = useState('');
311-
const [starting, setStarting] = useState(false);
242+
const [busy, setBusy] = useState(false);
312243
const running = status?.running ?? false;
313244
const configured = status?.configured ?? false;
314245

315-
const handleStart = async () => {
246+
const wrap = (fn: () => Promise<void>) => async () => {
247+
setBusy(true);
248+
try { await fn(); } finally { setBusy(false); }
249+
};
250+
251+
const handleFirstStart = wrap(async () => {
316252
if (!botToken.trim()) return;
317-
setStarting(true);
318253
await onStart({ bot_token: botToken.trim() });
319254
setBotToken('');
320-
setStarting(false);
321-
};
255+
});
322256

323257
return (
324258
<Card className="rounded-lg">
325259
<CardHeader className="pb-0">
326260
<div className="flex items-center justify-between">
327261
<CardTitle className="flex items-center gap-2">
328262
Telegram
329-
{running && <RunningBadge />}
330-
{!running && configured && <StoppedBadge />}
263+
{running && (
264+
<span className="inline-flex items-center text-xs text-green-700 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-2 py-0.5 rounded-full">
265+
Running
266+
</span>
267+
)}
268+
{!running && configured && (
269+
<span className="inline-flex items-center text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-100 dark:bg-yellow-900/30 px-2 py-0.5 rounded-full">
270+
Stopped
271+
</span>
272+
)}
331273
</CardTitle>
332-
{running && (
333-
<div className="flex items-center gap-2">
334-
<Button variant="outline" size="sm" onClick={onGenerateCode}>
335-
Pair Device
336-
</Button>
337-
<Button variant="destructive" size="sm" onClick={onStop}>
338-
<Square className="h-3 w-3 mr-1" />
339-
Stop
340-
</Button>
341-
</div>
342-
)}
343-
{!running && configured && (
344-
<div className="flex items-center gap-2">
345-
<Button size="sm" onClick={onRestart}>
346-
Start
347-
</Button>
348-
<Button
349-
variant="ghost"
350-
size="sm"
351-
onClick={onRemove}
352-
className="text-text-muted hover:text-red-600"
353-
>
354-
<Trash2 className="h-3 w-3" />
355-
</Button>
356-
</div>
357-
)}
274+
<div className="flex items-center gap-2">
275+
{running && (
276+
<>
277+
<Button variant="outline" size="sm" onClick={onGenerateCodeDirect}>
278+
Pair Device
279+
</Button>
280+
<Button variant="destructive" size="sm" disabled={busy} onClick={wrap(onStop)}>
281+
<Square className="h-3 w-3 mr-1" />
282+
Stop
283+
</Button>
284+
</>
285+
)}
286+
{!running && configured && (
287+
<>
288+
<Button size="sm" disabled={busy} onClick={wrap(onRestart)}>
289+
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Start'}
290+
</Button>
291+
<Button
292+
variant="ghost"
293+
size="sm"
294+
disabled={busy}
295+
onClick={wrap(onRemove)}
296+
className="text-text-muted hover:text-red-600"
297+
>
298+
<Trash2 className="h-3 w-3" />
299+
</Button>
300+
</>
301+
)}
302+
</div>
358303
</div>
359304
</CardHeader>
360305
<CardContent className="pt-3 space-y-2">
@@ -366,11 +311,11 @@ function TelegramGatewayCard({
366311
placeholder="Bot token from @BotFather"
367312
value={botToken}
368313
onChange={(e) => setBotToken(e.target.value)}
369-
onKeyDown={(e) => e.key === 'Enter' && handleStart()}
314+
onKeyDown={(e) => e.key === 'Enter' && handleFirstStart()}
370315
className="text-sm"
371316
/>
372-
<Button size="sm" onClick={handleStart} disabled={starting || !botToken.trim()}>
373-
{starting ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Start'}
317+
<Button size="sm" onClick={handleFirstStart} disabled={busy || !botToken.trim()}>
318+
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Start'}
374319
</Button>
375320
</div>
376321
<p className="text-xs text-text-muted">

0 commit comments

Comments
 (0)