Compare commits

..

No commits in common. "e6acd8c5dff261e4508e1c1a6ace9c017e74e501" and "e6ebf2cbff367d1a78966140353d433a2d315da1" have entirely different histories.

11 changed files with 205 additions and 507 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,69 +1,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<title>NoteJS - Система заметок</title> <title>NoteJS - Система заметок</title>
<!-- Предотвращение мерцания темы --> <!-- Предотвращение мерцания темы -->
<script> <script>
(function () { (function () {
try { try {
// Получаем сохраненную тему // Получаем сохраненную тему
const savedTheme = localStorage.getItem("theme"); const savedTheme = localStorage.getItem("theme");
// Получаем системные предпочтения // Получаем системные предпочтения
const systemPrefersDark = window.matchMedia( const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)" "(prefers-color-scheme: dark)"
).matches; ).matches;
// Определяем тему: сохраненная или системная // Определяем тему: сохраненная или системная
const theme = savedTheme || (systemPrefersDark ? "dark" : "light"); const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
// Функция для конвертации hex в RGB // Функция для конвертации hex в RGB
function hexToRgb(hex) { function hexToRgb(hex) {
const cleanHex = hex.replace("#", ""); const cleanHex = hex.replace("#", "");
const r = parseInt(cleanHex.substring(0, 2), 16); const r = parseInt(cleanHex.substring(0, 2), 16);
const g = parseInt(cleanHex.substring(2, 4), 16); const g = parseInt(cleanHex.substring(2, 4), 16);
const b = parseInt(cleanHex.substring(4, 6), 16); const b = parseInt(cleanHex.substring(4, 6), 16);
return `${r}, ${g}, ${b}`; return `${r}, ${g}, ${b}`;
} }
// Получаем и устанавливаем accentColor // Получаем и устанавливаем accentColor
const savedAccentColor = localStorage.getItem("accentColor"); const savedAccentColor = localStorage.getItem("accentColor");
const accentColor = savedAccentColor || "#007bff"; const accentColor = savedAccentColor || "#007bff";
// Устанавливаем тему и переменные до загрузки CSS // Устанавливаем тему и переменные до загрузки CSS
if (theme === "dark") { if (theme === "dark") {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
} else { } else {
document.documentElement.setAttribute("data-theme", "light"); document.documentElement.setAttribute("data-theme", "light");
} }
// Устанавливаем CSS переменные для accent цвета // Устанавливаем CSS переменные для accent цвета
document.documentElement.style.setProperty("--accent-color", accentColor); document.documentElement.style.setProperty("--accent-color", accentColor);
document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor)); document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor));
// Устанавливаем цвет для meta theme-color // Устанавливаем цвет для meta theme-color
const themeColorMeta = document.querySelector('meta[name="theme-color"]'); const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) { if (themeColorMeta) {
themeColorMeta.setAttribute( themeColorMeta.setAttribute(
"content", "content",
theme === "dark" ? "#1a1a1a" : accentColor theme === "dark" ? "#1a1a1a" : accentColor
); );
} }
} catch (e) { } catch (e) {
// В случае ошибки устанавливаем светлую тему по умолчанию // В случае ошибки устанавливаем светлую тему по умолчанию
document.documentElement.setAttribute("data-theme", "light"); document.documentElement.setAttribute("data-theme", "light");
document.documentElement.style.setProperty("--accent-color", "#007bff"); document.documentElement.style.setProperty("--accent-color", "#007bff");
document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255"); document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255");
} }
})(); })();
</script> </script>
<!-- Критические стили темы для предотвращения flash эффекта --> <!-- Критические стили темы для предотвращения flash эффекта -->
<style> <style>
:root { :root {
--accent-color: #007bff; --accent-color: #007bff;
@ -89,94 +89,94 @@
color: var(--text-primary); color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
</style> </style>
<!-- PWA Meta Tags --> <!-- PWA Meta Tags -->
<meta <meta
name="description" name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/> />
<meta name="theme-color" content="#007bff" /> <meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta <meta
name="apple-mobile-web-app-status-bar-style" name="apple-mobile-web-app-status-bar-style"
content="black-translucent" content="black-translucent"
/> />
<meta name="apple-mobile-web-app-title" content="NoteJS" /> <meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" /> <meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" /> <meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" /> <meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" /> <meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" /> <meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<!-- Icons --> <!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" /> <link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
href="/icons/icon-32x32.png" href="/icons/icon-32x32.png"
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="16x16" sizes="16x16"
href="/icons/icon-16x16.png" href="/icons/icon-16x16.png"
/> />
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" /> <link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" /> <link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" /> <link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" /> <link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="114x114" sizes="114x114"
href="/icons/icon-128x128.png" href="/icons/icon-128x128.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="120x120" sizes="120x120"
href="/icons/icon-128x128.png" href="/icons/icon-128x128.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="144x144" sizes="144x144"
href="/icons/icon-144x144.png" href="/icons/icon-144x144.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="152x152" sizes="152x152"
href="/icons/icon-152x152.png" href="/icons/icon-152x152.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href="/icons/icon-192x192.png" href="/icons/icon-192x192.png"
/> />
<link rel="mask-icon" href="/icon.svg" color="#007bff" /> <link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest --> <!-- Manifest -->
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-B61qRIc-.js"></script> <script type="module" crossorigin src="/assets/index-42KwbWCP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DK8OUj6L.css"> <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> <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body> <body>
<div id="root"> <div id="root">
<!-- Индикатор загрузки до монтирования React --> <!-- Индикатор загрузки до монтирования React -->
<div id="initial-loading" style=" <div id="initial-loading" style="
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: var(--bg-primary); background-color: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 9999; z-index: 9999;
"> ">
<style> <style>
#initial-loading-spinner { #initial-loading-spinner {
width: 50px; width: 50px;
@ -193,43 +193,43 @@
@keyframes initial-loading-spin { @keyframes initial-loading-spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
</style> </style>
<div id="initial-loading-spinner"></div> <div id="initial-loading-spinner"></div>
</div> </div>
</div> </div>
<script> <script>
// Скрываем индикатор загрузки сразу после загрузки DOM // Скрываем индикатор загрузки сразу после загрузки DOM
// React удалит этот элемент при первом рендере через createRoot // React удалит этот элемент при первом рендере через createRoot
(function() { (function() {
// Используем MutationObserver для отслеживания изменений в #root // Используем MutationObserver для отслеживания изменений в #root
const observer = new MutationObserver(function(mutations) { const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) { mutations.forEach(function(mutation) {
// Если React начал добавлять элементы в #root, удаляем индикатор // Если React начал добавлять элементы в #root, удаляем индикатор
if (mutation.addedNodes.length > 0) { if (mutation.addedNodes.length > 0) {
const loadingEl = document.getElementById('initial-loading'); const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) { if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl); loadingEl.parentNode.removeChild(loadingEl);
} }
observer.disconnect(); observer.disconnect();
} }
}); });
}); });
// Начинаем наблюдение за изменениями в #root // Начинаем наблюдение за изменениями в #root
const root = document.getElementById('root'); const root = document.getElementById('root');
if (root) { if (root) {
observer.observe(root, { childList: true, subtree: true }); observer.observe(root, { childList: true, subtree: true });
// Фолбэк: если через 2 секунды элемент все еще есть, удаляем вручную // Фолбэк: если через 2 секунды элемент все еще есть, удаляем вручную
setTimeout(function() { setTimeout(function() {
const loadingEl = document.getElementById('initial-loading'); const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) { if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl); loadingEl.parentNode.removeChild(loadingEl);
} }
observer.disconnect(); observer.disconnect();
}, 2000); }, 2000);
} }
})(); })();
</script> </script>
</body> </body>
</html> </html>

