Compare commits

...

3 Commits

Author SHA1 Message Date
e6acd8c5df Обновлены файлы для продакшена 2025-11-06 22:03:25 +07:00
351ba7eb03 Улучшена логика проверки сетевого статуса в функции initOfflineMode, добавлены дополнительные проверки для определения оффлайн-режима. Обновлена обработка ошибок при создании заметок, добавлена логика для обработки сетевых ошибок и локального создания заметок. Оптимизирована функция checkNetworkStatus для более точного определения состояния сети с учетом таймаутов и различных типов ошибок. 2025-11-06 22:02:07 +07:00
2c23044ea4 Добавлены улучшения в конфигурацию Vite для PWA, включая обработку ошибок при кэшировании и фильтрацию манифеста. Обновлен серверный код для корректного возврата статуса аутентификации. Внесены изменения в клиентскую логику Axios для исключения определенных запросов из автоматического разлогинивания. Обновлен HTML-шаблон с улучшениями в загрузке и отображении темы. 2025-11-06 21:13:37 +07:00
11 changed files with 507 additions and 205 deletions

File diff suppressed because one or more lines are too long

View File

@ -158,7 +158,7 @@
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-42KwbWCP.js"></script>
<script type="module" crossorigin src="/assets/index-B61qRIc-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DK8OUj6L.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body>

View File

@ -0,0 +1 @@
if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js', { scope: '/' })})}

View File

@ -1 +1 @@
if(!self.define){let e,i={};const n=(n,c)=>(n=new URL(n+".js",c).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(c,o)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let r={};const d=e=>n(e,s),a={module:{uri:s},exports:r,require:d};i[s]=Promise.all(c.map(e=>a[e]||d(e))).then(e=>(o(...e),r))}}define(["./workbox-57555046"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.precacheAndRoute([{url:"assets/index-CRKRzJj1.js",revision:null},{url:"assets/index-QEK5TGz3.css",revision:null},{url:"assets/workbox-window.prod.es5-B9K5rw8f.js",revision:null},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"52c85beb0841c0c7c8ddf774370cff39"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"))),e.registerRoute(/^https:\/\/api\./i,new e.NetworkFirst({cacheName:"api-cache",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/i,new e.CacheFirst({cacheName:"image-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});
if(!self.define){let e,i={};const n=(n,c)=>(n=new URL(n+".js",c).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(c,o)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let a={};const d=e=>n(e,s),r={module:{uri:s},exports:a,require:d};i[s]=Promise.all(c.map(e=>r[e]||d(e))).then(e=>(o(...e),a))}}define(["./workbox-e20531c6"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-B61qRIc-.js",revision:"96888e49126c254a0b6fb7a9428bddb6"},{url:"assets/index-DK8OUj6L.css",revision:"b1e2c4e8724be2f2bcee585338910e99"},{url:"icon.svg",revision:"0ec61aab261526d4c491e887a6f3374e"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"45ec10836831308e81415ddb8cc82efd"},{url:"logo.svg",revision:"5962d0d24d9cd26cd8aaff9cb6f54a5a"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"0ec61aab261526d4c491e887a6f3374e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api/,/^\/uploads/]})),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});

File diff suppressed because one or more lines are too long

View File

@ -785,7 +785,8 @@ app.get("/api/auth/status", (req, res) => {
username: req.session.username,
});
} else {
res.status(401).json({ authenticated: false });
// Возвращаем 200, так как неавторизованное состояние - это норма, а не ошибка
res.status(200).json({ authenticated: false });
}
});

View File

