@@ -98,3 +98,25 @@ Pachete de sistem instalate în timpul evoluției: |
||
| 98 | 98 |
- `ripgrep` pe jumper |
| 99 | 99 |
- `rsync` pe jumper |
| 100 | 100 |
- `sqlite3` pe mazeri/GitPrep |
| 101 |
+ |
|
| 102 |
+## 2026-06-06 - OTP Login Keeps a Password-Manager-Friendly Form Shape |
|
| 103 |
+ |
|
| 104 |
+Observație: unele password managere și autofill-uri mobile nu inițiau corect pe login-ul Madagascar Local Authority, deși completarea mergea pe o pagină similară din PBX management. |
|
| 105 |
+ |
|
| 106 |
+Diferențe relevante observate pe pagina care funcționa: |
|
| 107 |
+ |
|
| 108 |
+- formular clasic `method="post"` |
|
| 109 |
+- câmp de cont cu `autocomplete="username"` |
|
| 110 |
+- câmp unic pentru codul OTP, separat de cele 6 căsuțe vizuale |
|
| 111 |
+ |
|
| 112 |
+Decizie: |
|
| 113 |
+ |
|
| 114 |
+- UI-ul rămâne cu 6 căsuțe OTP |
|
| 115 |
+- formularul include și câmpuri ajutătoare off-screen pentru `username` și OTP agregat |
|
| 116 |
+- JS sincronizează codul complet între câmpul agregat și cele 6 căsuțe |
|
| 117 |
+- formularul păstrează `autocomplete="on"` la nivel de form, ca hint-urile specifice de pe câmpuri să nu fie neutralizate |
|
| 118 |
+ |
|
| 119 |
+Scop: |
|
| 120 |
+ |
|
| 121 |
+- compatibilitate mai bună cu password managere și autofill mobil |
|
| 122 |
+- fără a complica interfața vizibilă |
|
@@ -1191,6 +1191,21 @@ sub app_html {
|
||
| 1191 | 1191 |
padding-bottom: 0; |
| 1192 | 1192 |
} |
| 1193 | 1193 |
.login-card form.busy { opacity: .72; pointer-events: none; }
|
| 1194 |
+ .pm-helper-fields {
|
|
| 1195 |
+ position: absolute; |
|
| 1196 |
+ left: -10000px; |
|
| 1197 |
+ top: auto; |
|
| 1198 |
+ width: 1px; |
|
| 1199 |
+ height: 1px; |
|
| 1200 |
+ overflow: hidden; |
|
| 1201 |
+ opacity: 0.01; |
|
| 1202 |
+ } |
|
| 1203 |
+ .pm-helper-fields input {
|
|
| 1204 |
+ width: 1px; |
|
| 1205 |
+ height: 1px; |
|
| 1206 |
+ padding: 0; |
|
| 1207 |
+ border: 0; |
|
| 1208 |
+ } |
|
| 1194 | 1209 |
/* 6 separate OTP digit boxes */ |
| 1195 | 1210 |
.otp-row {
|
| 1196 | 1211 |
display: flex; |
@@ -1299,14 +1314,19 @@ sub app_html {
|
||
| 1299 | 1314 |
<h1>Madagascar Local Authority</h1> |
| 1300 | 1315 |
<p>Hosts, DNS & Local CA</p> |
| 1301 | 1316 |
</div> |
| 1302 |
- <form id="login-form"> |
|
| 1317 |
+ <form id="login-form" method="post" action="/api/login" autocomplete="on" novalidate> |
|
| 1318 |
+ <div class="pm-helper-fields" aria-hidden="true"> |
|
| 1319 |
+ <input type="text" id="login-account" name="username" autocomplete="username" autocapitalize="off" spellcheck="false"> |
|
| 1320 |
+ <input type="text" id="otp-autofill" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code"> |
|
| 1321 |
+ <input type="hidden" id="otp-hidden" name="otp"> |
|
| 1322 |
+ </div> |
|
| 1303 | 1323 |
<div class="otp-row"> |
| 1304 |
- <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="one-time-code"> |
|
| 1305 |
- <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit"> |
|
| 1306 |
- <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit"> |
|
| 1307 |
- <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit"> |
|
| 1308 |
- <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit"> |
|
| 1309 |
- <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit"> |
|
| 1324 |
+ <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" aria-label="Digit 1"> |
|
| 1325 |
+ <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" aria-label="Digit 2"> |
|
| 1326 |
+ <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" aria-label="Digit 3"> |
|
| 1327 |
+ <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" aria-label="Digit 4"> |
|
| 1328 |
+ <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" aria-label="Digit 5"> |
|
| 1329 |
+ <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" aria-label="Digit 6"> |
|
| 1310 | 1330 |
</div> |
| 1311 | 1331 |
</form> |
| 1312 | 1332 |
<div id="login-error"></div> |
@@ -1419,9 +1439,7 @@ sub app_html {
|
||
| 1419 | 1439 |
$('app').style.display = 'none';
|
| 1420 | 1440 |
$('login-screen').style.display = 'flex';
|
| 1421 | 1441 |
$('login-error').textContent = errorText || '';
|
| 1422 |
- document.querySelectorAll('.otp-digit').forEach(i => { i.value = ''; i.classList.remove('filled'); });
|
|
| 1423 |
- const first = document.querySelector('.otp-digit');
|
|
| 1424 |
- if (first) first.focus(); |
|
| 1442 |
+ clearOtp(); |
|
| 1425 | 1443 |
} |
| 1426 | 1444 |
|
| 1427 | 1445 |
function showApp() {
|
@@ -1594,15 +1612,31 @@ sub app_html {
|
||
| 1594 | 1612 |
return value.replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
| 1595 | 1613 |
} |
| 1596 | 1614 |
|
| 1615 |
+ const ACCOUNT_STORAGE_KEY = 'mla_last_account'; |
|
| 1616 |
+ |
|
| 1597 | 1617 |
// OTP digit boxes — auto-advance, backspace, paste |
| 1598 | 1618 |
const otpDigits = Array.from(document.querySelectorAll('.otp-digit'));
|
| 1619 |
+ const otpAutofill = $('otp-autofill');
|
|
| 1620 |
+ const otpHidden = $('otp-hidden');
|
|
| 1621 |
+ const loginAccount = $('login-account');
|
|
| 1622 |
+ |
|
| 1623 |
+ if (loginAccount) {
|
|
| 1624 |
+ const rememberedAccount = localStorage.getItem(ACCOUNT_STORAGE_KEY); |
|
| 1625 |
+ if (rememberedAccount && !loginAccount.value) loginAccount.value = rememberedAccount; |
|
| 1626 |
+ loginAccount.addEventListener('input', () => {
|
|
| 1627 |
+ const value = (loginAccount.value || '').trim(); |
|
| 1628 |
+ if (value) localStorage.setItem(ACCOUNT_STORAGE_KEY, value); |
|
| 1629 |
+ }); |
|
| 1630 |
+ } |
|
| 1631 |
+ |
|
| 1599 | 1632 |
otpDigits[0].focus(); |
| 1600 | 1633 |
|
| 1601 | 1634 |
otpDigits.forEach((input, idx) => {
|
| 1602 | 1635 |
input.addEventListener('keydown', (e) => {
|
| 1603 | 1636 |
if (e.key === 'Backspace') {
|
| 1604 |
- if (input.value) { input.value = ''; input.classList.remove('filled'); }
|
|
| 1605 |
- else if (idx > 0) { otpDigits[idx - 1].value = ''; otpDigits[idx - 1].classList.remove('filled'); otpDigits[idx - 1].focus(); }
|
|
| 1637 |
+ if (input.value) setOtpDigit(idx, ''); |
|
| 1638 |
+ else if (idx > 0) { setOtpDigit(idx - 1, ''); otpDigits[idx - 1].focus(); }
|
|
| 1639 |
+ syncOtpFields(); |
|
| 1606 | 1640 |
e.preventDefault(); |
| 1607 | 1641 |
} |
| 1608 | 1642 |
}); |
@@ -1613,6 +1647,7 @@ sub app_html {
|
||
| 1613 | 1647 |
return; |
| 1614 | 1648 |
} |
| 1615 | 1649 |
setOtpDigit(idx, digits); |
| 1650 |
+ syncOtpFields(); |
|
| 1616 | 1651 |
if (digits && idx < otpDigits.length - 1) otpDigits[idx + 1].focus(); |
| 1617 | 1652 |
maybeSubmitOtp(); |
| 1618 | 1653 |
}); |
@@ -1640,15 +1675,32 @@ sub app_html {
|
||
| 1640 | 1675 |
if (targetIdx < otpDigits.length) setOtpDigit(targetIdx, ch); |
| 1641 | 1676 |
}); |
| 1642 | 1677 |
} |
| 1678 |
+ syncOtpFields(); |
|
| 1643 | 1679 |
const next = Math.min((digits.length >= otpDigits.length ? digits.length : startIdx + digits.length), otpDigits.length - 1); |
| 1644 | 1680 |
otpDigits[next].focus(); |
| 1645 | 1681 |
maybeSubmitOtp(); |
| 1646 | 1682 |
} |
| 1647 | 1683 |
|
| 1648 | 1684 |
function getOtp() { return otpDigits.map(i => i.value).join(''); }
|
| 1685 |
+ function syncOtpFields() {
|
|
| 1686 |
+ const otp = getOtp(); |
|
| 1687 |
+ if (otpHidden) otpHidden.value = otp; |
|
| 1688 |
+ if (otpAutofill && otpAutofill.value !== otp) otpAutofill.value = otp; |
|
| 1689 |
+ } |
|
| 1649 | 1690 |
function otpReady() { return otpDigits.every(i => /^\d$/.test(i.value)); }
|
| 1650 | 1691 |
function maybeSubmitOtp() { if (otpReady()) $('login-form').requestSubmit(); }
|
| 1651 |
- function clearOtp() { otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); }); otpDigits[0].focus(); }
|
|
| 1692 |
+ function clearOtp() {
|
|
| 1693 |
+ otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); });
|
|
| 1694 |
+ if (otpAutofill) otpAutofill.value = ''; |
|
| 1695 |
+ if (otpHidden) otpHidden.value = ''; |
|
| 1696 |
+ otpDigits[0].focus(); |
|
| 1697 |
+ } |
|
| 1698 |
+ |
|
| 1699 |
+ if (otpAutofill) {
|
|
| 1700 |
+ const handleAutofill = () => fillOtp(otpAutofill.value, 0); |
|
| 1701 |
+ otpAutofill.addEventListener('input', handleAutofill);
|
|
| 1702 |
+ otpAutofill.addEventListener('change', handleAutofill);
|
|
| 1703 |
+ } |
|
| 1652 | 1704 |
|
| 1653 | 1705 |
$('login-form').addEventListener('submit', async (event) => {
|
| 1654 | 1706 |
event.preventDefault(); |