Showing 2 changed files with 87 additions and 13 deletions
+22 -0
.doc/development-log.md
@@ -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ă
+65 -13
scripts/host_manager.pl
@@ -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 &amp; 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[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();