77 approvedOperations,
88 completedOperations,
99 activeOperations,
10+ operations,
1011 hasOperations,
1112 hasPendingOperations,
1213 hasApprovedOperations,
@@ -23,11 +24,53 @@ const {
2324
2425const isExecuting = shallowRef (false )
2526const otpInput = shallowRef (' ' )
27+ const otpError = shallowRef (' ' )
2628
27- /** Check if any active operation needs OTP */
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 )
61+
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) */
2870const hasOtpFailures = computed (() =>
2971 activeOperations .value .some (
30- (op : PendingOperation ) => op .status === ' failed' && op .result ?.requiresOtp ,
72+ (op : PendingOperation ) =>
73+ op .status === ' failed' && (op .result ?.requiresOtp || op .result ?.authFailure ),
3174 ),
3275)
3376
@@ -46,14 +89,25 @@ async function handleExecute(otp?: string) {
4689
4790/** Retry all OTP-failed operations with the provided OTP */
4891async function handleRetryWithOtp() {
49- if (! otpInput .value .trim ()) return
50-
5192 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 = ' '
52105 otpInput .value = ' '
53106
54- // First, re-approve all OTP-failed operations
107+ // First, re-approve all OTP/auth -failed operations
55108 const otpFailedOps = activeOperations .value .filter (
56- (op : PendingOperation ) => op .status === ' failed' && op .result ?.requiresOtp ,
109+ (op : PendingOperation ) =>
110+ op .status === ' failed' && (op .result ?.requiresOtp || op .result ?.authFailure ),
57111 )
58112 for (const op of otpFailedOps ) {
59113 await retryOperation (op .id )
@@ -63,9 +117,25 @@ async function handleRetryWithOtp() {
63117 await handleExecute (otp )
64118}
65119
120+ /** Retry failed operations with web auth (no OTP) */
121+ async function handleRetryWebAuth() {
122+ // Find all failed operations that need auth retry
123+ const failedOps = activeOperations .value .filter (
124+ (op : PendingOperation ) =>
125+ op .status === ' failed' && (op .result ?.requiresOtp || op .result ?.authFailure ),
126+ )
127+
128+ for (const op of failedOps ) {
129+ await retryOperation (op .id )
130+ }
131+
132+ await handleExecute ()
133+ }
134+
66135async function handleClearAll() {
67136 await clearOperations ()
68137 otpInput .value = ' '
138+ otpError .value = ' '
69139}
70140
71141function getStatusColor(status : string ): string {
@@ -228,7 +298,7 @@ watch(isExecuting, executing => {
228298 </li >
229299 </ul >
230300
231- <!-- Inline OTP prompt (appears when operations need OTP) -->
301+ <!-- Inline OTP prompt (appears when web auth fails and OTP is needed as fallback ) -->
232302 <div
233303 v-if =" hasOtpFailures"
234304 class =" p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg"
@@ -240,29 +310,49 @@ watch(isExecuting, executing => {
240310 {{ $t('operations.queue.otp_prompt') }}
241311 </span >
242312 </div >
243- <form class =" flex items-center gap-2" @submit.prevent =" handleRetryWithOtp" >
244- <label for =" otp-input" class =" sr-only" >{{ $t('operations.queue.otp_label') }}</label >
245- <InputBase
246- id =" otp-input"
247- v-model =" otpInput"
248- type =" text"
249- name =" otp-code"
250- inputmode =" numeric"
251- pattern =" [0-9]*"
252- :placeholder =" $t('operations.queue.otp_placeholder')"
253- autocomplete =" one-time-code"
254- spellcheck =" false"
255- class =" flex-1 min-w-25"
256- size =" small"
257- />
258- <button
259- type =" submit"
260- :disabled =" !otpInput.trim() || isExecuting"
261- 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"
262- >
263- {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
264- </button >
313+ <form class =" flex flex-col gap-1" @submit.prevent =" handleRetryWithOtp" >
314+ <div class =" flex items-center gap-2" >
315+ <label for =" otp-input" class =" sr-only" >{{ $t('operations.queue.otp_label') }}</label >
316+ <InputBase
317+ id =" otp-input"
318+ v-model =" otpInput"
319+ type =" text"
320+ name =" otp-code"
321+ inputmode =" numeric"
322+ pattern =" [0-9]*"
323+ maxlength =" 6"
324+ :placeholder =" $t('operations.queue.otp_placeholder')"
325+ autocomplete =" one-time-code"
326+ spellcheck =" false"
327+ :class =" ['flex-1 min-w-25', otpError ? 'border-red-500 focus:outline-red-500' : '']"
328+ size =" small"
329+ @input =" otpError = ''"
330+ />
331+ <button
332+ type =" submit"
333+ :disabled =" isExecuting"
334+ 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"
335+ >
336+ {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }}
337+ </button >
338+ </div >
339+ <p v-if =" otpError" class =" text-xs text-red-400 font-mono" >
340+ {{ otpError }}
341+ </p >
265342 </form >
343+ <div class =" flex items-center gap-2 my-3" >
344+ <div class =" flex-1 h-px bg-amber-500/30" />
345+ <span class =" text-xs text-amber-400 font-mono uppercase" >{{ $t('common.or') }}</span >
346+ <div class =" flex-1 h-px bg-amber-500/30" />
347+ </div >
348+ <button
349+ type =" button"
350+ :disabled =" isExecuting"
351+ 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"
352+ @click =" handleRetryWebAuth"
353+ >
354+ {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_web_auth') }}
355+ </button >
266356 </div >
267357
268358 <!-- Action buttons -->
@@ -288,6 +378,15 @@ watch(isExecuting, executing => {
288378 : `${$t('operations.queue.execute')} (${approvedOperations.length})`
289379 }}
290380 </button >
381+ <button
382+ v-if =" authUrl"
383+ type =" button"
384+ 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"
385+ @click =" handleOpenAuthUrl"
386+ >
387+ <span class =" i-carbon:launch w-4 h-4 inline-block me-1" aria-hidden =" true" />
388+ {{ $t('operations.queue.open_web_auth') }}
389+ </button >
291390 </div >
292391
293392 <!-- Completed operations log (collapsed by default) -->
0 commit comments