Showing 1 changed files with 67 additions and 73 deletions
+67 -73
scripts/host_manager.pl
@@ -1292,30 +1292,21 @@ sub app_html {
1292 1292
       padding: 0;
1293 1293
       border: 0;
1294 1294
     }
1295
-    /* OTP row: transparent overlay input + 6 visual-only div boxes */
1295
+    /* 6 separate OTP digit boxes — Safari detects the group and anchors
1296
+       its OTP autofill to the first box (same pattern as pbx-mgmt). */
1296 1297
     .otp-row {
1297
-      position: relative;
1298 1298
       display: flex;
1299 1299
       gap: var(--otp-gap);
1300 1300
       justify-content: center;
1301 1301
     }
1302
-    #otp-autofill {
1303
-      position: absolute; inset: 0; width: 100%; height: 100%;
1304
-      opacity: 0.01; cursor: text; z-index: 1;
1305
-      font-size: 0; border: none; outline: none;
1306
-      background: transparent; color: transparent; caret-color: transparent;
1307
-      padding: 0; margin: 0;
1308
-    }
1309
-    .otp-digit {
1302
+    .otp-row input {
1310 1303
       width: var(--otp-size); height: 56px; border: 1.5px solid #dde2ec; border-radius: 10px;
1311
-      font-size: 22px; font-weight: 600; color: var(--ink);
1312
-      background: #f8fafc;
1304
+      font-size: 22px; font-weight: 600; text-align: center; color: var(--ink);
1305
+      background: #f8fafc; caret-color: transparent; outline: none;
1313 1306
       transition: border-color .15s, background .15s;
1314
-      display: flex; align-items: center; justify-content: center;
1315
-      user-select: none; pointer-events: none;
1316 1307
     }
1317
-    .otp-digit.active { border-color: var(--accent); background: #fff; }
1318
-    .otp-digit.filled { border-color: #b3c6f0; background: #fff; }
1308
+    .otp-row input:focus { border-color: var(--accent); background: #fff; }
1309
+    .otp-row input.filled { border-color: #b3c6f0; background: #fff; }
1319 1310
     #login-error {
1320 1311
       color: var(--bad); font-size: 13px; text-align: center;
1321 1312
       min-height: 18px; margin-top: -52px;
@@ -1339,7 +1330,7 @@ sub app_html {
1339 1330
         --otp-gap: 12px;
1340 1331
         padding: 36px 22px 34px;
1341 1332
       }
1342
-      .otp-digit { height: 52px; }
1333
+      .otp-row input { height: 52px; }
1343 1334
       .login-card form { padding-bottom: 0; }
1344 1335
     }
1345 1336
     @media (max-height: 720px) {
@@ -1505,13 +1496,12 @@ sub app_html {
1505 1496
           <input type="hidden" id="otp-hidden" name="otp">
1506 1497
         </div>
1507 1498
         <div class="otp-row">
1508
-          <input type="text" id="otp-autofill" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" maxlength="6" aria-label="Verification code">
1509
-          <div class="otp-digit" aria-hidden="true"></div>
1510
-          <div class="otp-digit" aria-hidden="true"></div>
1511
-          <div class="otp-digit" aria-hidden="true"></div>
1512
-          <div class="otp-digit" aria-hidden="true"></div>
1513
-          <div class="otp-digit" aria-hidden="true"></div>
1514
-          <div class="otp-digit" aria-hidden="true"></div>
1499
+          <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" aria-label="Digit 1">
1500
+          <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" aria-label="Digit 2">
1501
+          <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" aria-label="Digit 3">
1502
+          <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" aria-label="Digit 4">
1503
+          <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" aria-label="Digit 5">
1504
+          <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" aria-label="Digit 6">
1515 1505
         </div>
1516 1506
       </form>
1517 1507
       <div id="login-error"></div>
@@ -2011,7 +2001,6 @@ sub app_html {
2011 2001
 
2012 2002
     // OTP digit boxes — auto-advance, backspace, paste
2013 2003
     const otpDigits = Array.from(document.querySelectorAll('.otp-digit'));
2014
-    const otpAutofill = $('otp-autofill');
2015 2004
     const otpHidden = $('otp-hidden');
2016 2005
     const loginAccount = $('login-account');
2017 2006
 
@@ -2024,73 +2013,78 @@ sub app_html {
2024 2013
       });
2025 2014
     }
2026 2015
 
2027
-    if (otpAutofill) otpAutofill.focus();
2028
-    highlightActiveBox(0);
2029
-
2030
-    function highlightActiveBox(idx) {
2031
-      otpDigits.forEach((d, i) => d.classList.toggle('active', i === idx));
2032
-    }
2033
-
2034 2016
     function setOtpDigit(idx, value) {
2035 2017
       const digit = (value || '').replace(/\D/g, '').slice(0, 1);
2036
-      otpDigits[idx].textContent = digit;
2018
+      otpDigits[idx].value = digit;
2037 2019
       otpDigits[idx].classList.toggle('filled', !!digit);
2038 2020
     }
2039 2021
 
2022
+    // Spread multiple digits across boxes starting at startIdx. Used for paste
2023
+    // and for Safari OTP autofill, which drops the whole code into the first box.
2040 2024
     function fillOtp(text, startIdx = 0) {
2041
-      const digits = (text || '').replace(/\D/g, '').slice(0, otpDigits.length);
2042
-      if (!digits) return;
2043
-      if (digits.length >= otpDigits.length) {
2044
-        otpDigits.forEach((_, i) => setOtpDigit(i, digits[i] || ''));
2045
-      } else {
2046
-        digits.split('').forEach((ch, offset) => {
2047
-          const targetIdx = startIdx + offset;
2048
-          if (targetIdx < otpDigits.length) setOtpDigit(targetIdx, ch);
2049
-        });
2025
+      const digits = (text || '').replace(/\D/g, '').split('');
2026
+      if (!digits.length) return;
2027
+      let last = startIdx;
2028
+      for (let i = 0; i < digits.length && startIdx + i < otpDigits.length; i++) {
2029
+        last = startIdx + i;
2030
+        setOtpDigit(last, digits[i]);
2050 2031
       }
2051
-      const next = Math.min((digits.length >= otpDigits.length ? digits.length : startIdx + digits.length), otpDigits.length - 1);
2052
-      if (otpAutofill) otpAutofill.value = getOtp();
2053 2032
       syncOtpFields();
2054
-      highlightActiveBox(next);
2033
+      if (last < otpDigits.length - 1) otpDigits[last + 1].focus();
2034
+      else otpDigits[last].focus();
2055 2035
       maybeSubmitOtp();
2056 2036
     }
2057 2037
 
2058
-    function getOtp() { return otpDigits.map(i => i.textContent).join(''); }
2059
-    function syncOtpFields() {
2060
-      const otp = getOtp();
2061
-      if (otpHidden) otpHidden.value = otp;
2038
+    function getOtp() { return otpDigits.map(i => i.value).join(''); }
2039
+    function syncOtpFields() { if (otpHidden) otpHidden.value = getOtp(); }
2040
+    function otpReady() { return otpDigits.every(i => /^\d$/.test(i.value)); }
2041
+    function maybeSubmitOtp() {
2042
+      if (otpReady()) setTimeout(() => $('login-form').requestSubmit(), 300);
2062 2043
     }
2063
-    function otpReady() { return otpDigits.every(i => /^\d$/.test(i.textContent)); }
2064
-    function maybeSubmitOtp() { if (otpReady()) $('login-form').requestSubmit(); }
2065 2044
     function clearOtp() {
2066
-      otpDigits.forEach(i => { i.textContent = ''; i.classList.remove('filled', 'active'); });
2067
-      if (otpAutofill) { otpAutofill.value = ''; otpAutofill.focus(); }
2045
+      otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); });
2068 2046
       if (otpHidden) otpHidden.value = '';
2069
-      highlightActiveBox(0);
2047
+      otpDigits[0].focus();
2070 2048
     }
2071 2049
 
2072
-    if (otpAutofill) {
2073
-      otpAutofill.addEventListener('keydown', (e) => {
2074
-        if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter'].includes(e.key)) return;
2075
-        if (e.metaKey || e.ctrlKey) return;
2076
-        if (!/^\d$/.test(e.key)) e.preventDefault();
2077
-      });
2078
-      otpAutofill.addEventListener('input', () => {
2079
-        const val = (otpAutofill.value || '').replace(/\D/g, '').slice(0, otpDigits.length);
2080
-        if (otpAutofill.value !== val) { otpAutofill.value = val; return; }
2081
-        otpDigits.forEach((_, i) => setOtpDigit(i, val[i] || ''));
2050
+    otpDigits.forEach((input, idx) => {
2051
+      input.addEventListener('input', () => {
2052
+        // A single box may receive several digits at once (autofill / typing fast).
2053
+        if (input.value.replace(/\D/g, '').length > 1) {
2054
+          fillOtp(input.value, idx);
2055
+          return;
2056
+        }
2057
+        setOtpDigit(idx, input.value);
2082 2058
         syncOtpFields();
2083
-        highlightActiveBox(Math.min(val.length, otpDigits.length - 1));
2059
+        if (input.value && idx < otpDigits.length - 1) otpDigits[idx + 1].focus();
2084 2060
         maybeSubmitOtp();
2085 2061
       });
2086
-      otpAutofill.addEventListener('change', () => {
2087
-        const val = (otpAutofill.value || '').replace(/\D/g, '').slice(0, otpDigits.length);
2088
-        otpDigits.forEach((_, i) => setOtpDigit(i, val[i] || ''));
2089
-        syncOtpFields();
2090
-        highlightActiveBox(Math.min(val.length, otpDigits.length - 1));
2091
-        maybeSubmitOtp();
2062
+
2063
+      input.addEventListener('paste', (e) => {
2064
+        e.preventDefault();
2065
+        const text = (e.clipboardData || window.clipboardData).getData('text');
2066
+        fillOtp(text, idx);
2092 2067
       });
2093
-    }
2068
+
2069
+      input.addEventListener('keydown', (e) => {
2070
+        if (e.key === 'Backspace') {
2071
+          e.preventDefault();
2072
+          if (input.value) { setOtpDigit(idx, ''); }
2073
+          else if (idx > 0) { setOtpDigit(idx - 1, ''); otpDigits[idx - 1].focus(); }
2074
+          syncOtpFields();
2075
+        } else if (e.key === 'ArrowLeft' && idx > 0) {
2076
+          e.preventDefault();
2077
+          otpDigits[idx - 1].focus();
2078
+        } else if (e.key === 'ArrowRight' && idx < otpDigits.length - 1) {
2079
+          e.preventDefault();
2080
+          otpDigits[idx + 1].focus();
2081
+        }
2082
+      });
2083
+    });
2084
+
2085
+    // Focus the first box: it carries autocomplete="one-time-code", so Safari
2086
+    // anchors its OTP autofill there instead of latching onto each box.
2087
+    otpDigits[0].focus();
2094 2088
 
2095 2089
     document.querySelectorAll('[data-page-link]').forEach(link => {
2096 2090
       link.addEventListener('click', (event) => {
@@ -2103,7 +2097,7 @@ sub app_html {
2103 2097
 
2104 2098
     $('login-form').addEventListener('submit', async (event) => {
2105 2099
       event.preventDefault();
2106
-      if (!otpReady()) return;
2100
+      if (!otpReady() || $('login-form').classList.contains('busy')) return;
2107 2101
       $('login-form').classList.add('busy');
2108 2102
       $('login-error').textContent = '';
2109 2103
       try {