From 9ca8d8312a8bcd01d00843d64b8c24bfd4b7a52d Mon Sep 17 00:00:00 2001 From: Server <10.9.8.250@reg.snw.su> Date: Mon, 10 Aug 2026 18:09:29 +0000 Subject: [PATCH] Render cached manifest before background sync --- README.md | 6 ++++-- docslib/__init__.py | 2 +- docslib/templates/client.js | 28 ++++++++++++++++++++-------- docslib/templates/index.html | 14 +++++++++++--- docslib/templates/loader.js | 27 +++++---------------------- pyproject.toml | 2 +- tests/test_server.py | 24 ++++++++++++++++++++++-- uv.lock | 2 +- 8 files changed, 65 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 43eb124..381a0fd 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ SharedDocsLib — небольшой Python-фреймворк для докум UI не содержит языко-зависимых служебных подписей. Базовый шрифт — `system-ui 16px`; служебные UTF-8-пиктограммы используют Arial, а навигационные стрелки остаются в `system-ui`. AMOLED-тема включена по умолчанию и сохраняется локально после переключения. -На десктопе компактный header содержит полное название документации и search/theme/refresh, а слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Кнопка `⟳` вращается во время проверки manifest; при наведении она заменяет поиск и тему, показывая ISO-дату и короткий `revision`, а при нажатии синхронизирует manifest и перезагружает страницу. +На десктопе компактный header содержит полное название документации и search/theme/refresh, а слева постоянно видны header-кнопки и дерево страниц. Поисковый placeholder медленно печатает первые слова случайного названия, добавляет троеточие только в конце и сохраняет готовый текст 5 секунд; при вводе поле раскрывает ранжированные результаты вниз. Кнопка `⟳` вращается во время проверки manifest; при наведении она заменяет поиск и тему, показывая ISO-дату и короткий `revision`, а при нажатии запускает повторную фоновую проверку обновления. На мобильных устройствах используется viewport `100dvh` и нижний dock. Кнопка `☰` заменяет содержимое страницы копией desktop-sidebar. Ввод в нижний поиск заменяет содержимое результатами, расположенными снизу вверх по релевантности. Тема и обновление на мобильном скрыты. @@ -194,7 +194,9 @@ Manifest имеет версию схемы, заголовок, `revision`, `up } ``` -При открытии минимальный HTML-loader показывает серый fixed-overlay размером `100vw × 100dvh`, проверяет IndexedDB и один раз запрашивает `manifest_path`. Затем он применяет CSS, запускает системный JavaScript и удаляет overlay только после события первого успешного рендера. При недоступном сервере используется кеш или встроенная копия manifest. +При открытии минимальный HTML-loader показывает тематический fixed-overlay размером `100vw × 100dvh` с индикатором загрузки. Он немедленно читает manifest из IndexedDB; если записи ещё нет, сначала сохраняет встроенную копию. Затем loader применяет CSS, запускает системный JavaScript, рендерит документацию и удаляет overlay. + +Только после первого рендера запускается фоновый запрос к `manifest_path`: пользователь уже может читать документацию и работать с интерфейсом. Найденное обновление сохраняется в IndexedDB без замены текущей страницы и применяется при следующем открытии. При недоступном сервере продолжает работать уже отрендеренная кешированная или встроенная версия. `custom_css` и `custom_js` принимают пути к UTF-8-файлам. Пользовательский CSS добавляется после системного, поэтому может переопределять тему. Пользовательский JavaScript загружается после системного runtime и до события `docslib:rendered`: diff --git a/docslib/__init__.py b/docslib/__init__.py index f423191..d91e53a 100644 --- a/docslib/__init__.py +++ b/docslib/__init__.py @@ -5,4 +5,4 @@ from .server import build, create_app, run from .ui import HeaderButton __all__ = ["Component", "HeaderButton", "Page", "build", "create_app", "run"] -__version__ = "0.3.1" +__version__ = "0.3.2" diff --git a/docslib/templates/client.js b/docslib/templates/client.js index f3d141a..69facd1 100644 --- a/docslib/templates/client.js +++ b/docslib/templates/client.js @@ -43,6 +43,7 @@ let placeholderPhase = "typing"; let lastPlaceholderPage = -1; let customScriptLoaded = false; + let lastSyncedRevision = String(manifest.revision || ""); const dbPromise = new Promise((resolve, reject) => { if (!window.indexedDB) return reject(new Error("IndexedDB")); @@ -446,16 +447,24 @@ setSyncing(true); let changed = false; try { + if ( + isManifest(embedded) && + embedded.schema_version === manifest.schema_version && + String(embedded.updated_at || "") > String(manifest.updated_at || "") + ) { + await cachePut(embedded).catch(() => {}); + lastSyncedRevision = String(embedded.revision || lastSyncedRevision); + changed = embedded.revision !== manifest.revision; + } const response = await fetch(config.manifestUrl, { cache: "no-store" }); if (!response.ok) throw new Error(String(response.status)); const remote = await response.json(); if (!isManifest(remote) || remote.schema_version !== embedded.schema_version) throw new Error("manifest"); - changed = remote.revision !== manifest.revision || remote.updated_at !== manifest.updated_at; - if (changed) { + const remoteChanged = remote.revision !== lastSyncedRevision; + if (remoteChanged) { await cachePut(remote).catch(() => {}); - manifest = remote; - location.reload(); - return true; + lastSyncedRevision = String(remote.revision || lastSyncedRevision); + changed = true; } elements.refresh.classList.remove("error"); updateIdentity(); @@ -474,7 +483,10 @@ const response = await fetch(config.liveUrl, { cache: "no-store" }); if (!response.ok) return; const live = await response.json(); - if (live.revision && live.revision !== manifest.revision) await syncManifest(); + if (live.revision && live.revision !== lastSyncedRevision) { + const changed = await syncManifest(); + if (changed) location.reload(); + } } catch (_) { /* local preview may stop */ } } @@ -482,6 +494,7 @@ applyTheme(localStorage.getItem("docslib-theme") || "dark"); renderAll(); announceRendered(); + syncManifest(); if (config.livePreview) setInterval(pollLive, 1200); } @@ -503,8 +516,7 @@ elements.refresh.addEventListener("blur", () => elements.toolbar.classList.remove("refresh-expanded")); elements.refresh.addEventListener("click", async () => { if (syncing) return; - const changed = await syncManifest(); - if (!changed) location.reload(); + await syncManifest(); }); document.addEventListener("pointerdown", (event) => { if (!elements.desktopSearchShell.contains(event.target)) elements.desktopSearchShell.classList.remove("open"); diff --git a/docslib/templates/index.html b/docslib/templates/index.html index 5677f25..777d62e 100644 --- a/docslib/templates/index.html +++ b/docslib/templates/index.html @@ -5,13 +5,21 @@