@@ -36,15 +36,14 @@ interface PairingCodeResponse {
3636async 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
5049export 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-
293223function 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