@ -42,13 +42,23 @@ axiosClient.interceptors.response.use(
"/user/delete-account", // Удаление аккаунта
];
// URL, где 401 не должен обрабатываться как ошибка сессии
const statusCheckUrls = [
"/auth/status", // Проверка статуса аутентификации
];
// Проверяем, является ли это запросом с проверкой пароля
const isPasswordProtected = passwordProtectedUrls.some((url) =>
error.config?.url?.includes(url)
);
// Разлогиниваем только если это НЕ запрос с проверкой пароля
if (!isPasswordProtected) {
// Проверяем, является ли это запросом проверки статуса
const isStatusCheck = statusCheckUrls.some((url) =>
error.config?.url?.includes(url)
);
// Разлогиниваем только если это НЕ запрос с проверкой пароля и НЕ проверка статуса
if (!isPasswordProtected && !isStatusCheck) {
// Очищаем IndexedDB при автоматическом разлогинивании
dbManager.clearAll().catch((err) => {
console.error("Ошибка очистки IndexedDB при 401:", err);

View File

@ -239,10 +239,62 @@ export const offlineNotesApi = {
store.dispatch(addNote(noteWithSyncStatus));
return noteWithSyncStatus;
} catch (error) {
console.error("Error creating note, falling back to local:", error);
// Fallback на локальное создание
return offlineNotesApi.create(note);
} catch (error: any) {
// Проверяем, является ли это сетевой ошибкой
const isNetworkError =
!error.response &&
(error.code === 'ERR_NETWORK' ||
error.message === 'Network Error' ||
error.message?.includes('ERR_INTERNET_DISCONNECTED') ||
error.message?.includes('Failed to fetch'));
if (isNetworkError) {
console.error("Network error creating note, falling back to local:", error);
// Принудительно обновляем статус сети при ошибке
lastNetworkCheck = { time: Date.now(), status: false };
store.dispatch(setOfflineMode(true));
// Fallback на локальное создание напрямую, без рекурсии
console.log("[Offline] Creating note locally after network error");
const tempId = generateTempId();
const now = new Date().toISOString();
const newNote: Note = {
...note,
id: tempId,
user_id: userId || 0,
created_at: now,
updated_at: now,
is_pinned: 0,
is_archived: 0,
images: [],
files: [],
syncStatus: "pending",
};
// Сохраняем в IndexedDB
await dbManager.saveNote(newNote);
// Добавляем в очередь синхронизации
await dbManager.addToSyncQueue({
type: "create",
noteId: tempId,
data: note,
timestamp: Date.now(),
retries: 0,
});
// Обновляем UI
store.dispatch(addNote(newNote));
await updatePendingSyncCount();
return newNote;
} else {
// Если это не сетевая ошибка, пробрасываем её дальше
console.error("Error creating note (not a network error):", error);
throw error;
}
}
},

View File

@ -24,7 +24,29 @@ async function initOfflineMode() {
console.log('[Init] IndexedDB initialized');
// Проверка состояния сети
const isOnline = await checkNetworkStatus();
// Сначала проверяем navigator.onLine для быстрой проверки
let isOnline: boolean = navigator.onLine;
// Если navigator.onLine = false, точно оффлайн (не нужно делать fetch)
if (!navigator.onLine) {
isOnline = false;
} else {
// Если navigator.onLine = true, делаем дополнительную проверку через fetch
try {
isOnline = await checkNetworkStatus();
} catch (error) {
// Если проверка сети упала с ошибкой, скорее всего мы оффлайн
console.warn('[Init] Network status check failed, assuming offline:', error);
isOnline = false;
}
}
// Финальная проверка: если navigator.onLine = false, точно оффлайн
// Это важно, так как navigator.onLine может обновиться во время проверки
if (!navigator.onLine) {
isOnline = false;
}
store.dispatch(setOfflineMode(!isOnline));
console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`);

View File

@ -47,6 +47,7 @@ export async function checkNetworkStatus(): Promise<boolean> {
}
// Дополнительная проверка через fetch с коротким таймаутом
// Используем короткий таймаут для быстрого определения оффлайна
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
@ -61,9 +62,36 @@ export async function checkNetworkStatus(): Promise<boolean> {
clearTimeout(timeoutId);
return response.ok;
} catch (error) {
// Если запрос не удался, но navigator.onLine = true, считаем что онлайн
// (возможно, просто таймаут или другая проблема)
} catch (error: any) {
// Если запрос не удался, проверяем тип ошибки
const isAbortError = error.name === 'AbortError'; // Таймаут
const isNetworkError =
error.message === 'Failed to fetch' ||
error.message?.includes('NetworkError') ||
error.message?.includes('ERR_INTERNET_DISCONNECTED') ||
error.message?.includes('ERR_NETWORK') ||
error.message?.includes('network request failed');
// Если это явная сетевая ошибка (не таймаут), точно оффлайн
if (isNetworkError && !isAbortError) {
return false;
}
// Если это таймаут, проверяем navigator.onLine
// Таймаут может быть как из-за оффлайна, так и из-за медленного соединения
if (isAbortError) {
// Если navigator.onLine = false, точно оффлайн
if (!navigator.onLine) {
return false;
}
// Если navigator.onLine = true, но таймаут - возможно медленное соединение
// Но для безопасности считаем оффлайном, так как запрос не прошел
return false;
}
// Если это не сетевая ошибка и не таймаут (например, CORS или другая проблема),
// но navigator.onLine = true, считаем что онлайн
// (возможно, просто другая проблема, но сеть есть)
return navigator.onLine;
}
}

View File

@ -89,6 +89,29 @@ export default defineConfig({
},
workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
// Игнорируем параметры URL при кешировании
ignoreURLParametersMatching: [/^utm_/, /^fbclid$/],
// Обработка ошибок при precaching - не падаем на 404
navigateFallback: "/index.html",
navigateFallbackDenylist: [/^\/api/, /^\/uploads/],
// Обработка ошибок при загрузке файлов для precaching
dontCacheBustURLsMatching: /\.\w{8}\./,
// Фильтруем манифест, чтобы исключить несуществующие файлы и дубликаты
manifestTransforms: [
async (manifestEntries) => {
// Фильтруем дубликаты
const seen = new Set<string>();
const filtered = manifestEntries.filter((entry) => {
// Удаляем дубликаты
if (seen.has(entry.url)) {
return false;
}
seen.add(entry.url);
return true;
});
return { manifest: filtered, warnings: [] };
},
],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\./,
@ -139,6 +162,8 @@ export default defineConfig({
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
// Обработка ошибок при precaching - игнорируем 404 ошибки
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
},
registerType: "prompt",
devOptions: {