From 956dd1a75b98dca0cefde1c0066caf133524cdae Mon Sep 17 00:00:00 2001 From: TraceLumen Date: Sat, 19 Sep 2026 17:36:33 +0300 Subject: [PATCH] v1.1.0 upd connections instruct and checkup --- src/lib/common/services/network.service.js | 93 +++------ src/lib/common/ui/NetworkGate.svelte | 104 +++++++--- src/lib/pages/main/MainMenu.svelte | 2 +- src/lib/pages/print/PrintMenu.svelte | 40 ++-- src/lib/pages/print/PrintProgress.svelte | 222 ++++++++++----------- 5 files changed, 222 insertions(+), 239 deletions(-) diff --git a/src/lib/common/services/network.service.js b/src/lib/common/services/network.service.js index cfaacbc..361b600 100644 --- a/src/lib/common/services/network.service.js +++ b/src/lib/common/services/network.service.js @@ -1,88 +1,45 @@ // lib/common/services/network.service.js // -// Проверка подключения через эндпоинты бэкенда: +// Определение сети, в которой находится устройство. // -// - /api/is_server — публичный сервер unitprint.ru. -// Отвечает {"is_server":true} → интернет есть, скан доступен; -// - local.unitprint.ru/api/is_local — локальный сервер печати. -// Отвечает {"is_local":true} → устройство в Wi-Fi принтера (UnitPrintLocal), -// печать доступна. +// - внутри локальной сети принтера (Wi-Fi UnitPrintLocal) +// домен unitprint.ru резолвится на 192.168.20.1 — локальный сервер печати; +// - из интернета домен отвечает публичным адресом 95.165.135.233. // -// Сайт всегда отдаётся с unitprint.ru, поэтому проверка локалки ходит -// на отдельный хост local.unitprint.ru: вне локальной сети принтера -// он просто недоступен. -// -// Скан работает только через интернет, печать — только через локальную сеть. +// Клиент не может сам отличить эти случаи по IP (сертификаты, mixed content), +// поэтому спрашиваем сам сервер: GET /api/is_local → { "is_local": true|false }. +// Эндпоинт обязан быть на обоих инстансах: +// локальный → { "is_local": true } +// публичный → { "is_local": false } -export const LOCAL_HOST = 'local.unitprint.ru'; +export const DOMAIN = 'unitprint.ru'; +export const PUBLIC_IP = '95.165.135.233'; +export const LOCAL_IP = '192.168.20.1'; export const LOCAL_WIFI_NAME = 'UnitPrintLocal'; /** - * GET с таймаутом, читаем JSON. - * @returns {Promise} null — если хост недоступен или ошибка. + * Определяет текущий режим сети. + * @returns {Promise<'local' | 'internet' | 'offline'>} + * - 'local' → отвечающий сервер — локальный (домен → 192.168.20.1) + * - 'internet' → отвечающий сервер — публичный (домен → 95.165.135.233) + * - 'offline' → сервер вообще не ответил */ -async function getJson(url, timeoutMs = 4000) { - if (typeof fetch === 'undefined') return null; +export async function detectNetwork(timeoutMs = 6000) { + if (typeof fetch === 'undefined') return 'offline'; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { - const res = await fetch(url, { - method: 'GET', + const res = await fetch('/api/is_local', { cache: 'no-store', signal: controller.signal, }); - if (!res.ok) return null; - return await res.json(); + if (!res.ok) return 'offline'; + const data = await res.json(); + if (typeof data?.is_local !== 'boolean') return 'offline'; + return data.is_local ? 'local' : 'internet'; } catch { - return null; + return 'offline'; } finally { clearTimeout(timer); } } - -/** - * Доступность хоста в режиме 'no-cors': CORS не мешает, важен сам факт ответа. - */ -async function canReach(url, timeoutMs = 4000) { - if (typeof fetch === 'undefined') return false; - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - try { - await fetch(url, { - method: 'GET', - mode: 'no-cors', - cache: 'no-store', - signal: controller.signal, - }); - return true; - } catch { - return false; - } finally { - clearTimeout(timer); - } -} - -/** - * Устройство в локальной сети принтера: - * local.unitprint.ru/api/is_local отвечает {"is_local":true}. - */ -export async function isLocalNetwork() { - const ts = Date.now(); - const httpsUrl = `https://${LOCAL_HOST}/api/is_local?ping=${ts}`; - const httpUrl = `http://${LOCAL_HOST}/api/is_local?ping=${ts}`; - - const data = (await getJson(httpsUrl, 3000)) ?? (await getJson(httpUrl, 3000)); - if (data) return data.is_local === true; - - // JSON не читается (CORS / самоподписанный сертификат) —fallback: - // сам факт ответа local.unitprint.ru означает, что мы в локалке. - return canReach(httpsUrl, 3000); -} - -/** - * Есть интернет: публичный сервер отвечает на /api/is_server. - */ -export async function isServerReachable() { - const data = await getJson(`/api/is_server?ping=${Date.now()}`, 5000); - return data?.is_server === true; -} diff --git a/src/lib/common/ui/NetworkGate.svelte b/src/lib/common/ui/NetworkGate.svelte index cb419f5..5f76324 100644 --- a/src/lib/common/ui/NetworkGate.svelte +++ b/src/lib/common/ui/NetworkGate.svelte @@ -1,24 +1,33 @@ + @@ -71,15 +65,8 @@ {:else if store.showPayment} - {#if printNetGate === 'blocked'} + {#if printNetGate} - {:else if printNetGate === 'checking'} -
-
-
-

