77 approvedOperations,
88 completedOperations,
99 activeOperations,
10+ operations,
1011 hasOperations,
1112 hasPendingOperations,
1213 hasApprovedOperations,
@@ -21,15 +22,55 @@ const {
2122 refreshState,
2223} = useConnector ()
2324
24- const { settings } = useSettings ()
25-
2625const isExecuting = shallowRef (false )
2726const otpInput = shallowRef (' ' )
27+ const otpError = shallowRef (' ' )
28+
29+ const authUrl = computed (() => {
30+ const op = operations .value .find (o => o .status === ' running' && o .authUrl )
31+ return op ?.authUrl ?? null
32+ })
33+
34+ const authPollTimer = shallowRef <ReturnType <typeof setInterval > | null >(null )
35+
36+ function startAuthPolling() {
37+ stopAuthPolling ()
38+ let remaining = 3
39+ authPollTimer .value = setInterval (async () => {
40+ try {
41+ await refreshState ()
42+ } catch {
43+ stopAuthPolling ()
44+ return
45+ }
46+ remaining --
47+ if (remaining <= 0 ) {
48+ stopAuthPolling ()
49+ }
50+ }, 20000 )
51+ }
52+
53+ function stopAuthPolling() {
54+ if (authPollTimer .value ) {
55+ clearInterval (authPollTimer .value )
56+ authPollTimer .value = null
57+ }
58+ }
59+
60+ onUnmounted (stopAuthPolling )
2861
29- /** Check if any active operation needs OTP */
62+ function handleOpenAuthUrl() {
63+ if (authUrl .value ) {
64+ window .open (authUrl .value , ' _blank' , ' noopener,noreferrer' )
65+ startAuthPolling ()
66+ }
67+ }
68+
69+ /** Check if any active operation needs OTP (fallback for web auth failures) */
3070const hasOtpFailures = computed (() =>
3171 activeOperations .value .some (
32- (op : PendingOperation ) => op .status === ' failed' && op .result ?.requiresOtp ,
72+ (op : PendingOperation ) =>
73+ op .status === ' failed' && (op .result ?.requiresOtp || op .result ?.authFailure ),
3374 ),
3475)
3576
@@ -48,9 +89,19 @@ async function handleExecute(otp?: string) {
4889
4990/** Retry all OTP-failed operations with the provided OTP */
5091async function handleRetryWithOtp() {
51- if (! otpInput .value .trim ()) return
52-
5392 const otp = otpInput .value .trim ()
93+
94+ if (! otp ) {
95+ otpError .value = ' OTP required'
96+ return
97+ }
98+
99+ if (! / ^ \d {6} $ / .test (otp )) {
100+ otpError .value = ' OTP must be a 6-digit code'
101+ return
102+ }
103+
104+ otpError .value = ' '
54105 otpInput .value = ' '
55106
56107 // First, re-approve all OTP-failed operations
@@ -65,12 +116,15 @@ async function handleRetryWithOtp() {
65116 await handleExecute (otp )
66117}
67118
68- /** Retry all OTP-failed operations using web auth (no OTP needed) */
69- async function handleRetryWithWebAuth() {
70- const otpFailedOps = activeOperations .value .filter (
71- (op : PendingOperation ) => op .status === ' failed' && op .result ?.requiresOtp ,
119+ /** Retry failed operations with web auth (no OTP) */
120+ async function handleRetryWebAuth() {
121+ // Find all failed operations that need auth retry
122+ const failedOps = activeOperations .value .filter (
123+ (op : PendingOperation ) =>
124+ op .status === ' failed' && (op .result ?.requiresOtp || op .result ?.authFailure ),
72125 )
73- for (const op of otpFailedOps ) {
126+
127+ for (const op of failedOps ) {
74128 await retryOperation (op .id )
75129 }
76130
@@ -80,6 +134,7 @@ async function handleRetryWithWebAuth() {
80134async function handleClearAll() {
81135 await clearOperations ()
82136 otpInput .value = ' '
137+ otpError .value = ' '
83138}
84139
85140function getStatusColor(status : string ): string {
@@ -242,7 +297,7 @@ watch(isExecuting, executing => {
242297 </li >
243298 </ul >
244299
245- <!-- Inline OTP prompt (appears when operations need OTP) -->
300+ <!-- Inline OTP prompt (appears when web auth fails and OTP is needed as fallback ) -->
246301 <div
247302 v-if =" hasOtpFailures"
248303 class =" p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg"
@@ -254,35 +309,46 @@ watch(isExecuting, executing => {
254309 {{ $t('operations.queue.otp_prompt') }}
255310 </span >
256311 </div >
257- <form class =" flex items-center gap-2" @submit.prevent =" handleRetryWithOtp" >
258- <label for =" otp-input" class =" sr-only" >{{ $t('operations.queue.otp_label') }}</label >
259- <InputBase
260- id =" otp-input"
261- v-model =" otpInput"
262- type =" text"
263- name =" otp-code"
264- inputmode =" numeric"
265- pattern =" [0-9]*"
266- :placeholder =" $t('operations.queue.otp_placeholder')"
267- autocomplete =" one-time-code"
268- spellcheck =" false"
269- class =" flex-1 min-w-25"
270- size =" small"
271- />
272- <button
273- type =" submit"
274- :disabled =" !otpInput.trim() || isExecuting"
275- class =" px-3 py-2 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50"
276- >
277- {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
278- </button >
312+ <form class =" flex flex-col gap-1" @submit.prevent =" handleRetryWithOtp" >
313+ <div class =" flex items-center gap-2" >
314+ <label for =" otp-input" class =" sr-only" >{{ $t('operations.queue.otp_label') }}</label >
315+ <InputBase
316+ id =" otp-input"
317+ v-model =" otpInput"
318+ type =" text"
319+ name =" otp-code"
320+ inputmode =" numeric"
321+ pattern =" [0-9]*"
322+ maxlength =" 6"
323+ :placeholder =" $t('operations.queue.otp_placeholder')"
324+ autocomplete =" one-time-code"
325+ spellcheck =" false"
326+ :class =" ['flex-1 min-w-25', otpError ? 'border-red-500 focus:outline-red-500' : '']"
327+ size =" small"
328+ @input =" otpError = ''"
329+ />
330+ <button
331+ type =" submit"
332+ :disabled =" isExecuting"
333+ class =" px-3 py-2 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50"
334+ >
335+ {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
336+ </button >
337+ </div >
338+ <p v-if =" otpError" class =" text-xs text-red-400 font-mono" >
339+ {{ otpError }}
340+ </p >
279341 </form >
342+ <div class =" flex items-center gap-2 my-3" >
343+ <div class =" flex-1 h-px bg-amber-500/30" />
344+ <span class =" text-xs text-amber-400 font-mono uppercase" >{{ $t('common.or') }}</span >
345+ <div class =" flex-1 h-px bg-amber-500/30" />
346+ </div >
280347 <button
281- v-if =" settings.connector.webAuth"
282348 type =" button"
283349 :disabled =" isExecuting"
284- class =" w-full mt-2 px-3 py-2 font-mono text-xs text-fg bg-bg-subtle border border-border rounded transition-all duration-200 hover:text-fg hover:border-border-hover disabled:opacity-50 disabled:cursor-not-allowed"
285- @click =" handleRetryWithWebAuth "
350+ class =" w-full px-3 py-2 font-mono text-xs text-fg bg-bg-subtle border border-border rounded transition-all duration-200 hover:text-fg hover:border-border-hover disabled:opacity-50 disabled:cursor-not-allowed"
351+ @click =" handleRetryWebAuth "
286352 >
287353 {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_web_auth') }}
288354 </button >
@@ -311,6 +377,15 @@ watch(isExecuting, executing => {
311377 : `${$t('operations.queue.execute')} (${approvedOperations.length})`
312378 }}
313379 </button >
380+ <button
381+ v-if =" authUrl"
382+ type =" button"
383+ class =" flex-1 px-4 py-2 font-mono text-sm text-accent bg-accent/10 border border-accent/30 rounded-md transition-colors duration-200 hover:bg-accent/20"
384+ @click =" handleOpenAuthUrl"
385+ >
386+ <span class =" i-carbon:launch w-4 h-4 inline-block me-1" aria-hidden =" true" />
387+ {{ $t('operations.queue.open_web_auth') }}
388+ </button >
314389 </div >
315390
316391 <!-- Completed operations log (collapsed by default) -->
0 commit comments