View File

@ -1 +0,0 @@
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 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")}); 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")});

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -239,62 +239,10 @@ export const offlineNotesApi = {
store.dispatch(addNote(noteWithSyncStatus)); store.dispatch(addNote(noteWithSyncStatus));
return noteWithSyncStatus; return noteWithSyncStatus;
} catch (error: any) { } catch (error) {
// Проверяем, является ли это сетевой ошибкой console.error("Error creating note, falling back to local:", error);
const isNetworkError = // Fallback на локальное создание
!error.response && return offlineNotesApi.create(note);
(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,29 +24,7 @@ async function initOfflineMode() {
console.log('[Init] IndexedDB initialized'); console.log('[Init] IndexedDB initialized');
// Проверка состояния сети // Проверка состояния сети
// Сначала проверяем navigator.onLine для быстрой проверки const isOnline = await checkNetworkStatus();
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)); store.dispatch(setOfflineMode(!isOnline));
console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`); console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`);

View File

@ -47,7 +47,6 @@ export async function checkNetworkStatus(): Promise<boolean> {
} }
// Дополнительная проверка через fetch с коротким таймаутом // Дополнительная проверка через fetch с коротким таймаутом
// Используем короткий таймаут для быстрого определения оффлайна
try { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000); const timeoutId = setTimeout(() => controller.abort(), 2000);
@ -62,36 +61,9 @@ export async function checkNetworkStatus(): Promise<boolean> {
clearTimeout(timeoutId); clearTimeout(timeoutId);
return response.ok; return response.ok;
} catch (error: any) { } catch (error) {
// Если запрос не удался, проверяем тип ошибки // Если запрос не удался, но navigator.onLine = true, считаем что онлайн
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; return navigator.onLine;
} }
} }

View File

@ -89,29 +89,6 @@ export default defineConfig({
}, },
workbox: { workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"], 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: [ runtimeCaching: [
{ {
urlPattern: /^https:\/\/api\./, urlPattern: /^https:\/\/api\./,
@ -162,8 +139,6 @@ export default defineConfig({
cleanupOutdatedCaches: true, cleanupOutdatedCaches: true,
skipWaiting: true, skipWaiting: true,
clientsClaim: true, clientsClaim: true,
// Обработка ошибок при precaching - игнорируем 404 ошибки
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
}, },
registerType: "prompt", registerType: "prompt",
devOptions: { devOptions: {