The login showed the OTP autofill banner on whichever box was focused, not only the first. Cause: autocomplete="one-time-code" on the digit inputs made Safari mark the whole six-box group as the OTP target and re-present the banner on each focused box. Remove one-time-code from the digit inputs (all six are now autocomplete="off" required). The username helper stays off-screen and the visible UI stays at six boxes, per the documented "Password-Manager-Friendly Form Shape" decision — no visible username/password fields added. The first box keeps a permanent accent border as a "start here" cue. Conditional focus is unchanged: focus the first box for a returning operator (username remembered in localStorage), otherwise leave focus off the boxes so Safari's autofill is not dismissed. Out-of-order entry and the 300 ms delay before autosubmit are preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -1277,6 +1277,9 @@ sub app_html {
|
||
| 1277 | 1277 |
padding-bottom: 0; |
| 1278 | 1278 |
} |
| 1279 | 1279 |
.login-card form.busy { opacity: .72; pointer-events: none; }
|
| 1280 |
+ /* Off-screen helper fields keep the visible UI to the 6 OTP boxes while still |
|
| 1281 |
+ giving the password manager a username anchor and an aggregated OTP target |
|
| 1282 |
+ (see development-log: "Password-Manager-Friendly Form Shape"). */ |
|
| 1280 | 1283 |
.pm-helper-fields {
|
| 1281 | 1284 |
position: absolute; |
| 1282 | 1285 |
left: -10000px; |
@@ -1292,8 +1295,9 @@ sub app_html {
|
||
| 1292 | 1295 |
padding: 0; |
| 1293 | 1296 |
border: 0; |
| 1294 | 1297 |
} |
| 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). */ |
|
| 1298 |
+ /* 6 separate OTP digit boxes. No autocomplete="one-time-code" on them: that |
|
| 1299 |
+ hint was what made Safari mark the whole group and re-present its OTP |
|
| 1300 |
+ autofill on every focused box. Without it, the banner stays on the first. */ |
|
| 1297 | 1301 |
.otp-row {
|
| 1298 | 1302 |
display: flex; |
| 1299 | 1303 |
gap: var(--otp-gap); |
@@ -1305,6 +1309,7 @@ sub app_html {
|
||
| 1305 | 1309 |
background: #f8fafc; caret-color: transparent; outline: none; |
| 1306 | 1310 |
transition: border-color .15s, background .15s; |
| 1307 | 1311 |
} |
| 1312 |
+ .otp-row input:first-child { border-color: var(--accent); }
|
|
| 1308 | 1313 |
.otp-row input:focus { border-color: var(--accent); background: #fff; }
|
| 1309 | 1314 |
.otp-row input.filled { border-color: #b3c6f0; background: #fff; }
|
| 1310 | 1315 |
#login-error {
|
@@ -1496,12 +1501,12 @@ sub app_html {
|
||
| 1496 | 1501 |
<input type="hidden" id="otp-hidden" name="otp"> |
| 1497 | 1502 |
</div> |
| 1498 | 1503 |
<div class="otp-row"> |
| 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"> |
|
| 1504 |
+ <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" autocomplete="off" required aria-label="Digit 1"> |
|
| 1505 |
+ <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" autocomplete="off" required aria-label="Digit 2"> |
|
| 1506 |
+ <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" autocomplete="off" required aria-label="Digit 3"> |
|
| 1507 |
+ <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" autocomplete="off" required aria-label="Digit 4"> |
|
| 1508 |
+ <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" autocomplete="off" required aria-label="Digit 5"> |
|
| 1509 |
+ <input type="text" class="otp-digit" maxlength="1" inputmode="numeric" autocomplete="off" required aria-label="Digit 6"> |
|
| 1505 | 1510 |
</div> |
| 1506 | 1511 |
</form> |
| 1507 | 1512 |
<div id="login-error"></div> |
@@ -2044,7 +2049,10 @@ sub app_html {
|
||
| 2044 | 2049 |
function clearOtp() {
|
| 2045 | 2050 |
otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); });
|
| 2046 | 2051 |
if (otpHidden) otpHidden.value = ''; |
| 2047 |
- otpDigits[0].focus(); |
|
| 2052 |
+ // Same conditional focus as on load: don't steal focus to the OTP boxes for |
|
| 2053 |
+ // an unknown operator, so Safari's autofill anchor on the username stays. |
|
| 2054 |
+ if (loginAccount && !loginAccount.value) loginAccount.focus(); |
|
| 2055 |
+ else otpDigits[0].focus(); |
|
| 2048 | 2056 |
} |
| 2049 | 2057 |
|
| 2050 | 2058 |
otpDigits.forEach((input, idx) => {
|
@@ -2082,9 +2090,13 @@ sub app_html {
|
||
| 2082 | 2090 |
}); |
| 2083 | 2091 |
}); |
| 2084 | 2092 |
|
| 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(); |
|
| 2093 |
+ // Focus the first OTP box only for a returning operator (username known). |
|
| 2094 |
+ // For an unknown operator, leave focus on the username field so Safari can |
|
| 2095 |
+ // present its OTP autofill anchored there without being dismissed by a focus |
|
| 2096 |
+ // change (pbx-admin pattern). |
|
| 2097 |
+ if (loginAccount && loginAccount.value) otpDigits[0].focus(); |
|
| 2098 |
+ else if (loginAccount) loginAccount.focus(); |
|
| 2099 |
+ else otpDigits[0].focus(); |
|
| 2088 | 2100 |
|
| 2089 | 2101 |
document.querySelectorAll('[data-page-link]').forEach(link => {
|
| 2090 | 2102 |
link.addEventListener('click', (event) => {
|