feat: implement the open fallback access point and captive portal
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #041a2b; color: #e8f7ff; } body { margin: 0; padding: max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right)) max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left)); background: radial-gradient(circle at top, #0e4563, #041a2b 58%); } main { width: min(100%, 38rem); margin: 0 auto; } .eyebrow { color: #8fdbf3; font-size: .78rem; font-weight: 800; letter-spacing: .08em; } h1 { margin: .25rem 0 .5rem; } section, form { margin-top: 1.1rem; padding: 1rem; border: 1px solid #3a7593; border-radius: .8rem; background: rgb(5 31 50 / 88%); } .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } h2 { margin: 0; font-size: 1.1rem; } .networks { display: grid; gap: .5rem; } .network { width: 100%; min-height: 2.75rem; border: 1px solid #548eaa; border-radius: .5rem; background: #123d58; color: #effaff; text-align: left; } label, input, button { display: block; width: 100%; box-sizing: border-box; } label { margin-top: .85rem; font-weight: 700; } input, button { min-height: 2.75rem; margin-top: .35rem; border: 1px solid #609bbb; border-radius: .5rem; padding: .55rem .7rem; font: inherit; } input { background: #061f33; color: #effaff; } button { background: #54c2e5; color: #062033; font-weight: 800; cursor: pointer; } button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #ffe56a; outline-offset: 3px; } .hint, #scan-status { color: #c1ddea; font-size: .9rem; } a { color: #9ce8ff; }
|
||||
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #041a2b; color: #e8f7ff; } body { margin: 0; padding: max(1.25rem, env(safe-area-inset-top)) max(1.25rem, env(safe-area-inset-right)) max(1.25rem, env(safe-area-inset-bottom)) max(1.25rem, env(safe-area-inset-left)); background: radial-gradient(circle at top, #0e4563, #041a2b 58%); } main { width: min(100%, 38rem); margin: 0 auto; } .eyebrow { color: #8fdbf3; font-size: .78rem; font-weight: 800; letter-spacing: .08em; } h1 { margin: .25rem 0 .5rem; } section, form { margin-top: 1.1rem; padding: 1rem; border: 1px solid #3a7593; border-radius: .8rem; background: rgb(5 31 50 / 88%); } .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } h2 { margin: 0; font-size: 1.1rem; } .networks { display: grid; gap: .5rem; } .network { width: 100%; min-height: 2.75rem; border: 1px solid #548eaa; border-radius: .5rem; background: #123d58; color: #effaff; text-align: left; } label, input, button { display: block; width: 100%; box-sizing: border-box; } label { margin-top: .85rem; font-weight: 700; } input, button { min-height: 2.75rem; margin-top: .35rem; border: 1px solid #609bbb; border-radius: .5rem; padding: .55rem .7rem; font: inherit; } input { background: #061f33; color: #effaff; } button { background: #54c2e5; color: #062033; font-weight: 800; cursor: pointer; } button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #ffe56a; outline-offset: 3px; } .hint, #scan-status, #network-status { color: #c1ddea; font-size: .9rem; } .secondary { margin-top: 1rem; background: #315a75; color: #effaff; } a { color: #9ce8ff; }
|
||||
|
||||
+4
-2
@@ -23,9 +23,11 @@
|
||||
<input id="ssid" name="ssid" maxlength="32" autocomplete="off" required>
|
||||
<label for="password">Пароль</label>
|
||||
<input id="password" name="password" type="password" maxlength="63" autocomplete="new-password">
|
||||
<p class="hint">Пароль не отображается на этой странице. Проверка и сохранение сети будут доступны на следующем шаге настройки.</p>
|
||||
<button type="submit">Продолжить</button>
|
||||
<p class="hint">Пароль не отображается после отправки. Пока идёт проверка, оставайтесь в Battleship-open.</p>
|
||||
<button type="submit">Проверить и сохранить</button>
|
||||
</form>
|
||||
<button id="delete-network" type="button" class="secondary">Удалить сохранённую сеть</button>
|
||||
<p id="network-status" role="status"></p>
|
||||
<p><a href="/">Открыть игру</a></p>
|
||||
</main>
|
||||
<script src="/setup.js" defer></script>
|
||||
|
||||
+27
-5
@@ -4,20 +4,42 @@
|
||||
const ssid = document.querySelector('#ssid');
|
||||
const refresh = document.querySelector('#refresh');
|
||||
const form = document.querySelector('#network-form');
|
||||
const password = document.querySelector('#password');
|
||||
const networkStatus = document.querySelector('#network-status');
|
||||
const remove = document.querySelector('#delete-network');
|
||||
let timer = 0;
|
||||
const scan = async () => {
|
||||
clearTimeout(timer); refresh.disabled = true; status.textContent = 'Ищем сети…';
|
||||
try {
|
||||
const response = await fetch('/setup/scan', { cache: 'no-store' });
|
||||
const text = await response.text();
|
||||
const response = await fetch('/api/network/scan', { cache: 'no-store' });
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error('scan unavailable');
|
||||
if (text.trim() === 'scanning') { timer = setTimeout(scan, 700); return; }
|
||||
const names = [...new Set(text.split('\n').map(value => value.trim()).filter(Boolean))];
|
||||
if (result.state === 'scanning') { timer = setTimeout(scan, 700); return; }
|
||||
const names = [...new Set(result.networks || [])];
|
||||
networks.replaceChildren(...names.map(name => { const button = document.createElement('button'); button.className = 'network'; button.type = 'button'; button.textContent = name; button.addEventListener('click', () => { ssid.value = name; ssid.focus(); }); return button; }));
|
||||
status.textContent = names.length ? 'Выберите сеть или введите её вручную.' : 'Сети не найдены. Введите скрытую сеть вручную.';
|
||||
} catch (_) { status.textContent = 'Не удалось выполнить поиск. Введите сеть вручную.'; }
|
||||
finally { if (!timer) refresh.disabled = false; }
|
||||
};
|
||||
const updateStatus = async () => {
|
||||
try {
|
||||
const result = await fetch('/api/network/status', { cache: 'no-store' }).then(response => response.json());
|
||||
networkStatus.textContent = result.message || '';
|
||||
if (result.state === 'validating') setTimeout(updateStatus, 1000);
|
||||
} catch (_) { networkStatus.textContent = 'Не удалось получить состояние сети.'; }
|
||||
};
|
||||
refresh.addEventListener('click', scan);
|
||||
form.addEventListener('submit', event => { event.preventDefault(); status.textContent = 'Сеть выбрана. Проверка и сохранение будут доступны на следующем шаге настройки.'; document.querySelector('#password').value = ''; });
|
||||
form.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/network/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ssid: ssid.value, password: password.value }) });
|
||||
const result = await response.json(); password.value = ''; networkStatus.textContent = result.message || 'Проверяем подключение…';
|
||||
if (response.ok) updateStatus();
|
||||
} catch (_) { password.value = ''; networkStatus.textContent = 'Не удалось начать проверку. Повторите попытку.'; }
|
||||
});
|
||||
remove.addEventListener('click', async () => {
|
||||
try { const result = await fetch('/api/network/delete', { method: 'POST' }).then(response => response.json()); networkStatus.textContent = result.message || 'Сеть удалена.'; }
|
||||
catch (_) { networkStatus.textContent = 'Не удалось удалить сеть.'; }
|
||||
});
|
||||
updateStatus();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user