@@ -818,8 +818,65 @@ sub app_html {
|
||
| 818 | 818 |
} |
| 819 | 819 |
* { box-sizing: border-box; }
|
| 820 | 820 |
body { margin: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: #eef2f6; font-size: 14px; }
|
| 821 |
- header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 2; }
|
|
| 822 |
- h1 { margin: 0; font-size: 18px; font-weight: 700; letter-spacing: 0; }
|
|
| 821 |
+ |
|
| 822 |
+ /* ── Login screen ── */ |
|
| 823 |
+ #login-screen {
|
|
| 824 |
+ display: flex; |
|
| 825 |
+ align-items: center; |
|
| 826 |
+ justify-content: center; |
|
| 827 |
+ min-height: 100dvh; |
|
| 828 |
+ padding: 24px; |
|
| 829 |
+ background: #13182a; |
|
| 830 |
+ } |
|
| 831 |
+ .login-card {
|
|
| 832 |
+ background: #fff; |
|
| 833 |
+ border-radius: 16px; |
|
| 834 |
+ padding: 44px 36px 36px; |
|
| 835 |
+ width: 100%; |
|
| 836 |
+ max-width: 380px; |
|
| 837 |
+ display: grid; |
|
| 838 |
+ gap: 24px; |
|
| 839 |
+ box-shadow: 0 8px 40px rgba(0,0,0,.28); |
|
| 840 |
+ } |
|
| 841 |
+ .login-card .brand { text-align: center; display: grid; gap: 6px; }
|
|
| 842 |
+ .login-card .brand .icon {
|
|
| 843 |
+ margin: 0 auto 4px; |
|
| 844 |
+ width: 52px; height: 52px; border-radius: 14px; |
|
| 845 |
+ background: #e8f0fe; display: flex; align-items: center; justify-content: center; |
|
| 846 |
+ } |
|
| 847 |
+ .login-card .brand .icon svg { width: 26px; height: 26px; fill: var(--accent); }
|
|
| 848 |
+ .login-card .brand h1 { margin: 0; font-size: 22px; font-weight: 700; color: var(--ink); }
|
|
| 849 |
+ .login-card .brand p { margin: 0; color: var(--muted); font-size: 13px; }
|
|
| 850 |
+ .login-card form { display: grid; gap: 16px; }
|
|
| 851 |
+ .login-card .field-label { font-size: 13px; font-weight: 600; color: var(--ink); }
|
|
| 852 |
+ /* 6 separate OTP digit boxes */ |
|
| 853 |
+ .otp-row { display: flex; gap: 8px; justify-content: center; }
|
|
| 854 |
+ .otp-row input {
|
|
| 855 |
+ width: 48px; height: 56px; border: 1.5px solid #dde2ec; border-radius: 10px; |
|
| 856 |
+ font-size: 22px; font-weight: 600; text-align: center; color: var(--ink); |
|
| 857 |
+ background: #f8fafc; caret-color: transparent; outline: none; |
|
| 858 |
+ transition: border-color .15s, background .15s; |
|
| 859 |
+ } |
|
| 860 |
+ .otp-row input:focus { border-color: var(--accent); background: #fff; }
|
|
| 861 |
+ .otp-row input.filled { border-color: #b3c6f0; background: #fff; }
|
|
| 862 |
+ .login-card button.primary {
|
|
| 863 |
+ width: 100%; border: none; background: var(--accent); color: #fff; |
|
| 864 |
+ border-radius: 10px; padding: 13px; font: inherit; font-size: 15px; |
|
| 865 |
+ font-weight: 600; cursor: pointer; min-height: 48px; |
|
| 866 |
+ display: flex; align-items: center; justify-content: center; gap: 8px; |
|
| 867 |
+ } |
|
| 868 |
+ .login-card button.primary:hover { background: #0f52b8; }
|
|
| 869 |
+ .login-card button.primary:disabled { opacity: .55; cursor: not-allowed; }
|
|
| 870 |
+ #login-error {
|
|
| 871 |
+ color: var(--bad); font-size: 13px; text-align: center; |
|
| 872 |
+ min-height: 18px; margin-top: -8px; |
|
| 873 |
+ } |
|
| 874 |
+ |
|
| 875 |
+ /* ── App shell (hidden until authenticated) ── */ |
|
| 876 |
+ #app { display: none; }
|
|
| 877 |
+ header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 2; }
|
|
| 878 |
+ h1 { margin: 0; font-size: 17px; font-weight: 700; }
|
|
| 879 |
+ .header-right { display: flex; align-items: center; gap: 10px; }
|
|
| 823 | 880 |
main { padding: 16px; display: grid; gap: 16px; max-width: 1280px; margin: 0 auto; }
|
| 824 | 881 |
.toolbar, .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
| 825 | 882 |
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px; }
|
@@ -846,13 +903,10 @@ sub app_html {
|
||
| 846 | 903 |
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 14px; }
|
| 847 | 904 |
.span2 { grid-column: 1 / -1; }
|
| 848 | 905 |
label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; font-weight: 650; }
|
| 849 |
- .auth { display: flex; gap: 8px; align-items: center; }
|
|
| 850 |
- .auth input { width: 130px; }
|
|
| 851 | 906 |
.muted { color: var(--muted); }
|
| 852 | 907 |
.problems { padding: 10px 14px; display: grid; gap: 8px; }
|
| 853 | 908 |
.problem { border-left: 3px solid var(--warn); padding: 7px 9px; background: #fffaf0; }
|
| 854 | 909 |
@media (max-width: 760px) {
|
| 855 |
- header { align-items: stretch; flex-direction: column; }
|
|
| 856 | 910 |
.grid { grid-template-columns: 1fr; }
|
| 857 | 911 |
table { min-width: 760px; }
|
| 858 | 912 |
.table-wrap { overflow-x: auto; }
|
@@ -860,78 +914,110 @@ sub app_html {
|
||
| 860 | 914 |
</style> |
| 861 | 915 |
</head> |
| 862 | 916 |
<body> |
| 863 |
- <header> |
|
| 864 |
- <h1>Host Manager</h1> |
|
| 865 |
- <form class="auth" id="login-form"> |
|
| 866 |
- <span id="auth-state" class="muted">locked</span> |
|
| 867 |
- <input id="otp" name="otp" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="OTP"> |
|
| 868 |
- <button class="primary" type="submit">Login</button> |
|
| 869 |
- <button type="button" id="logout">Logout</button> |
|
| 870 |
- </form> |
|
| 871 |
- </header> |
|
| 872 |
- <main> |
|
| 873 |
- <section class="toolbar"> |
|
| 874 |
- <button id="refresh" data-auth-required>Refresh</button> |
|
| 875 |
- <a class="linkbtn" data-auth-required href="/download/hosts.yaml">hosts.yaml</a> |
|
| 876 |
- <a class="linkbtn" data-auth-required href="/download/local-hosts.tsv">local-hosts.tsv</a> |
|
| 877 |
- <a class="linkbtn" data-auth-required href="/download/monitoring.json">monitoring.json</a> |
|
| 878 |
- <button id="write-tsv" data-auth-required>Write local-hosts.tsv</button> |
|
| 879 |
- <span id="message" class="muted"></span> |
|
| 880 |
- </section> |
|
| 881 |
- |
|
| 882 |
- <section class="panel"> |
|
| 883 |
- <div class="panel-head"> |
|
| 884 |
- <h2>Overview</h2> |
|
| 885 |
- <div class="stats" id="stats"></div> |
|
| 886 |
- </div> |
|
| 887 |
- <div class="problems" id="problems"></div> |
|
| 888 |
- </section> |
|
| 889 | 917 |
|
| 890 |
- <section class="panel"> |
|
| 891 |
- <div class="panel-head"> |
|
| 892 |
- <h2>Hosts</h2> |
|
| 893 |
- <input id="filter" placeholder="filter" style="max-width: 240px"> |
|
| 918 |
+ <!-- ── Login screen ── --> |
|
| 919 |
+ <div id="login-screen"> |
|
| 920 |
+ <div class="login-card"> |
|
| 921 |
+ <div class="brand"> |
|
| 922 |
+ <div class="icon"> |
|
| 923 |
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
| 924 |
+ <path d="M20 3H4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 5H5V5h14v3zm1 5H4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1zm-1 5H5v-3h14v3z"/> |
|
| 925 |
+ <circle cx="17" cy="7" r="1"/><circle cx="19.5" cy="7" r="1"/> |
|
| 926 |
+ <circle cx="17" cy="15.5" r="1"/><circle cx="19.5" cy="15.5" r="1"/> |
|
| 927 |
+ </svg> |
|
| 928 |
+ </div> |
|
| 929 |
+ <h1>Host Manager</h1> |
|
| 930 |
+ <p>madagascar.xdev.ro</p> |
|
| 894 | 931 |
</div> |
| 895 |
- <div class="table-wrap"> |
|
| 896 |
- <table> |
|
| 897 |
- <thead> |
|
| 898 |
- <tr> |
|
| 899 |
- <th style="width: 120px">ID</th> |
|
| 900 |
- <th style="width: 130px">hosts_ip</th> |
|
| 901 |
- <th style="width: 130px">dns_ip</th> |
|
| 902 |
- <th>Names</th> |
|
| 903 |
- <th style="width: 150px">Roles</th> |
|
| 904 |
- <th style="width: 110px">Monitoring</th> |
|
| 905 |
- <th style="width: 90px">Status</th> |
|
| 906 |
- </tr> |
|
| 907 |
- </thead> |
|
| 908 |
- <tbody id="hosts"></tbody> |
|
| 909 |
- </table> |
|
| 932 |
+ <form id="login-form"> |
|
| 933 |
+ <div class="field-label">Cod Authenticator</div> |
|
| 934 |
+ <div class="otp-row"> |
|
| 935 |
+ <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit" autocomplete="one-time-code"> |
|
| 936 |
+ <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
|
| 937 |
+ <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
|
| 938 |
+ <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
|
| 939 |
+ <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
|
| 940 |
+ <input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
|
| 941 |
+ </div> |
|
| 942 |
+ <button class="primary" type="submit" id="login-btn">Autentifică-te</button> |
|
| 943 |
+ </form> |
|
| 944 |
+ <div id="login-error"></div> |
|
| 945 |
+ </div> |
|
| 946 |
+ </div> |
|
| 947 |
+ |
|
| 948 |
+ <!-- ── App (shown after login) ── --> |
|
| 949 |
+ <div id="app"> |
|
| 950 |
+ <header> |
|
| 951 |
+ <h1>Host Manager</h1> |
|
| 952 |
+ <div class="header-right"> |
|
| 953 |
+ <span class="muted" id="app-updated"></span> |
|
| 954 |
+ <button type="button" id="logout">Logout</button> |
|
| 910 | 955 |
</div> |
| 911 |
- </section> |
|
| 956 |
+ </header> |
|
| 957 |
+ <main> |
|
| 958 |
+ <section class="toolbar"> |
|
| 959 |
+ <button id="refresh">Refresh</button> |
|
| 960 |
+ <a class="linkbtn" href="/download/hosts.yaml">hosts.yaml</a> |
|
| 961 |
+ <a class="linkbtn" href="/download/local-hosts.tsv">local-hosts.tsv</a> |
|
| 962 |
+ <a class="linkbtn" href="/download/monitoring.json">monitoring.json</a> |
|
| 963 |
+ <button id="write-tsv">Write local-hosts.tsv</button> |
|
| 964 |
+ <span id="message" class="muted"></span> |
|
| 965 |
+ </section> |
|
| 966 |
+ |
|
| 967 |
+ <section class="panel"> |
|
| 968 |
+ <div class="panel-head"> |
|
| 969 |
+ <h2>Overview</h2> |
|
| 970 |
+ <div class="stats" id="stats"></div> |
|
| 971 |
+ </div> |
|
| 972 |
+ <div class="problems" id="problems"></div> |
|
| 973 |
+ </section> |
|
| 912 | 974 |
|
| 913 |
- <section class="panel"> |
|
| 914 |
- <div class="panel-head"> |
|
| 915 |
- <h2>Edit host</h2> |
|
| 916 |
- <span class="muted">write access requires OTP</span> |
|
| 917 |
- </div> |
|
| 918 |
- <form id="host-form" class="grid"> |
|
| 919 |
- <label>ID<input name="id" required></label> |
|
| 920 |
- <label>Status<select name="status"><option>active</option><option>planned</option><option>retired</option></select></label> |
|
| 921 |
- <label>hosts_ip<input name="hosts_ip" required></label> |
|
| 922 |
- <label>dns_ip<input name="dns_ip" required></label> |
|
| 923 |
- <label class="span2">Names<textarea name="names" required></textarea></label> |
|
| 924 |
- <label>Roles<input name="roles"></label> |
|
| 925 |
- <label>Sources<input name="sources"></label> |
|
| 926 |
- <label>Monitoring<select name="monitoring"><option>pending</option><option>enabled</option><option>disabled</option></select></label> |
|
| 927 |
- <label>Notes<input name="notes"></label> |
|
| 928 |
- <div class="span2"> |
|
| 929 |
- <button class="primary" type="submit">Save host</button> |
|
| 930 |
- <button class="danger" type="button" id="delete-host">Delete host</button> |
|
| 975 |
+ <section class="panel"> |
|
| 976 |
+ <div class="panel-head"> |
|
| 977 |
+ <h2>Hosts</h2> |
|
| 978 |
+ <input id="filter" placeholder="filter" style="max-width: 240px"> |
|
| 931 | 979 |
</div> |
| 932 |
- </form> |
|
| 933 |
- </section> |
|
| 934 |
- </main> |
|
| 980 |
+ <div class="table-wrap"> |
|
| 981 |
+ <table> |
|
| 982 |
+ <thead> |
|
| 983 |
+ <tr> |
|
| 984 |
+ <th style="width: 120px">ID</th> |
|
| 985 |
+ <th style="width: 130px">hosts_ip</th> |
|
| 986 |
+ <th style="width: 130px">dns_ip</th> |
|
| 987 |
+ <th>Names</th> |
|
| 988 |
+ <th style="width: 150px">Roles</th> |
|
| 989 |
+ <th style="width: 110px">Monitoring</th> |
|
| 990 |
+ <th style="width: 90px">Status</th> |
|
| 991 |
+ </tr> |
|
| 992 |
+ </thead> |
|
| 993 |
+ <tbody id="hosts"></tbody> |
|
| 994 |
+ </table> |
|
| 995 |
+ </div> |
|
| 996 |
+ </section> |
|
| 997 |
+ |
|
| 998 |
+ <section class="panel"> |
|
| 999 |
+ <div class="panel-head"> |
|
| 1000 |
+ <h2>Edit host</h2> |
|
| 1001 |
+ </div> |
|
| 1002 |
+ <form id="host-form" class="grid"> |
|
| 1003 |
+ <label>ID<input name="id" required></label> |
|
| 1004 |
+ <label>Status<select name="status"><option>active</option><option>planned</option><option>retired</option></select></label> |
|
| 1005 |
+ <label>hosts_ip<input name="hosts_ip" required></label> |
|
| 1006 |
+ <label>dns_ip<input name="dns_ip" required></label> |
|
| 1007 |
+ <label class="span2">Names<textarea name="names" required></textarea></label> |
|
| 1008 |
+ <label>Roles<input name="roles"></label> |
|
| 1009 |
+ <label>Sources<input name="sources"></label> |
|
| 1010 |
+ <label>Monitoring<select name="monitoring"><option>pending</option><option>enabled</option><option>disabled</option></select></label> |
|
| 1011 |
+ <label>Notes<input name="notes"></label> |
|
| 1012 |
+ <div class="span2"> |
|
| 1013 |
+ <button class="primary" type="submit">Save host</button> |
|
| 1014 |
+ <button class="danger" type="button" id="delete-host">Delete host</button> |
|
| 1015 |
+ </div> |
|
| 1016 |
+ </form> |
|
| 1017 |
+ </section> |
|
| 1018 |
+ </main> |
|
| 1019 |
+ </div> |
|
| 1020 |
+ |
|
| 935 | 1021 |
<script> |
| 936 | 1022 |
let state = { hosts: [], problems: [], authenticated: false };
|
| 937 | 1023 |
|
@@ -945,44 +1031,37 @@ sub app_html {
|
||
| 945 | 1031 |
return body; |
| 946 | 1032 |
} |
| 947 | 1033 |
|
| 1034 |
+ function showLogin(errorText) {
|
|
| 1035 |
+ $('app').style.display = 'none';
|
|
| 1036 |
+ $('login-screen').style.display = 'flex';
|
|
| 1037 |
+ $('login-error').textContent = errorText || '';
|
|
| 1038 |
+ document.querySelectorAll('.otp-digit').forEach(i => { i.value = ''; i.classList.remove('filled'); });
|
|
| 1039 |
+ const first = document.querySelector('.otp-digit');
|
|
| 1040 |
+ if (first) first.focus(); |
|
| 1041 |
+ } |
|
| 1042 |
+ |
|
| 1043 |
+ function showApp() {
|
|
| 1044 |
+ $('login-screen').style.display = 'none';
|
|
| 1045 |
+ $('app').style.display = 'block';
|
|
| 1046 |
+ } |
|
| 1047 |
+ |
|
| 948 | 1048 |
async function refresh() {
|
| 949 | 1049 |
const session = await api('/api/session');
|
| 950 | 1050 |
state.authenticated = session.authenticated; |
| 951 |
- updateAuthUi(); |
|
| 952 |
- if (!state.authenticated) {
|
|
| 953 |
- state.hosts = []; |
|
| 954 |
- state.problems = []; |
|
| 955 |
- renderLocked(); |
|
| 956 |
- return; |
|
| 957 |
- } |
|
| 1051 |
+ if (!state.authenticated) { showLogin(); return; }
|
|
| 1052 |
+ showApp(); |
|
| 958 | 1053 |
const data = await api('/api/hosts');
|
| 959 | 1054 |
state.hosts = data.hosts || []; |
| 960 | 1055 |
state.problems = data.problems || []; |
| 961 | 1056 |
render(data); |
| 962 | 1057 |
} |
| 963 | 1058 |
|
| 964 |
- function updateAuthUi() {
|
|
| 965 |
- $('auth-state').textContent = state.authenticated ? 'authenticated' : 'locked';
|
|
| 966 |
- document.querySelectorAll('[data-auth-required]').forEach(el => {
|
|
| 967 |
- if (el.tagName === 'A') {
|
|
| 968 |
- el.setAttribute('aria-disabled', state.authenticated ? 'false' : 'true');
|
|
| 969 |
- } else {
|
|
| 970 |
- el.disabled = !state.authenticated; |
|
| 971 |
- } |
|
| 972 |
- }); |
|
| 973 |
- } |
|
| 974 |
- |
|
| 975 |
- function renderLocked() {
|
|
| 976 |
- $('stats').innerHTML = '<span class="stat">authentication required</span>';
|
|
| 977 |
- $('problems').innerHTML = '<div class="muted" style="padding: 8px 0">Login with OTP to access host data, downloads, and configuration actions.</div>';
|
|
| 978 |
- $('hosts').innerHTML = '';
|
|
| 979 |
- } |
|
| 980 |
- |
|
| 981 | 1059 |
function render(data) {
|
| 1060 |
+ $('app-updated').textContent = data.updated_at ? 'updated ' + data.updated_at : '';
|
|
| 1061 |
+ |
|
| 982 | 1062 |
$('stats').innerHTML = [
|
| 983 | 1063 |
['hosts', data.counts.hosts], |
| 984 | 1064 |
['problems', data.counts.problems], |
| 985 |
- ['updated', data.updated_at || 'unknown'] |
|
| 986 | 1065 |
].map(([k, v]) => `<span class="stat">${k}: ${escapeHtml(String(v))}</span>`).join('');
|
| 987 | 1066 |
|
| 988 | 1067 |
$('problems').innerHTML = state.problems.length
|
@@ -1020,7 +1099,7 @@ sub app_html {
|
||
| 1020 | 1099 |
form.elements.names.value = (host.names || []).join('\n');
|
| 1021 | 1100 |
form.elements.roles.value = (host.roles || []).join(' ');
|
| 1022 | 1101 |
form.elements.sources.value = (host.sources || []).join(' ');
|
| 1023 |
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
| 1102 |
+ form.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
| 1024 | 1103 |
} |
| 1025 | 1104 |
|
| 1026 | 1105 |
function formObject(form) {
|
@@ -1031,25 +1110,65 @@ sub app_html {
|
||
| 1031 | 1110 |
return value.replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
| 1032 | 1111 |
} |
| 1033 | 1112 |
|
| 1034 |
- $('refresh').addEventListener('click', () => refresh().catch(e => msg(e.message)));
|
|
| 1035 |
- $('filter').addEventListener('input', renderHosts);
|
|
| 1113 |
+ // OTP digit boxes — auto-advance, backspace, paste |
|
| 1114 |
+ const otpDigits = Array.from(document.querySelectorAll('.otp-digit'));
|
|
| 1115 |
+ otpDigits[0].focus(); |
|
| 1116 |
+ |
|
| 1117 |
+ otpDigits.forEach((input, idx) => {
|
|
| 1118 |
+ input.addEventListener('keydown', (e) => {
|
|
| 1119 |
+ if (e.key === 'Backspace') {
|
|
| 1120 |
+ if (input.value) { input.value = ''; input.classList.remove('filled'); }
|
|
| 1121 |
+ else if (idx > 0) { otpDigits[idx - 1].value = ''; otpDigits[idx - 1].classList.remove('filled'); otpDigits[idx - 1].focus(); }
|
|
| 1122 |
+ e.preventDefault(); |
|
| 1123 |
+ } |
|
| 1124 |
+ }); |
|
| 1125 |
+ input.addEventListener('input', (e) => {
|
|
| 1126 |
+ const val = input.value.replace(/\D/g, '').slice(-1); |
|
| 1127 |
+ input.value = val; |
|
| 1128 |
+ input.classList.toggle('filled', !!val);
|
|
| 1129 |
+ if (val && idx < otpDigits.length - 1) otpDigits[idx + 1].focus(); |
|
| 1130 |
+ if (val && idx === otpDigits.length - 1) $('login-form').requestSubmit();
|
|
| 1131 |
+ }); |
|
| 1132 |
+ input.addEventListener('paste', (e) => {
|
|
| 1133 |
+ const text = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
|
| 1134 |
+ e.preventDefault(); |
|
| 1135 |
+ text.split('').slice(0, otpDigits.length).forEach((ch, i) => {
|
|
| 1136 |
+ otpDigits[i].value = ch; |
|
| 1137 |
+ otpDigits[i].classList.add('filled');
|
|
| 1138 |
+ }); |
|
| 1139 |
+ const next = Math.min(text.length, otpDigits.length - 1); |
|
| 1140 |
+ otpDigits[next].focus(); |
|
| 1141 |
+ if (text.length >= otpDigits.length) $('login-form').requestSubmit();
|
|
| 1142 |
+ }); |
|
| 1143 |
+ }); |
|
| 1144 |
+ |
|
| 1145 |
+ function getOtp() { return otpDigits.map(i => i.value).join(''); }
|
|
| 1146 |
+ function clearOtp() { otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); }); otpDigits[0].focus(); }
|
|
| 1036 | 1147 |
|
| 1037 | 1148 |
$('login-form').addEventListener('submit', async (event) => {
|
| 1038 | 1149 |
event.preventDefault(); |
| 1150 |
+ const btn = $('login-btn');
|
|
| 1151 |
+ btn.disabled = true; |
|
| 1152 |
+ $('login-error').textContent = '';
|
|
| 1039 | 1153 |
try {
|
| 1040 |
- await api('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ otp: $('otp').value }) });
|
|
| 1041 |
- $('otp').value = '';
|
|
| 1042 |
- msg('authenticated');
|
|
| 1154 |
+ await api('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ otp: getOtp() }) });
|
|
| 1043 | 1155 |
await refresh(); |
| 1044 |
- } catch (e) { msg(e.message); }
|
|
| 1156 |
+ } catch (e) {
|
|
| 1157 |
+ showLogin(e.message === 'invalid_otp' ? 'Cod incorect.' : e.message); |
|
| 1158 |
+ } finally {
|
|
| 1159 |
+ btn.disabled = false; |
|
| 1160 |
+ } |
|
| 1045 | 1161 |
}); |
| 1046 | 1162 |
|
| 1047 | 1163 |
$('logout').addEventListener('click', async () => {
|
| 1048 | 1164 |
await api('/api/logout', { method: 'POST' }).catch(() => {});
|
| 1049 |
- msg('logged out');
|
|
| 1050 |
- await refresh(); |
|
| 1165 |
+ clearOtp(); |
|
| 1166 |
+ showLogin(); |
|
| 1051 | 1167 |
}); |
| 1052 | 1168 |
|
| 1169 |
+ $('refresh').addEventListener('click', () => refresh().catch(e => msg(e.message)));
|
|
| 1170 |
+ $('filter').addEventListener('input', renderHosts);
|
|
| 1171 |
+ |
|
| 1053 | 1172 |
$('host-form').addEventListener('submit', async (event) => {
|
| 1054 | 1173 |
event.preventDefault(); |
| 1055 | 1174 |
try {
|
@@ -1078,7 +1197,7 @@ sub app_html {
|
||
| 1078 | 1197 |
} catch (e) { msg(e.message); }
|
| 1079 | 1198 |
}); |
| 1080 | 1199 |
|
| 1081 |
- refresh().catch(e => msg(e.message)); |
|
| 1200 |
+ refresh().catch(() => showLogin()); |
|
| 1082 | 1201 |
</script> |
| 1083 | 1202 |
</body> |
| 1084 | 1203 |
</html> |