feat: initial JaniChat messenger — PWA, WebSocket, admin panel

This commit is contained in:
Ai
2026-05-22 11:06:43 +03:00
commit 1cabe9d04f
46 changed files with 3570 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
{
"name": "JaniChat",
"short_name": "JaniChat",
"description": "Корпоративный мессенджер",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "business"]
}

58
frontend/public/sw.js Normal file
View File

@@ -0,0 +1,58 @@
const CACHE_NAME = 'janichat-v1';
const STATIC_ASSETS = ['/', '/index.html'];
self.addEventListener('install', (event) => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS).catch(() => {}))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/') || event.request.url.includes('/ws')) return;
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
});
self.addEventListener('push', (event) => {
let data = { title: 'JaniChat', body: 'Новое сообщение', chatId: null };
try { data = event.data.json(); } catch {}
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon-192.png',
badge: '/icon-192.png',
data: { chatId: data.chatId },
vibrate: [200, 100, 200],
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const chatId = event.notification.data?.chatId;
const url = chatId ? `/?chat=${chatId}` : '/';
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clients => {
for (const client of clients) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.postMessage({ type: 'open_chat', chatId });
return client.focus();
}
}
return self.clients.openWindow(url);
})
);
});