Проверка подключения…

-
-
{:else} - .net-checking { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px; padding: 40px 20px; color: #8b93a1; font-size: 15px; font-weight: 600; text-align: center; } - .net-spinner { width: 42px; height: 42px; border-radius: 50%; border: 3px solid rgba(255,255,255,0.12); border-top-color: #6366f1; animation: net-spin 0.8s linear infinite; } - @keyframes net-spin { to { transform: rotate(360deg); } } .page-container { width: 100%; min-height: 100dvh; display: flex; flex-direction: column; background: radial-gradient(120% 60% at 50% -10%, rgba(99,102,241,0.18) 0%, transparent 60%); font-family: system-ui, -apple-system, sans-serif; color: #f5f7fa; } .header { padding: 24px 20px 8px; display: flex; align-items: center; position: relative; } .back-btn { background: none; border: none; color: #6366f1; font-size: 15px; font-weight: 600; cursor: pointer; padding: 8px 4px; margin-right: auto; min-height: 44px; } diff --git a/src/lib/pages/print/PrintProgress.svelte b/src/lib/pages/print/PrintProgress.svelte index f3a0a69..65fb025 100644 --- a/src/lib/pages/print/PrintProgress.svelte +++ b/src/lib/pages/print/PrintProgress.svelte @@ -109,17 +109,18 @@ /** * Инструкция очистки лотка: * - если сервер явно просит awaiting_clear_output И есть уже напечатанные файлы; - * - для двусторонней печати — до первого прохода, - * пока ещё не было flip/pickup/done И есть напечатанные файлы. + * - для двусторонней печати — ДО первого прохода, + * пока ещё не было flip/pickup/done И есть напечатанные файлы + * (т.е. файл не первый в очереди и лоток уже занят). */ - const showClearOutput = $derived( - store.jobActive && - !firstSideFinished && - ( - (hasClearPhase && hasPrintedFiles) || - (isDuplex && hasPrintedFiles && ['waiting_start', 'printing', 'awaiting_clear_output'].includes(jobPhase ?? '')) - ) - ); + const showClearOutput = $derived( + store.jobActive && + ( + jobPhase === 'waiting_start' || + jobPhase === 'awaiting_clear_output' || + jobFiles.some((f) => f.status === 'awaiting_clear_output') + ) + ); const showFlip = $derived( store.jobActive && @@ -149,11 +150,40 @@ showPickup && jobPhase === 'awaiting_pickup' ); - const clearButtonLabel = $derived( - jobPhase === 'waiting_start' - ? 'Убрал листы — начать печать' - : 'Убрал листы — продолжать' + const clearButtonLabel = $derived( + jobPhase === 'waiting_start' + ? 'Начать печать' + : 'Убрал листы — продолжать' + ); + + /** + * В какой карточке файла показывать инструкцию. + * Инструкция всегда живёт ВНУТРИ карточки соответствующего файла, + * а не отдельным блоком над списком. + */ + const flipTarget = $derived( + showFlip ? (jobFiles.find((f) => f.status === 'awaiting_flip') ?? null) : null ); + const pickupTarget = $derived( + showPickup ? (jobFiles.find((f) => f.status === 'awaiting_pickup') ?? null) : null + ); + const clearTarget = $derived.by(() => { + if (!showClearOutput) return null; + return ( + jobFiles.find((f) => f.status === 'awaiting_clear_output') ?? + (jobPhase !== 'waiting_start' ? jobFiles.find((f) => f.status === 'printing') : undefined) ?? + jobFiles.find((f) => f.status === 'queued') ?? + null + ); + }); + + /** @returns {'clear' | 'flip' | 'pickup' | null} */ + function hintFor(f) { + if (showClearOutput && clearTarget === f) return 'clear'; + if (showFlip && flipTarget === f) return 'flip'; + if (showPickup && pickupTarget === f) return 'pickup'; + return null; + } const HOLD_MS = 1500; let hold = $state(0); @@ -201,57 +231,15 @@
- {#if showClearOutput} -
-

- Уберите распечатанные листы из выходного лотка -

-
    -
  1. Уберите распечатанные листы из выходного лотка
  2. - {#if clearActionAvailable} -
  3. Нажмите кнопку ниже для продолжения
  4. - {:else} -
  5. Дождитесь завершения первого прохода
  6. - {/if} -
-
- {:else if showFlip} -
-

- Переложите бумагу -

-
    -
  1. Не переворачивая, положите листы в слот ручной подачи
  2. - {#if flipActionAvailable} -
  3. Нажмите кнопку ниже для печати обратной стороны
  4. - {:else} -
  5. Дождитесь завершения текущего этапа
  6. - {/if} -
-
- {:else if showPickup} -
-

- Печать завершается... -

-
    -
  1. Дождитесь завершения печати
  2. - {#if pickupActionAvailable} -
  3. Нажмите кнопку ниже для подтверждения
  4. - {:else} -
  5. Дождитесь завершения текущего этапа
  6. - {/if} -
-
- {/if} - {#if store.job} {#each store.job.files as f, i (i + '-' + f.name)} {@const b = badge(f.name)} + {@const hint = hintFor(f)} {@const isActive = f.status === 'awaiting_flip' || f.status === 'awaiting_clear_output' || f.status === 'awaiting_pickup' - || f.status === 'printing'} + || f.status === 'printing' + || hint !== null}
@@ -262,6 +250,41 @@
+ {#if hint === 'clear'} +
    + {#if jobPhase !== 'waiting_start'} +
  1. Уберите распечатанные листы из выходного лотка
  2. + {/if} + + {#if clearActionAvailable} +
  3. + Нажмите кнопку ниже + {jobPhase === 'waiting_start' ? 'для начала печати' : 'для продолжения'} +
  4. + {:else} +
  5. Дождитесь завершения первого прохода
  6. + {/if} +
+ {:else if hint === 'flip'} +
    +
  1. Не переворачивая, положите листы в слот ручной подачи
  2. + {#if flipActionAvailable} +
  3. Нажмите кнопку ниже для печати обратной стороны
  4. + {:else} +
  5. Дождитесь завершения текущего этапа
  6. + {/if} +
+ {:else if hint === 'pickup'} +
    +
  1. Дождитесь завершения печати
  2. + {#if pickupActionAvailable} +
  3. Нажмите кнопку ниже для подтверждения
  4. + {:else} +
  5. Дождитесь завершения текущего этапа
  6. + {/if} +
+ {/if} + {#if f.error}

{f.error}

{/if} @@ -271,19 +294,17 @@