Showing 1 changed files with 71 additions and 58 deletions
+71 -58
scripts/host_manager.pl
@@ -1292,21 +1292,30 @@ sub app_html {
1292 1292
       padding: 0;
1293 1293
       border: 0;
1294 1294
     }
1295
-    /* 6 separate OTP digit boxes */
1295
+    /* OTP row: transparent overlay input + 6 visual-only div boxes */
1296 1296
     .otp-row {
1297
+      position: relative;
1297 1298
       display: flex;
1298 1299
       gap: var(--otp-gap);
1299 1300
       justify-content: center;
1300 1301
     }
1301
-    .otp-row input {
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 1310
       width: var(--otp-size); height: 56px; border: 1.5px solid #dde2ec; border-radius: 10px;
1303
-      font-size: 22px; font-weight: 600; text-align: center; color: var(--ink);
1304
-      background: #f8fafc; caret-color: transparent; outline: none;
1311
+      font-size: 22px; font-weight: 600; color: var(--ink);
1312
+      background: #f8fafc;
1305 1313
       transition: border-color .15s, background .15s;
1314
+      display: flex; align-items: center; justify-content: center;
1315
+      user-select: none; pointer-events: none;
1306 1316
     }
1307
-    .otp-row input:focus { border-color: var(--accent); background: #fff; }
1308
-    .otp-row input.focus-proxy { border-color: var(--accent); background: #fff; }
1309
-    .otp-row input.filled { border-color: #b3c6f0; background: #fff; }
1317
+    .otp-digit.active { border-color: var(--accent); background: #fff; }
1318
+    .otp-digit.filled { border-color: #b3c6f0; background: #fff; }
1310 1319
     #login-error {
1311 1320
       color: var(--bad); font-size: 13px; text-align: center;
1312 1321
       min-height: 18px; margin-top: -52px;
@@ -1330,7 +1339,7 @@ sub app_html {
1330 1339
         --otp-gap: 12px;
1331 1340
         padding: 36px 22px 34px;
1332 1341
       }
1333
-      .otp-row input { height: 52px; }
1342
+      .otp-digit { height: 52px; }
1334 1343
       .login-card form { padding-bottom: 0; }
1335 1344
     }
1336 1345
     @media (max-height: 720px) {
@@ -1376,6 +1385,7 @@ sub app_html {
1376 1385
     .pill.ok { color: var(--ok); border-color: #b7dfc1; background: #edf8ef; }
1377 1386
     .pill.warn { color: var(--warn); border-color: #f1d184; background: #fff7df; }
1378 1387
     .pill.bad { color: var(--bad); border-color: #f0b8b3; background: #fff0ee; }
1388
+    .pill.derived { border-style: dashed; }
1379 1389
     .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 14px; }
1380 1390
     .span2 { grid-column: 1 / -1; }
1381 1391
     label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; font-weight: 650; }
@@ -1492,16 +1502,16 @@ sub app_html {
1492 1502
       <form id="login-form" method="post" action="/api/login" autocomplete="on" novalidate>
1493 1503
         <div class="pm-helper-fields" aria-hidden="true">
1494 1504
           <input type="text" id="login-account" name="username" autocomplete="username" autocapitalize="off" spellcheck="false">
1495
-          <input type="text" id="otp-autofill" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
1496 1505
           <input type="hidden" id="otp-hidden" name="otp">
1497 1506
         </div>
1498 1507
         <div class="otp-row">
1499
-          <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="off" aria-label="Digit 1">
1500
-          <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="off" aria-label="Digit 2">
1501
-          <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="off" aria-label="Digit 3">
1502
-          <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="off" aria-label="Digit 4">
1503
-          <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="off" aria-label="Digit 5">
1504
-          <input type="text" inputmode="numeric" pattern="[0-9]*" class="otp-digit" autocomplete="off" aria-label="Digit 6">
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>
1505 1515
         </div>
1506 1516
       </form>
1507 1517
       <div id="login-error"></div>
@@ -1890,6 +1900,8 @@ sub app_html {
1890 1900
     function renderHosts() {
1891 1901
       const filter = $('filter').value.toLowerCase();
1892 1902
       $('hosts').innerHTML = state.hosts
1903
+        .slice()
1904
+        .sort((a, b) => String(a.id || '').localeCompare(String(b.id || '')))
1893 1905
         .filter(h => JSON.stringify(h).toLowerCase().includes(filter))
1894 1906
         .map(h => {
1895 1907
           const problems = state.problems.filter(p => p.host_id === h.id);
@@ -1898,7 +1910,7 @@ sub app_html {
1898 1910
             <td><button type="button" data-edit="${escapeHtml(h.id)}">${escapeHtml(h.id)}</button></td>
1899 1911
             <td>${escapeHtml(h.hosts_ip || '')}</td>
1900 1912
             <td>${escapeHtml(h.dns_ip || '')}</td>
1901
-            <td>${(h.names || []).map(n => `<span class="pill">${escapeHtml(n)}</span>`).join('')}</td>
1913
+            <td>${renderNamePills(h)}</td>
1902 1914
             <td>${(h.roles || []).map(n => `<span class="pill">${escapeHtml(n)}</span>`).join('')}</td>
1903 1915
             <td><span class="pill ${cls}">${escapeHtml(h.monitoring || '')}</span></td>
1904 1916
             <td>${escapeHtml(h.status || '')}</td>
@@ -1907,13 +1919,21 @@ sub app_html {
1907 1919
       document.querySelectorAll('[data-edit]').forEach(button => button.addEventListener('click', () => editHost(button.dataset.edit)));
1908 1920
     }
1909 1921
 
1922
+    function renderNamePills(host) {
1923
+      const declared = host.declared_names || host.names || [];
1924
+      const derived = host.derived_names || [];
1925
+      const declaredHtml = declared.map(name => `<span class="pill">${escapeHtml(name)}</span>`).join('');
1926
+      const derivedHtml = derived.map(name => `<span class="pill derived" title="derived from madagascar.xdev.ro">${escapeHtml(name)}</span>`).join('');
1927
+      return declaredHtml + derivedHtml;
1928
+    }
1929
+
1910 1930
     function editHost(id) {
1911 1931
       const host = state.hosts.find(h => h.id === id);
1912 1932
       if (!host) return;
1913 1933
       const form = $('host-form');
1914 1934
       clearHostFormMessage();
1915 1935
       for (const key of ['id', 'status', 'hosts_ip', 'dns_ip', 'monitoring', 'notes']) hostField(key).value = host[key] || '';
1916
-      hostField('names').value = (host.names || []).join('\n');
1936
+      hostField('names').value = (host.declared_names || host.names || []).join('\n');
1917 1937
       hostField('roles').value = (host.roles || []).join(' ');
1918 1938
       hostField('sources').value = (host.sources || []).join(' ');
1919 1939
       openHostModal('Edit host');
@@ -2004,38 +2024,16 @@ sub app_html {
2004 2024
       });
2005 2025
     }
2006 2026
 
2007
-    (otpAutofill || otpDigits[0]).focus();
2027
+    if (otpAutofill) otpAutofill.focus();
2028
+    highlightActiveBox(0);
2008 2029
 
2009
-    otpDigits.forEach((input, idx) => {
2010
-      input.addEventListener('keydown', (e) => {
2011
-        if (e.key === 'Backspace') {
2012
-          if (input.value) setOtpDigit(idx, '');
2013
-          else if (idx > 0) { setOtpDigit(idx - 1, ''); otpDigits[idx - 1].focus(); }
2014
-          syncOtpFields();
2015
-          e.preventDefault();
2016
-        }
2017
-      });
2018
-      input.addEventListener('input', () => {
2019
-        const digits = input.value.replace(/\D/g, '');
2020
-        if (digits.length > 1) {
2021
-          fillOtp(digits, digits.length >= otpDigits.length ? 0 : idx);
2022
-          return;
2023
-        }
2024
-        setOtpDigit(idx, digits);
2025
-        syncOtpFields();
2026
-        if (digits && idx < otpDigits.length - 1) otpDigits[idx + 1].focus();
2027
-        maybeSubmitOtp();
2028
-      });
2029
-      input.addEventListener('paste', (e) => {
2030
-        const text = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
2031
-        e.preventDefault();
2032
-        fillOtp(text, text.length >= otpDigits.length ? 0 : idx);
2033
-      });
2034
-    });
2030
+    function highlightActiveBox(idx) {
2031
+      otpDigits.forEach((d, i) => d.classList.toggle('active', i === idx));
2032
+    }
2035 2033
 
2036 2034
     function setOtpDigit(idx, value) {
2037 2035
       const digit = (value || '').replace(/\D/g, '').slice(0, 1);
2038
-      otpDigits[idx].value = digit;
2036
+      otpDigits[idx].textContent = digit;
2039 2037
       otpDigits[idx].classList.toggle('filled', !!digit);
2040 2038
     }
2041 2039
 
@@ -2050,33 +2048,48 @@ sub app_html {
2050 2048
           if (targetIdx < otpDigits.length) setOtpDigit(targetIdx, ch);
2051 2049
         });
2052 2050
       }
2053
-      syncOtpFields();
2054 2051
       const next = Math.min((digits.length >= otpDigits.length ? digits.length : startIdx + digits.length), otpDigits.length - 1);
2055
-      otpDigits[next].focus();
2052
+      if (otpAutofill) otpAutofill.value = getOtp();
2053
+      syncOtpFields();
2054
+      highlightActiveBox(next);
2056 2055
       maybeSubmitOtp();
2057 2056
     }
2058 2057
 
2059
-    function getOtp() { return otpDigits.map(i => i.value).join(''); }
2058
+    function getOtp() { return otpDigits.map(i => i.textContent).join(''); }
2060 2059
     function syncOtpFields() {
2061 2060
       const otp = getOtp();
2062 2061
       if (otpHidden) otpHidden.value = otp;
2063
-      if (otpAutofill && otpAutofill.value !== otp) otpAutofill.value = otp;
2064 2062
     }
2065
-    function otpReady() { return otpDigits.every(i => /^\d$/.test(i.value)); }
2063
+    function otpReady() { return otpDigits.every(i => /^\d$/.test(i.textContent)); }
2066 2064
     function maybeSubmitOtp() { if (otpReady()) $('login-form').requestSubmit(); }
2067 2065
     function clearOtp() {
2068
-      otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); });
2069
-      if (otpAutofill) otpAutofill.value = '';
2066
+      otpDigits.forEach(i => { i.textContent = ''; i.classList.remove('filled', 'active'); });
2067
+      if (otpAutofill) { otpAutofill.value = ''; otpAutofill.focus(); }
2070 2068
       if (otpHidden) otpHidden.value = '';
2071
-      (otpAutofill || otpDigits[0]).focus();
2069
+      highlightActiveBox(0);
2072 2070
     }
2073 2071
 
2074 2072
     if (otpAutofill) {
2075
-      const handleAutofill = () => fillOtp(otpAutofill.value, 0);
2076
-      otpAutofill.addEventListener('input', handleAutofill);
2077
-      otpAutofill.addEventListener('change', handleAutofill);
2078
-      otpAutofill.addEventListener('focus', () => otpDigits[0].classList.add('focus-proxy'));
2079
-      otpAutofill.addEventListener('blur',  () => otpDigits[0].classList.remove('focus-proxy'));
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] || ''));
2082
+        syncOtpFields();
2083
+        highlightActiveBox(Math.min(val.length, otpDigits.length - 1));
2084
+        maybeSubmitOtp();
2085
+      });
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();
2092
+      });
2080 2093
     }
2081 2094
 
2082 2095
     document.querySelectorAll('[data-page-link]').forEach(link => {