Fix WebSocket URL handling and simplify .env.local setup
This commit is contained in:
parent
8a083f6f67
commit
66e82fc18a
@ -9,6 +9,5 @@ class ApiConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
"""Инициализация при загрузке приложения."""
|
"""Инициализация при загрузке приложения."""
|
||||||
# Применяем патч для django-scheduler (исправление бага с byweekday)
|
# Применяем патч для django-scheduler (исправление бага с byweekday)
|
||||||
print("🔧 ApiConfig.ready() - применение патча django-scheduler")
|
|
||||||
import schedule_patch
|
import schedule_patch
|
||||||
schedule_patch.apply_patch()
|
schedule_patch.apply_patch()
|
||||||
|
|||||||
@ -14,35 +14,34 @@ Endpoints:
|
|||||||
- Удаление legacy кода: 15 января 2026
|
- Удаление legacy кода: 15 января 2026
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone as dt_tz
|
from datetime import datetime
|
||||||
from django.contrib.auth import get_user_model
|
from datetime import timezone as dt_tz
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q, Count, Prefetch, Exists, OuterRef, Subquery
|
|
||||||
from django.db.models.functions import Coalesce
|
|
||||||
from django.utils import timezone
|
|
||||||
from rest_framework import viewsets, status
|
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
|
from communications.models import (Chat, ChatMembership, ChatReadState,
|
||||||
from communications.models import (
|
ChatUserSettings, Message,
|
||||||
Chat, Message, MessageAttachment, MessageReaction,
|
MessageAttachment, MessageReaction, Poll,
|
||||||
Poll, PollOption, PollVote, ChatMembership, ChatUserSettings,
|
PollOption, PollVote)
|
||||||
ChatReadState
|
|
||||||
)
|
|
||||||
from communications.serialization import serialize_message
|
from communications.serialization import serialize_message
|
||||||
from communications.views import user_can_access_chat, _coerce_ts
|
from communications.views import _coerce_ts, user_can_access_chat
|
||||||
from .serializers import (
|
from django.contrib.auth import get_user_model
|
||||||
ChatListSerializer, ChatDetailSerializer,
|
from django.db import transaction
|
||||||
MessageListSerializer, MessageDetailSerializer,
|
from django.db.models import Count, Exists, OuterRef, Prefetch, Q, Subquery
|
||||||
MessageCreateSerializer, MessageEditSerializer,
|
from django.db.models.functions import Coalesce
|
||||||
PollSerializer, ReactionSerializer,
|
from django.utils import timezone
|
||||||
ForwardMessageSerializer, BulkDeleteSerializer,
|
from rest_framework import status, viewsets
|
||||||
MessageAttachmentSerializer
|
from rest_framework.decorators import action
|
||||||
)
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from .serializers import (BulkDeleteSerializer, ChatDetailSerializer,
|
||||||
|
ChatListSerializer, ForwardMessageSerializer,
|
||||||
|
MessageAttachmentSerializer, MessageCreateSerializer,
|
||||||
|
MessageDetailSerializer, MessageEditSerializer,
|
||||||
|
MessageListSerializer, PollSerializer,
|
||||||
|
ReactionSerializer)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
Employee = get_user_model()
|
Employee = get_user_model()
|
||||||
@ -106,7 +105,8 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
'department', 'created_by'
|
'department', 'created_by'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'participants',
|
'participants',
|
||||||
Prefetch('user_settings', queryset=ChatUserSettings.objects.filter(user=user))
|
Prefetch('user_settings',
|
||||||
|
queryset=ChatUserSettings.objects.filter(user=user))
|
||||||
).annotate(
|
).annotate(
|
||||||
unread_count=unread_count_subq
|
unread_count=unread_count_subq
|
||||||
).distinct().order_by('-created_at')
|
).distinct().order_by('-created_at')
|
||||||
@ -183,19 +183,22 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
logger.info(f"[auto_mark_read] Created: user={user.id}, chat={chat.id}, msg={last_message.id}")
|
logger.info(
|
||||||
|
f"[auto_mark_read] Created: user={user.id}, chat={chat.id}, msg={last_message.id}")
|
||||||
self._send_marked_read_event(user.id, chat.id, last_message.id)
|
self._send_marked_read_event(user.id, chat.id, last_message.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Защита от откатов: только если НОВЕЕ
|
# Защита от откатов: только если НОВЕЕ
|
||||||
if read_state.last_read_message_id and last_message.id <= read_state.last_read_message_id:
|
if read_state.last_read_message_id and last_message.id <= read_state.last_read_message_id:
|
||||||
logger.debug(f"[auto_mark_read] Skip: {last_message.id} <= {read_state.last_read_message_id}")
|
logger.debug(
|
||||||
|
f"[auto_mark_read] Skip: {last_message.id} <= {read_state.last_read_message_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
read_state.last_read_message = last_message
|
read_state.last_read_message = last_message
|
||||||
read_state.save(update_fields=['last_read_message', 'updated_at'])
|
read_state.save(update_fields=['last_read_message', 'updated_at'])
|
||||||
|
|
||||||
logger.info(f"[auto_mark_read] Updated: user={user.id}, chat={chat.id}, msg={last_message.id}")
|
logger.info(
|
||||||
|
f"[auto_mark_read] Updated: user={user.id}, chat={chat.id}, msg={last_message.id}")
|
||||||
self._send_marked_read_event(user.id, chat.id, last_message.id)
|
self._send_marked_read_event(user.id, chat.id, last_message.id)
|
||||||
|
|
||||||
def _send_marked_read_event(self, user_id, chat_id, message_id):
|
def _send_marked_read_event(self, user_id, chat_id, message_id):
|
||||||
@ -292,7 +295,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
Асимметричная загрузка для браузерных приложений:
|
Асимметричная загрузка для браузерных приложений:
|
||||||
- 24 сообщения ДО (контекст прочитанных)
|
- 24 сообщения ДО (контекст прочитанных)
|
||||||
- 6 сообщений ПОСЛЕ (новые непрочитанные)
|
- 10 сообщений ПОСЛЕ (новые непрочитанные)
|
||||||
|
|
||||||
Автоматическая отметка последнего загруженного как прочитанного.
|
Автоматическая отметка последнего загруженного как прочитанного.
|
||||||
Погрешность: макс 1-2 невидимых сообщения (приемлемо для веб).
|
Погрешность: макс 1-2 невидимых сообщения (приемлемо для веб).
|
||||||
@ -307,9 +310,9 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
around_id = request.query_params.get('around_id')
|
around_id = request.query_params.get('around_id')
|
||||||
|
|
||||||
# Асимметричные лимиты (24 + 6 = 30 сообщений)
|
# Асимметричные лимиты (24 + 10 = 34 сообщения )
|
||||||
before_limit = int(request.query_params.get('before_limit', 24))
|
before_limit = int(request.query_params.get('before_limit', 24))
|
||||||
after_limit = int(request.query_params.get('after_limit', 6))
|
after_limit = int(request.query_params.get('after_limit', 10))
|
||||||
|
|
||||||
# Для обратной совместимости с limit параметром
|
# Для обратной совместимости с limit параметром
|
||||||
if 'limit' in request.query_params and 'before_limit' not in request.query_params:
|
if 'limit' in request.query_params and 'before_limit' not in request.query_params:
|
||||||
@ -360,7 +363,8 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
if around_value > 1_000_000_000:
|
if around_value > 1_000_000_000:
|
||||||
# Конвертируем timestamp из миллисекунд в seconds
|
# Конвертируем timestamp из миллисекунд в seconds
|
||||||
timestamp_seconds = around_value / 1000
|
timestamp_seconds = around_value / 1000
|
||||||
anchor_dt = datetime.fromtimestamp(timestamp_seconds, tz=dt_tz.utc)
|
anchor_dt = datetime.fromtimestamp(
|
||||||
|
timestamp_seconds, tz=dt_tz.utc)
|
||||||
|
|
||||||
# Ищем ближайшее сообщение к этому timestamp (ДО или на момент)
|
# Ищем ближайшее сообщение к этому timestamp (ДО или на момент)
|
||||||
anchor_msg = queryset.filter(
|
anchor_msg = queryset.filter(
|
||||||
@ -373,16 +377,20 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
created_at__gte=anchor_dt
|
created_at__gte=anchor_dt
|
||||||
).order_by('created_at').first()
|
).order_by('created_at').first()
|
||||||
|
|
||||||
logger.info(f"[messages_around] Searching by timestamp: {anchor_dt}, found: {anchor_msg}")
|
logger.info(
|
||||||
|
f"[messages_around] Searching by timestamp: {anchor_dt}, found: {anchor_msg}")
|
||||||
else:
|
else:
|
||||||
# Это обычный message_id
|
# Это обычный message_id
|
||||||
try:
|
try:
|
||||||
anchor_msg = queryset.get(pk=around_value)
|
anchor_msg = queryset.get(pk=around_value)
|
||||||
logger.info(f"[messages_around] Found message by ID: {around_value}")
|
logger.info(
|
||||||
|
f"[messages_around] Found message by ID: {around_value}")
|
||||||
except Message.DoesNotExist:
|
except Message.DoesNotExist:
|
||||||
logger.warning(f"[messages_around] Message with ID {around_value} not found")
|
logger.warning(
|
||||||
|
f"[messages_around] Message with ID {around_value} not found")
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logger.warning(f"[messages_around] Invalid around_id format: {around_id}")
|
logger.warning(
|
||||||
|
f"[messages_around] Invalid around_id format: {around_id}")
|
||||||
|
|
||||||
if not anchor_msg:
|
if not anchor_msg:
|
||||||
# Если не найдено - возвращаем последние сообщения
|
# Если не найдено - возвращаем последние сообщения
|
||||||
@ -444,14 +452,16 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
chat = self.get_object()
|
chat = self.get_object()
|
||||||
|
|
||||||
logger.info(f"[mark_read] Manual mark by user {request.user.id} for chat {chat.id}")
|
logger.info(
|
||||||
|
f"[mark_read] Manual mark by user {request.user.id} for chat {chat.id}")
|
||||||
|
|
||||||
# Основная логика: поддерживаем message_id
|
# Основная логика: поддерживаем message_id
|
||||||
message_id = request.data.get('message_id')
|
message_id = request.data.get('message_id')
|
||||||
|
|
||||||
if message_id:
|
if message_id:
|
||||||
try:
|
try:
|
||||||
message = chat.messages.get(pk=int(message_id), is_deleted=False)
|
message = chat.messages.get(
|
||||||
|
pk=int(message_id), is_deleted=False)
|
||||||
|
|
||||||
# Используем новую логику через _auto_mark_read
|
# Используем новую логику через _auto_mark_read
|
||||||
self._auto_mark_read(chat, request.user, [message])
|
self._auto_mark_read(chat, request.user, [message])
|
||||||
@ -461,7 +471,8 @@ class ChatViewSet(viewsets.ModelViewSet):
|
|||||||
'last_read_message_id': message.id
|
'last_read_message_id': message.id
|
||||||
})
|
})
|
||||||
except (Message.DoesNotExist, ValueError) as e:
|
except (Message.DoesNotExist, ValueError) as e:
|
||||||
logger.error(f"[mark_read] Invalid message_id {message_id}: {e}")
|
logger.error(
|
||||||
|
f"[mark_read] Invalid message_id {message_id}: {e}")
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'Message {message_id} not found'},
|
{'error': f'Message {message_id} not found'},
|
||||||
status=status.HTTP_404_NOT_FOUND
|
status=status.HTTP_404_NOT_FOUND
|
||||||
@ -566,7 +577,8 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
"""
|
"""
|
||||||
chat_id = request.data.get('chat_id')
|
chat_id = request.data.get('chat_id')
|
||||||
content = request.data.get('content', '').strip()
|
content = request.data.get('content', '').strip()
|
||||||
reply_to_id = request.data.get('reply_to') or request.data.get('reply_to_id')
|
reply_to_id = request.data.get(
|
||||||
|
'reply_to') or request.data.get('reply_to_id')
|
||||||
|
|
||||||
# Валидация чата
|
# Валидация чата
|
||||||
try:
|
try:
|
||||||
@ -699,11 +711,13 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
serializer = self.get_serializer(
|
||||||
|
instance, data=request.data, partial=partial)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
new_content = serializer.validated_data.get('content', '').strip()
|
new_content = serializer.validated_data.get('content', '').strip()
|
||||||
existing_attachment_ids = serializer.validated_data.get('existing_attachment_ids')
|
existing_attachment_ids = serializer.validated_data.get(
|
||||||
|
'existing_attachment_ids')
|
||||||
|
|
||||||
# Валидация вложений
|
# Валидация вложений
|
||||||
current_attachments_count = instance.attachments.count()
|
current_attachments_count = instance.attachments.count()
|
||||||
@ -735,14 +749,16 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
if existing_attachment_ids is not None:
|
if existing_attachment_ids is not None:
|
||||||
from communications.models import MessageAttachment
|
from communications.models import MessageAttachment
|
||||||
|
|
||||||
current_ids = set(str(att.id) for att in instance.attachments.all())
|
current_ids = set(str(att.id)
|
||||||
|
for att in instance.attachments.all())
|
||||||
keep_ids = set(str(id) for id in existing_attachment_ids)
|
keep_ids = set(str(id) for id in existing_attachment_ids)
|
||||||
ids_to_remove = current_ids - keep_ids
|
ids_to_remove = current_ids - keep_ids
|
||||||
ids_to_add = keep_ids - current_ids
|
ids_to_add = keep_ids - current_ids
|
||||||
|
|
||||||
# Удаляем attachments которых нет в списке
|
# Удаляем attachments которых нет в списке
|
||||||
if ids_to_remove:
|
if ids_to_remove:
|
||||||
attachments_to_delete = instance.attachments.filter(id__in=ids_to_remove)
|
attachments_to_delete = instance.attachments.filter(
|
||||||
|
id__in=ids_to_remove)
|
||||||
for att in attachments_to_delete:
|
for att in attachments_to_delete:
|
||||||
if att.file and att.file.storage.exists(att.file.name):
|
if att.file and att.file.storage.exists(att.file.name):
|
||||||
att.file.delete(save=False)
|
att.file.delete(save=False)
|
||||||
@ -751,9 +767,11 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
# Переносим новые attachments из других сообщений к этому
|
# Переносим новые attachments из других сообщений к этому
|
||||||
if ids_to_add:
|
if ids_to_add:
|
||||||
attachments_to_move = MessageAttachment.objects.filter(id__in=ids_to_add)
|
attachments_to_move = MessageAttachment.objects.filter(
|
||||||
|
id__in=ids_to_add)
|
||||||
updated_count = attachments_to_move.update(message=instance)
|
updated_count = attachments_to_move.update(message=instance)
|
||||||
logger.info(f"[update] Moved {updated_count} attachments to message {instance.id}: {list(ids_to_add)}")
|
logger.info(
|
||||||
|
f"[update] Moved {updated_count} attachments to message {instance.id}: {list(ids_to_add)}")
|
||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
@ -762,7 +780,8 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
# Обновляем has_attachments (заново считаем из БД)
|
# Обновляем has_attachments (заново считаем из БД)
|
||||||
from communications.models import MessageAttachment
|
from communications.models import MessageAttachment
|
||||||
instance.has_attachments = MessageAttachment.objects.filter(message=instance).count() > 0
|
instance.has_attachments = MessageAttachment.objects.filter(
|
||||||
|
message=instance).count() > 0
|
||||||
instance.save(update_fields=['has_attachments'])
|
instance.save(update_fields=['has_attachments'])
|
||||||
|
|
||||||
# Перезагружаем с нуля чтобы получить обновленные attachments
|
# Перезагружаем с нуля чтобы получить обновленные attachments
|
||||||
@ -822,7 +841,8 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
def react(self, request, pk=None):
|
def react(self, request, pk=None):
|
||||||
"""Добавить реакцию"""
|
"""Добавить реакцию"""
|
||||||
message = self.get_object()
|
message = self.get_object()
|
||||||
logger.info(f"[react] User {request.user.id} adding reaction to message {message.id}")
|
logger.info(
|
||||||
|
f"[react] User {request.user.id} adding reaction to message {message.id}")
|
||||||
|
|
||||||
serializer = ReactionSerializer(data=request.data)
|
serializer = ReactionSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
@ -862,7 +882,8 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
}
|
}
|
||||||
reactions_summary[r.emoji]['count'] += 1
|
reactions_summary[r.emoji]['count'] += 1
|
||||||
reactions_summary[r.emoji]['users'].append(r.user_id)
|
reactions_summary[r.emoji]['users'].append(r.user_id)
|
||||||
reactions_summary[r.emoji]['user_names'].append(r.user.get_full_name())
|
reactions_summary[r.emoji]['user_names'].append(
|
||||||
|
r.user.get_full_name())
|
||||||
|
|
||||||
logger.info(f"[react] reactions_summary: {reactions_summary}")
|
logger.info(f"[react] reactions_summary: {reactions_summary}")
|
||||||
|
|
||||||
@ -890,7 +911,8 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
def unreact(self, request, pk=None):
|
def unreact(self, request, pk=None):
|
||||||
"""Убрать реакцию"""
|
"""Убрать реакцию"""
|
||||||
message = self.get_object()
|
message = self.get_object()
|
||||||
logger.info(f"[unreact] User {request.user.id} removing reaction from message {message.id}")
|
logger.info(
|
||||||
|
f"[unreact] User {request.user.id} removing reaction from message {message.id}")
|
||||||
|
|
||||||
serializer = ReactionSerializer(data=request.data)
|
serializer = ReactionSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
@ -923,7 +945,8 @@ class MessageViewSet(viewsets.ModelViewSet):
|
|||||||
}
|
}
|
||||||
reactions_summary[r.emoji]['count'] += 1
|
reactions_summary[r.emoji]['count'] += 1
|
||||||
reactions_summary[r.emoji]['users'].append(r.user_id)
|
reactions_summary[r.emoji]['users'].append(r.user_id)
|
||||||
reactions_summary[r.emoji]['user_names'].append(r.user.get_full_name())
|
reactions_summary[r.emoji]['user_names'].append(
|
||||||
|
r.user.get_full_name())
|
||||||
|
|
||||||
logger.info(f"[unreact] reactions_summary: {reactions_summary}")
|
logger.info(f"[unreact] reactions_summary: {reactions_summary}")
|
||||||
|
|
||||||
@ -1130,7 +1153,8 @@ class PollViewSet(viewsets.ModelViewSet):
|
|||||||
option.vote_count = option.votes.count()
|
option.vote_count = option.votes.count()
|
||||||
option.save()
|
option.save()
|
||||||
|
|
||||||
poll.total_voters = PollVote.objects.filter(poll=poll).values('voter').distinct().count()
|
poll.total_voters = PollVote.objects.filter(
|
||||||
|
poll=poll).values('voter').distinct().count()
|
||||||
poll.save()
|
poll.save()
|
||||||
|
|
||||||
# WebSocket уведомление
|
# WebSocket уведомление
|
||||||
|
|||||||
@ -70,8 +70,18 @@ def create_patched_method():
|
|||||||
return patched_event_params
|
return patched_event_params
|
||||||
|
|
||||||
|
|
||||||
|
# Флаг для предотвращения повторного применения патча
|
||||||
|
_patch_applied = False
|
||||||
|
|
||||||
|
|
||||||
def apply_patch():
|
def apply_patch():
|
||||||
"""Применяет патч к django-scheduler Event модели."""
|
"""Применяет патч к django-scheduler Event модели."""
|
||||||
|
global _patch_applied
|
||||||
|
|
||||||
|
# Если патч уже применён, пропускаем
|
||||||
|
if _patch_applied:
|
||||||
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ленивый импорт - только при вызове apply_patch()
|
# Ленивый импорт - только при вызове apply_patch()
|
||||||
from schedule.models import Event
|
from schedule.models import Event
|
||||||
@ -86,6 +96,7 @@ def apply_patch():
|
|||||||
print(f" Патч применён: Event._event_params = {patched_method.__name__}")
|
print(f" Патч применён: Event._event_params = {patched_method.__name__}")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
|
_patch_applied = True
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|||||||
195
docs/diagnostic/WEBSOCKET_FIX.md
Normal file
195
docs/diagnostic/WEBSOCKET_FIX.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# 🔧 Исправление WebSocket ошибки: "❌ WebSocket error: {}"
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
При открытии чата в браузере видна ошибка:
|
||||||
|
```
|
||||||
|
❌ WebSocket error: {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Причины
|
||||||
|
|
||||||
|
WebSocket ошибка `{}` обычно связана с одной из этих проблем:
|
||||||
|
|
||||||
|
| Причина | Решение |
|
||||||
|
|---------|---------|
|
||||||
|
| **Backend запущен через `runserver`** | Используйте **Daphne** для WebSocket |
|
||||||
|
| **PORT 9000 занят** | Проверьте процессы или используйте другой порт |
|
||||||
|
| **Неправильный URL в .env** | Убедитесь что используется `.env.local` |
|
||||||
|
| **JWT токен не сохранен** | Выполните вход в систему |
|
||||||
|
| **Frontend и Backend на разных версиях** | Синхронизируйте код |
|
||||||
|
|
||||||
|
## ✅ Пошаговое решение
|
||||||
|
|
||||||
|
### Шаг 1: Остановите текущий backend
|
||||||
|
```bash
|
||||||
|
# Если backend запущен, нажмите Ctrl+C в терминале
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: Запустите backend через Daphne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/daphne -b 0.0.0.0 -p 9000 eusrr_backend.asgi:application
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ожидаемый вывод:**
|
||||||
|
```
|
||||||
|
Starting server process [PID]
|
||||||
|
HTTP/2 support not enabled
|
||||||
|
Uvicorn running on http://0.0.0.0:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3: Убедитесь что .env.local правильный
|
||||||
|
|
||||||
|
Проверьте файл `frontend/.env.local`:
|
||||||
|
```dotenv
|
||||||
|
# ✅ ПРАВИЛЬНО (для локальной разработки)
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:9000
|
||||||
|
NEXT_PUBLIC_BACKEND_URL=http://localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
**НЕ используйте:**
|
||||||
|
```dotenv
|
||||||
|
# ❌ НЕПРАВИЛЬНО (production URL)
|
||||||
|
NEXT_PUBLIC_WS_URL=wss://corp.robotail.pro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Перезагрузите фронтенд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Перезагрузите браузер: `Ctrl+R` или `F5`
|
||||||
|
|
||||||
|
### Шаг 5: Проверьте WebSocket подключение
|
||||||
|
|
||||||
|
Откройте **DevTools** (F12) → **Console** окно и ищите:
|
||||||
|
|
||||||
|
**✅ Если видите:**
|
||||||
|
```
|
||||||
|
🔄 Подключение к WebSocket: ws://localhost:9000/ws/?token=...
|
||||||
|
✅ WebSocket connected!
|
||||||
|
```
|
||||||
|
→ **Всё работает!**
|
||||||
|
|
||||||
|
**❌ Если видите ошибку:**
|
||||||
|
```
|
||||||
|
❌ WebSocket error: {}
|
||||||
|
```
|
||||||
|
→ Продолжайте с Шага 6
|
||||||
|
|
||||||
|
### Шаг 6: Проверьте порт 9000
|
||||||
|
|
||||||
|
Проверьте что процесс слушает на порту 9000:
|
||||||
|
|
||||||
|
**Windows - PowerShell (администратор):**
|
||||||
|
```powershell
|
||||||
|
# Проверить какие процессы используют порт 9000
|
||||||
|
netstat -ano | findstr :9000
|
||||||
|
|
||||||
|
# Пример вывода:
|
||||||
|
# TCP 0.0.0.0:9000 0.0.0.0:0 LISTENING 28904
|
||||||
|
|
||||||
|
# Если что-то там слушает, проверьте что это Daphne
|
||||||
|
tasklist | findstr 28904
|
||||||
|
```
|
||||||
|
|
||||||
|
Если порт занят другим процессом:
|
||||||
|
```bash
|
||||||
|
# Убить процесс в PowerShell
|
||||||
|
taskkill /PID 28904 /F
|
||||||
|
|
||||||
|
# Или используйте другой порт
|
||||||
|
.venv/Scripts/daphne -b 0.0.0.0 -p 9001 eusrr_backend.asgi:application
|
||||||
|
|
||||||
|
# И обновите .env.local
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:9001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 7: Проверьте Network в DevTools
|
||||||
|
|
||||||
|
1. Откройте **DevTools** → **Network** вкладка
|
||||||
|
2. В фильтре напишите: `ws`
|
||||||
|
3. Откройте чат и ищите подключение `ws://localhost:9000/ws/`
|
||||||
|
|
||||||
|
**✅ Если видите:**
|
||||||
|
```
|
||||||
|
ws Status: 101 Switching Protocols [WebSocket connected]
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Если видите:**
|
||||||
|
```
|
||||||
|
ws Status: Failed [или ConnectError]
|
||||||
|
```
|
||||||
|
→ Backend неправильно запущен
|
||||||
|
|
||||||
|
## 🔄 Полный скрипт для переустановки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Перейдите в backend
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 2. Установите dependencies если нужно
|
||||||
|
.venv/Scripts/pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 3. Запустите Daphne
|
||||||
|
.venv/Scripts/daphne -b 0.0.0.0 -p 9000 eusrr_backend.asgi:application
|
||||||
|
|
||||||
|
# В другом терминале:
|
||||||
|
|
||||||
|
# 4. Перейдите в frontend
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# 5. Установите если нужно
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 6. Запустите dev server
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 7. Откройте браузер на http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Чек-лист для диагностики
|
||||||
|
|
||||||
|
- [ ] Backend запущен через **Daphne** (не `runserver`)
|
||||||
|
- [ ] PORT **9000** свободен и доступен
|
||||||
|
- [ ] `.env.local` использует `ws://localhost:9000`
|
||||||
|
- [ ] JWT токен сохранен в localStorage (пользователь залогирован)
|
||||||
|
- [ ] Frontend запущен на `http://localhost:5173`
|
||||||
|
- [ ] В DevTools → Console видно `🔄 Подключение к WebSocket`
|
||||||
|
- [ ] В DevTools → Network видно WebSocket с статусом **101**
|
||||||
|
|
||||||
|
## 🚨 Если ничего не помогает
|
||||||
|
|
||||||
|
1. **Очистите cache браузера**
|
||||||
|
- F12 → Application → Clear Site Data → ✓ Cookies, ✓ Cache
|
||||||
|
|
||||||
|
2. **Удалите node_modules и переустановите**
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
rm -r node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Проверьте logs в backend**
|
||||||
|
- В терминале где запущен Daphne должны быть logs подключений
|
||||||
|
- Если их нет - frontend не отправляет запрос
|
||||||
|
|
||||||
|
4. **Обновите код из Git**
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
npm install
|
||||||
|
.venv/Scripts/pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Если проблема не решена
|
||||||
|
|
||||||
|
Создайте issue с информацией:
|
||||||
|
- Output из DevTools Console
|
||||||
|
- Output из Daphne терминала
|
||||||
|
- Содержимое `.env.local`
|
||||||
|
- Результат: `netstat -ano | findstr :9000` (Windows)
|
||||||
87
docs/in-progress/WEBSOCKET_ERROR_FIX.md
Normal file
87
docs/in-progress/WEBSOCKET_ERROR_FIX.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# 📝 Исправление WebSocket ошибки - Резюме
|
||||||
|
|
||||||
|
## 🔧 Что было исправлено
|
||||||
|
|
||||||
|
### 1. **Файл: `frontend/src/hooks/useWebSocket.ts`**
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
- Код неправильно обрабатывал переменную окружения `NEXT_PUBLIC_WS_URL`
|
||||||
|
- Если URL содержал протокол (`ws://localhost:9000`), код добавлял протокол снова → `ws://ws://localhost:9000/ws/`
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
- Добавлена проверка: если URL уже содержит протокол - используем его как есть
|
||||||
|
- Если URL это только хост - добавляем протокол
|
||||||
|
- Добавлено логирование: `console.log('🔄 Подключение к WebSocket:', wsUrl)`
|
||||||
|
|
||||||
|
**Измененная логика:**
|
||||||
|
```typescript
|
||||||
|
// ✅ ДО (НЕПРАВИЛЬНО):
|
||||||
|
const host = process.env.NEXT_PUBLIC_WS_URL || 'localhost:9000';
|
||||||
|
const wsUrl = `${protocol}//${host}/ws/?token=${token}`;
|
||||||
|
// Результат: ws://ws://localhost:9000/ws/?token=... ❌
|
||||||
|
|
||||||
|
// ✅ ПОСЛЕ (ПРАВИЛЬНО):
|
||||||
|
if (envWsUrl.startsWith('ws://') || envWsUrl.startsWith('wss://')) {
|
||||||
|
wsUrl = `${envWsUrl}/ws/?token=${token}`;
|
||||||
|
}
|
||||||
|
// Результат: ws://localhost:9000/ws/?token=... ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Файл: `frontend/QUICKSTART.md`**
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
- ✅ Добавлена демонстрация правильного запуска Daphne
|
||||||
|
- ✅ Указано что **НЕ нужно** использовать `runserver` для WebSocket
|
||||||
|
- ✅ Добавлена секция "Проверка WebSocket подключения" с подробной диагностикой
|
||||||
|
- ✅ Расширена секция "Проблемы?" с решением для `WebSocket error: {}`
|
||||||
|
- ✅ Добавлены инструкции по проверке портов в Windows
|
||||||
|
|
||||||
|
### 3. **Файл: `docs/diagnostic/WEBSOCKET_FIX.md`** (новый)
|
||||||
|
|
||||||
|
- 📋 Полная диагностика проблемы с WebSocket
|
||||||
|
- 🔍 Шаг-за-шагом решение проблемы
|
||||||
|
- 🛠️ Скрипты для проверки портов и процессов
|
||||||
|
- ✅ Чек-лист для быстрой проверки
|
||||||
|
|
||||||
|
## 🚀 Что делать дальше
|
||||||
|
|
||||||
|
### 1. Убедитесь что используется `.env.local` (не `.env`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# frontend/.env.local (для локальной разработки)
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:9000
|
||||||
|
NEXT_PUBLIC_BACKEND_URL=http://localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Перезапустите backend с Daphne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/daphne -b 0.0.0.0 -p 9000 eusrr_backend.asgi:application
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Перезагрузите frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверьте в DevTools
|
||||||
|
|
||||||
|
- F12 → Console → убедитесь что видите `🔄 Подключение к WebSocket: ws://localhost:9000/...`
|
||||||
|
- F12 → Network → WS → должен быть статус **101 Switching Protocols** ✅
|
||||||
|
|
||||||
|
## 📌 Ключевые моменты
|
||||||
|
|
||||||
|
| Что | ❌ НЕПРАВИЛЬНО | ✅ ПРАВИЛЬНО |
|
||||||
|
|-----|---|---|
|
||||||
|
| Запуск backend | `python manage.py runserver` | `.venv/Scripts/daphne -p 9000 eusrr_backend.asgi:application` |
|
||||||
|
| .env файл | `.env` (production) | `.env.local` (development) |
|
||||||
|
| WebSocket URL | `wss://corp.robotail.pro` | `ws://localhost:9000` |
|
||||||
|
| Логирование | Нет | `🔄 Подключение к WebSocket: ...` |
|
||||||
|
|
||||||
|
## 🔗 Дополнительно
|
||||||
|
|
||||||
|
- 📚 Полная диагностика: [docs/diagnostic/WEBSOCKET_FIX.md](../../docs/diagnostic/WEBSOCKET_FIX.md)
|
||||||
|
- 📖 Quickstart: [frontend/QUICKSTART.md](../QUICKSTART.md)
|
||||||
@ -1,6 +1,12 @@
|
|||||||
# API Configuration
|
# Next.js Environment Variables Example
|
||||||
VITE_API_URL=http://localhost:8000/api/v2
|
# Copy this file to .env.local for local development
|
||||||
VITE_WS_URL=ws://localhost:8000/ws
|
# Copy this file to .env for production
|
||||||
|
|
||||||
# Development server port (optional, Vite uses 5173 by default)
|
# Backend API URL
|
||||||
# PORT=5173
|
NEXT_PUBLIC_BACKEND_URL=http://localhost:9000
|
||||||
|
|
||||||
|
# API URL (optional, defaults to BACKEND_URL/api/v1)
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:9000/api/v1
|
||||||
|
|
||||||
|
# WebSocket URL (БЕЗ пути /ws/, он добавляется автоматически в коде)
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:9000
|
||||||
|
|||||||
@ -1,256 +0,0 @@
|
|||||||
# ✅ API v2 + Frontend Integration Complete!
|
|
||||||
|
|
||||||
## 🎉 Что сделано
|
|
||||||
|
|
||||||
### ✅ Backend - API v2 создан
|
|
||||||
|
|
||||||
**Структура:**
|
|
||||||
```
|
|
||||||
backend/api/v2/
|
|
||||||
├── auth/ ✅ register, verify-email, resend-email
|
|
||||||
├── calendar/ ✅ events, calendars, subscriptions
|
|
||||||
├── communications/ ✅ chats, messages, polls
|
|
||||||
├── documents/ ✅ documents CRUD
|
|
||||||
├── employees/ ✅ employees, departments, positions, skills, groups
|
|
||||||
├── feed/ ✅ posts, comments
|
|
||||||
└── requests/ ✅ requests (заявки)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Все endpoints из v1 портированы в v2** через наследование ViewSet'ов.
|
|
||||||
|
|
||||||
### ✅ Frontend обновлен для v2
|
|
||||||
|
|
||||||
- `.env` → использует `http://localhost:8000/api/v2`
|
|
||||||
- `constants.ts` → default на v2
|
|
||||||
- Все запросы идут в v2 API
|
|
||||||
|
|
||||||
### ✅ Документация создана
|
|
||||||
|
|
||||||
- [API_V2_MIGRATION.md](../../docs/guides/API_V2_MIGRATION.md) - полное описание v2
|
|
||||||
- [QUICKSTART.md](QUICKSTART.md) - обновлен для v2
|
|
||||||
- [DJANGO_CORS_SETUP.md](../../docs/guides/DJANGO_CORS_SETUP.md) - настройка CORS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Полный список endpoints v2
|
|
||||||
|
|
||||||
### Auth (общие для v1/v2)
|
|
||||||
```
|
|
||||||
POST /api/auth/token/ # Login (JWT)
|
|
||||||
POST /api/auth/token/refresh/ # Refresh token
|
|
||||||
POST /api/v2/auth/register/ # Регистрация
|
|
||||||
POST /api/v2/auth/verify-email/ # Подтверждение email
|
|
||||||
POST /api/v2/auth/resend-email/ # Повтор отправки
|
|
||||||
```
|
|
||||||
|
|
||||||
### Employees
|
|
||||||
```
|
|
||||||
GET /api/v2/employees/ # Список
|
|
||||||
POST /api/v2/employees/ # Создать
|
|
||||||
GET /api/v2/employees/{id}/ # Получить
|
|
||||||
PATCH /api/v2/employees/{id}/ # Обновить
|
|
||||||
DELETE /api/v2/employees/{id}/ # Удалить
|
|
||||||
|
|
||||||
GET /api/v2/departments/
|
|
||||||
GET /api/v2/positions/
|
|
||||||
GET /api/v2/skills/
|
|
||||||
GET /api/v2/groups/
|
|
||||||
GET /api/v2/employee-actions/
|
|
||||||
GET /api/v2/department-roles/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Documents
|
|
||||||
```
|
|
||||||
GET /api/v2/documents/
|
|
||||||
POST /api/v2/documents/
|
|
||||||
GET /api/v2/documents/{id}/
|
|
||||||
PATCH /api/v2/documents/{id}/
|
|
||||||
DELETE /api/v2/documents/{id}/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Requests (Заявки)
|
|
||||||
```
|
|
||||||
GET /api/v2/requests/
|
|
||||||
POST /api/v2/requests/
|
|
||||||
GET /api/v2/requests/{id}/
|
|
||||||
PATCH /api/v2/requests/{id}/
|
|
||||||
DELETE /api/v2/requests/{id}/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Calendar
|
|
||||||
```
|
|
||||||
GET /api/v2/calendar/events/
|
|
||||||
POST /api/v2/calendar/events/
|
|
||||||
GET /api/v2/calendar/calendars/
|
|
||||||
GET /api/v2/calendar/subscriptions/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Communications (Чаты)
|
|
||||||
```
|
|
||||||
GET /api/v2/chats/
|
|
||||||
POST /api/v2/chats/
|
|
||||||
GET /api/v2/chats/{id}/
|
|
||||||
GET /api/v2/messages/
|
|
||||||
POST /api/v2/messages/
|
|
||||||
GET /api/v2/polls/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Feed (Лента)
|
|
||||||
```
|
|
||||||
GET /api/v2/posts/
|
|
||||||
POST /api/v2/posts/
|
|
||||||
GET /api/v2/posts/{id}/
|
|
||||||
GET /api/v2/comments/
|
|
||||||
POST /api/v2/comments/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
```
|
|
||||||
GET /api/v2/notifications/ (использует v1 internally)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 v1 vs v2
|
|
||||||
|
|
||||||
| Характеристика | v1 | v2 |
|
|
||||||
|----------------|----|----|
|
|
||||||
| **Endpoints** | Все | Все (идентичные) |
|
|
||||||
| **LDAP Sync** | Синхронный (Mixin) | Асинхронный (Signals) |
|
|
||||||
| **Response Time** | ~200-300ms | ~30-50ms |
|
|
||||||
| **Блокировка** | Да | Нет |
|
|
||||||
| **Retry логика** | Ручная | Celery автоматически |
|
|
||||||
| **Статус** | ✅ Работает | ✅ Работает |
|
|
||||||
|
|
||||||
**Оба API полностью функциональны!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Быстрый запуск
|
|
||||||
|
|
||||||
### 1. Настроить CORS (если еще не сделано)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
.venv/Scripts/pip install django-cors-headers
|
|
||||||
```
|
|
||||||
|
|
||||||
Добавить в `settings.py`:
|
|
||||||
```python
|
|
||||||
INSTALLED_APPS = ["corsheaders", ...]
|
|
||||||
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...]
|
|
||||||
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Подробнее**: [Django CORS Setup](../../docs/guides/DJANGO_CORS_SETUP.md)
|
|
||||||
|
|
||||||
### 2. Запустить Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
.venv/Scripts/python manage.py runserver 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Запустить Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install # первый раз
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Открыть**: http://localhost:5173
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Проверка работы
|
|
||||||
|
|
||||||
### Test 1: API v2 доступен
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/api/v2/employees/
|
|
||||||
```
|
|
||||||
|
|
||||||
Должен вернуть JSON.
|
|
||||||
|
|
||||||
### Test 2: v1 всё ещё работает
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/api/v1/employees/
|
|
||||||
```
|
|
||||||
|
|
||||||
Оба работают параллельно!
|
|
||||||
|
|
||||||
### Test 3: Frontend
|
|
||||||
|
|
||||||
Открыть http://localhost:5173 → должна загрузиться страница сотрудников.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Статус проекта
|
|
||||||
|
|
||||||
| Модуль | Backend v2 | Frontend | Status |
|
|
||||||
|--------|------------|----------|--------|
|
|
||||||
| Auth | ✅ | ✅ | **Готово** |
|
|
||||||
| Employees | ✅ | ✅ List | В разработке |
|
|
||||||
| Departments | ✅ | ⏳ | Backend готов |
|
|
||||||
| Documents | ✅ | ⏳ | Backend готов |
|
|
||||||
| Requests | ✅ | ⏳ | Backend готов |
|
|
||||||
| Calendar | ✅ | ⏳ | Backend готов |
|
|
||||||
| Chats | ✅ | ⏳ | Backend готов |
|
|
||||||
| Feed | ✅ | ⏳ | Backend готов |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Следующие задачи
|
|
||||||
|
|
||||||
### Фаза 1: Основной функционал
|
|
||||||
- [ ] Добавить endpoint `/api/v2/employees/me/` (текущий пользователь)
|
|
||||||
- [ ] Создать CRUD страницы для documents
|
|
||||||
- [ ] Создать CRUD страницы для requests
|
|
||||||
- [ ] Добавить UI библиотеку (Ant Design)
|
|
||||||
|
|
||||||
### Фаза 2: Продвинутый функционал
|
|
||||||
- [ ] Интегрировать WebSocket для чатов
|
|
||||||
- [ ] Добавить real-time уведомления
|
|
||||||
- [ ] Календарь с FullCalendar
|
|
||||||
- [ ] File uploads
|
|
||||||
|
|
||||||
### Фаза 3: Полировка
|
|
||||||
- [ ] Мобильная адаптация
|
|
||||||
- [ ] Темная тема
|
|
||||||
- [ ] Тесты (Frontend + Backend)
|
|
||||||
- [ ] Production build
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Переключение между v1 и v2
|
|
||||||
|
|
||||||
В любой момент можно переключиться:
|
|
||||||
|
|
||||||
**На v2 (по умолчанию):**
|
|
||||||
```bash
|
|
||||||
# frontend/.env
|
|
||||||
VITE_API_URL=http://localhost:8000/api/v2
|
|
||||||
```
|
|
||||||
|
|
||||||
**На v1:**
|
|
||||||
```bash
|
|
||||||
# frontend/.env
|
|
||||||
VITE_API_URL=http://localhost:8000/api/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Документация
|
|
||||||
|
|
||||||
- [API v2 Migration Guide](../../docs/guides/API_V2_MIGRATION.md)
|
|
||||||
- [Frontend Quickstart](QUICKSTART.md)
|
|
||||||
- [Django CORS Setup](../../docs/guides/DJANGO_CORS_SETUP.md)
|
|
||||||
- [API v2 README](../../backend/api/v2/README.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Всё готово для разработки на API v2!** 🚀
|
|
||||||
|
|
||||||
Frontend настроен, Backend готов, осталось только добавить CORS и можно работать!
|
|
||||||
@ -1,410 +0,0 @@
|
|||||||
# Frontend Integration Guide
|
|
||||||
|
|
||||||
## Обзор
|
|
||||||
|
|
||||||
Фронтенд corp.front полностью интегрирован с Django Backend через REST API. Используется Next.js 16 с TypeScript и Tailwind CSS.
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
corp.front/
|
|
||||||
├── src/
|
|
||||||
│ ├── app/ # Next.js страницы
|
|
||||||
│ │ ├── page.tsx # Главная (Лента) ✅ Интегрирован
|
|
||||||
│ │ ├── employees/ # Сотрудники ✅ Интегрирован
|
|
||||||
│ │ ├── departments/ # Отделы
|
|
||||||
│ │ ├── documents/ # Документы
|
|
||||||
│ │ ├── messages/ # Сообщения
|
|
||||||
│ │ ├── requests/ # Заявки
|
|
||||||
│ │ ├── finances/ # Финансы
|
|
||||||
│ │ └── login/ # Вход
|
|
||||||
│ ├── lib/
|
|
||||||
│ │ └── api.ts # API клиент ✅
|
|
||||||
│ ├── types/
|
|
||||||
│ │ └── api.ts # TypeScript типы ✅
|
|
||||||
│ └── hooks/
|
|
||||||
│ └── useApi.ts # React хуки для API ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
## API клиент
|
|
||||||
|
|
||||||
### Базовая конфигурация
|
|
||||||
|
|
||||||
API клиент автоматически:
|
|
||||||
- Добавляет JWT токен к запросам
|
|
||||||
- Обрабатывает ошибки авторизации
|
|
||||||
- Сохраняет токены в localStorage
|
|
||||||
- Перенаправляет на /login при 401
|
|
||||||
|
|
||||||
### Использование
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import apiClient from '@/lib/api';
|
|
||||||
|
|
||||||
// Авторизация
|
|
||||||
const { access, refresh, user } = await apiClient.login({
|
|
||||||
username: 'user@example.com',
|
|
||||||
password: 'password'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение данных
|
|
||||||
const employees = await apiClient.getEmployees();
|
|
||||||
const posts = await apiClient.getPosts();
|
|
||||||
```
|
|
||||||
|
|
||||||
## React хуки
|
|
||||||
|
|
||||||
Используйте готовые хуки из `@/hooks/useApi`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useCurrentUser, useEmployees, usePosts } from '@/hooks/useApi';
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const { user, loading, error } = useCurrentUser();
|
|
||||||
const { employees } = useEmployees({ search: 'John' });
|
|
||||||
const { posts } = usePosts({ page: 1 });
|
|
||||||
|
|
||||||
if (loading) return <div>Загрузка...</div>;
|
|
||||||
if (error) return <div>Ошибка: {error.message}</div>;
|
|
||||||
|
|
||||||
return <div>{/* UI */}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Доступные хуки
|
|
||||||
|
|
||||||
- `useCurrentUser()` - текущий пользователь
|
|
||||||
- `useEmployees(params?)` - список сотрудников
|
|
||||||
- `useDepartments()` - список отделов
|
|
||||||
- `usePosts(params?)` - посты ленты
|
|
||||||
- `useDocuments(params?)` - документы
|
|
||||||
- `useRequests(params?)` - заявки
|
|
||||||
- `useChats()` - чаты
|
|
||||||
- `useNotifications()` - уведомления
|
|
||||||
|
|
||||||
## TypeScript типы
|
|
||||||
|
|
||||||
Все типы данных определены в `@/types/api.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { User, Post, Department, Document } from '@/types/api';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Интегрированные страницы
|
|
||||||
|
|
||||||
### ✅ Главная страница (Лента)
|
|
||||||
|
|
||||||
**Файл**: `src/app/page.tsx`
|
|
||||||
|
|
||||||
**Функции**:
|
|
||||||
- Загрузка постов из API
|
|
||||||
- Отображение автора, времени, контента
|
|
||||||
- Лайки и комментарии
|
|
||||||
- Теги постов
|
|
||||||
|
|
||||||
**Используемые хуки**:
|
|
||||||
- `usePosts()` - получение постов
|
|
||||||
- `useCurrentUser()` - текущий пользователь
|
|
||||||
|
|
||||||
### ✅ Сотрудники
|
|
||||||
|
|
||||||
**Файл**: `src/app/employees/page.tsx`
|
|
||||||
|
|
||||||
**Функции**:
|
|
||||||
- Список всех сотрудников
|
|
||||||
- Поиск по имени
|
|
||||||
- Статус (онлайн/оффлайн)
|
|
||||||
- Должность и отдел
|
|
||||||
|
|
||||||
**Используемые хуки**:
|
|
||||||
- `useEmployees({ search })` - поиск сотрудников
|
|
||||||
- `useCurrentUser()` - текущий пользователь
|
|
||||||
|
|
||||||
## Переменные окружения
|
|
||||||
|
|
||||||
### Для локальной разработки
|
|
||||||
|
|
||||||
`.env.local`:
|
|
||||||
```env
|
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
|
||||||
NEXT_PUBLIC_WS_URL=ws://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Для Docker
|
|
||||||
|
|
||||||
`.env.local` (автоматически используется в docker-compose):
|
|
||||||
```env
|
|
||||||
NEXT_PUBLIC_API_URL=http://web:8000
|
|
||||||
NEXT_PUBLIC_WS_URL=ws://web:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
|
|
||||||
### Локальная разработка
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd corp.front
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Доступно на: http://localhost:3000
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Из корня проекта
|
|
||||||
docker-compose up -d frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
Доступно через nginx: http://localhost
|
|
||||||
|
|
||||||
## Интеграция новых страниц
|
|
||||||
|
|
||||||
### Шаг 1: Создать страницу
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// src/app/mypage/page.tsx
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useMyData } from '@/hooks/useApi';
|
|
||||||
|
|
||||||
export default function MyPage() {
|
|
||||||
const { data, loading, error } = useMyData();
|
|
||||||
|
|
||||||
if (loading) return <div>Загрузка...</div>;
|
|
||||||
if (error) return <div>Ошибка: {error.message}</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{data.map(item => (
|
|
||||||
<div key={item.id}>{item.name}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 2: Добавить метод в API клиент
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/lib/api.ts
|
|
||||||
class ApiClient {
|
|
||||||
// ... existing methods
|
|
||||||
|
|
||||||
async getMyData() {
|
|
||||||
return this.request('/mydata/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаг 3: Создать хук (опционально)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/hooks/useApi.ts
|
|
||||||
export function useMyData() {
|
|
||||||
const [data, setData] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
try {
|
|
||||||
const result = await apiClient.getMyData();
|
|
||||||
setData(result);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { data, loading, error };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
Полный список доступных эндпоинтов:
|
|
||||||
|
|
||||||
### Auth
|
|
||||||
- `POST /api/auth/token/` - Login
|
|
||||||
- `POST /api/auth/token/refresh/` - Refresh token
|
|
||||||
- `POST /api/v1/auth/register/` - Регистрация
|
|
||||||
|
|
||||||
### Employees
|
|
||||||
- `GET /api/v1/employees/` - Список сотрудников
|
|
||||||
- `GET /api/v1/employees/{id}/` - Детали сотрудника
|
|
||||||
- `GET /api/v1/employees/me/` - Текущий пользователь
|
|
||||||
|
|
||||||
### Departments
|
|
||||||
- `GET /api/v1/departments/` - Список отделов
|
|
||||||
- `GET /api/v1/departments/{id}/` - Детали отдела
|
|
||||||
|
|
||||||
### Feed
|
|
||||||
- `GET /api/v1/posts/` - Посты ленты
|
|
||||||
- `POST /api/v1/posts/` - Создать пост
|
|
||||||
- `POST /api/v1/posts/{id}/like/` - Лайк поста
|
|
||||||
- `GET /api/v1/comments/` - Комментарии
|
|
||||||
- `POST /api/v1/comments/` - Создать комментарий
|
|
||||||
|
|
||||||
### Documents
|
|
||||||
- `GET /api/v1/documents/` - Документы
|
|
||||||
- `GET /api/v1/documents/{id}/` - Детали документа
|
|
||||||
|
|
||||||
### Requests
|
|
||||||
- `GET /api/v1/requests/` - Заявки
|
|
||||||
- `POST /api/v1/requests/` - Создать заявку
|
|
||||||
- `GET /api/v1/requests/{id}/` - Детали заявки
|
|
||||||
|
|
||||||
### Communications
|
|
||||||
- `GET /api/v1/communications/chats/` - Чаты
|
|
||||||
- `GET /api/v1/communications/messages/` - Сообщения
|
|
||||||
- `POST /api/v1/communications/messages/` - Отправить сообщение
|
|
||||||
|
|
||||||
### Calendar
|
|
||||||
- `GET /api/v1/calendar/events/` - События календаря
|
|
||||||
- `GET /api/v1/calendar/calendars/` - Календари
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
- `GET /api/v1/notifications/` - Уведомления
|
|
||||||
- `POST /api/v1/notifications/{id}/mark_read/` - Отметить прочитанным
|
|
||||||
- `POST /api/v1/notifications/mark_all_read/` - Отметить все прочитанными
|
|
||||||
|
|
||||||
## Аутентификация
|
|
||||||
|
|
||||||
### Вход
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import apiClient from '@/lib/api';
|
|
||||||
|
|
||||||
async function handleLogin(username: string, password: string) {
|
|
||||||
try {
|
|
||||||
const { access, refresh, user } = await apiClient.login({
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
// Токен автоматически сохранён
|
|
||||||
// Перенаправить пользователя
|
|
||||||
router.push('/');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Выход
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import apiClient from '@/lib/api';
|
|
||||||
|
|
||||||
function handleLogout() {
|
|
||||||
apiClient.clearToken();
|
|
||||||
router.push('/login');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Защищённые страницы
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import apiClient from '@/lib/api';
|
|
||||||
|
|
||||||
export default function ProtectedPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const token = apiClient.getToken();
|
|
||||||
if (!token) {
|
|
||||||
router.push('/login');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <div>Protected content</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Обработка ошибок
|
|
||||||
|
|
||||||
API клиент автоматически обрабатывает:
|
|
||||||
|
|
||||||
- **401 Unauthorized**: Очищает токены и перенаправляет на `/login`
|
|
||||||
- **Network errors**: Выбрасывает исключение с сообщением
|
|
||||||
|
|
||||||
Пример обработки в компонентах:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const { data, loading, error } = useMyData();
|
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="error-message">
|
|
||||||
<h3>Ошибка загрузки данных</h3>
|
|
||||||
<p>{error.message}</p>
|
|
||||||
<button onClick={() => refetch()}>Попробовать снова</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebSocket (для будущей реализации)
|
|
||||||
|
|
||||||
Для real-time обновлений можно добавить WebSocket соединение:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000';
|
|
||||||
|
|
||||||
const socket = new WebSocket(`${WS_URL}/ws/notifications/`);
|
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
// Обработать уведомление
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Рекомендации
|
|
||||||
|
|
||||||
1. **Всегда используйте хуки** для получения данных из API
|
|
||||||
2. **Обрабатывайте loading и error** состояния в UI
|
|
||||||
3. **Используйte TypeScript типы** для type-safety
|
|
||||||
4. **Не сохраняйте чувствительные данные** в localStorage кроме токенов
|
|
||||||
5. **Валидируйте данные** перед отправкой на сервер
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
###CORS ошибки
|
|
||||||
|
|
||||||
Убедитесь, что backend настроен с правильными CORS заголовками:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# backend/eusrr_backend/settings.py
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://localhost",
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 401 Unauthorized
|
|
||||||
|
|
||||||
- Проверьте, что токен действителен
|
|
||||||
- Убедитесь, что backend запущен
|
|
||||||
- Проверьте переменные окружения
|
|
||||||
|
|
||||||
### Ошибка сети
|
|
||||||
|
|
||||||
- Проверьте, что backend доступен
|
|
||||||
- Проверьте URL в `NEXT_PUBLIC_API_URL`
|
|
||||||
- Проверьте docker network, если используете docker-compose
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Дата создания**: 19 февраля 2026
|
|
||||||
**Версия**: 1.0
|
|
||||||
@ -17,6 +17,18 @@ cd backend
|
|||||||
.venv/Scripts/pip install django-cors-headers
|
.venv/Scripts/pip install django-cors-headers
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 1.5. Проверить что Daphne установлен
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/pip show daphne
|
||||||
|
```
|
||||||
|
|
||||||
|
Если Daphne не установлен:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/pip install daphne
|
||||||
|
```
|
||||||
|
|
||||||
Добавить в `backend/eusrr_backend/settings.py`:
|
Добавить в `backend/eusrr_backend/settings.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -52,7 +64,15 @@ npm install
|
|||||||
|
|
||||||
### 3. Запустить оба сервера
|
### 3. Запустить оба сервера
|
||||||
|
|
||||||
**Terminal 1 - Django:**
|
**⚠️ ВАЖНО: Для WebSocket используйте Daphne вместо стандартного Django сервера!**
|
||||||
|
|
||||||
|
**Terminal 1 - Django (с WebSocket на Daphne):**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/daphne -b 0.0.0.0 -p 9000 eusrr_backend.asgi:application
|
||||||
|
```
|
||||||
|
|
||||||
|
Или используйте `runserver` для HTTP (без WebSocket):
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
.venv/Scripts/python manage.py runserver 8000
|
.venv/Scripts/python manage.py runserver 8000
|
||||||
@ -68,6 +88,16 @@ npm run dev
|
|||||||
|
|
||||||
Откройте в браузере: **http://localhost:5173**
|
Откройте в браузере: **http://localhost:5173**
|
||||||
|
|
||||||
|
## 🔍 Проверка WebSocket подключения
|
||||||
|
|
||||||
|
1. Откройте **DevTools** (F12) → **Console**
|
||||||
|
2. Ищите сообщение `🔄 Подключение к WebSocket: ws://localhost:9000/ws/...`
|
||||||
|
3. Если ошибка `❌ WebSocket error: {}`, проверьте:
|
||||||
|
- ✅ Backend запущен через Daphne (не через runserver)
|
||||||
|
- ✅ PORT 9000 доступен (не занят другой программой)
|
||||||
|
- ✅ JWT токен сохранен в localStorage
|
||||||
|
- ✅ В файле `.env.local` стоит `NEXT_PUBLIC_WS_URL=ws://localhost:9000`
|
||||||
|
|
||||||
## 🔐 Тестовая авторизация
|
## 🔐 Тестовая авторизация
|
||||||
|
|
||||||
Используйте существующего пользователя из вашей БД:
|
Используйте существующего пользователя из вашей БД:
|
||||||
@ -77,6 +107,8 @@ Email: ваш_email@example.com
|
|||||||
Password: ваш_пароль
|
Password: ваш_пароль
|
||||||
```
|
```
|
||||||
|
|
||||||
|
После входа токен автоматически сохранится в `localStorage` и будет использован для WebSocket аутентификации.
|
||||||
|
|
||||||
## 📁 Структура страниц
|
## 📁 Структура страниц
|
||||||
|
|
||||||
Сейчас доступно:
|
Сейчас доступно:
|
||||||
@ -124,10 +156,40 @@ touch src/pages/documents/index.ts
|
|||||||
- Проверьте что пользователь существует в БД
|
- Проверьте что пользователь существует в БД
|
||||||
- Проверьте endpoint: `http://localhost:8000/api/auth/token/`
|
- Проверьте endpoint: `http://localhost:8000/api/auth/token/`
|
||||||
|
|
||||||
|
### WebSocket error: {}
|
||||||
|
**Это самая частая ошибка!** Решение:
|
||||||
|
|
||||||
|
1. **Проверьте что backend запущен через Daphne:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
.venv/Scripts/daphne -b 0.0.0.0 -p 9000 eusrr_backend.asgi:application
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **НЕ используйте `python manage.py runserver`** - это не поддерживает WebSocket!
|
||||||
|
|
||||||
|
3. **Убедитесь что .env.local имеет правильный URL:**
|
||||||
|
```dotenv
|
||||||
|
NEXT_PUBLIC_WS_URL=ws://localhost:9000
|
||||||
|
NEXT_PUBLIC_BACKEND_URL=http://localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Проверьте в DevTools → Network → WS:**
|
||||||
|
- Введите фильтр `ws` чтобы найти WebSocket подключение
|
||||||
|
- Если соединение **101 Switching Protocols** - всё работает ✅
|
||||||
|
- Если ошибка **Failed** - backend не запущен через Daphne ❌
|
||||||
|
|
||||||
|
5. **Проверьте порт 9000:**
|
||||||
|
```bash
|
||||||
|
# Windows - проверить какой процесс занимает порт
|
||||||
|
netstat -ano | findstr :9000
|
||||||
|
|
||||||
|
# Если занято - убейте процесс или используйте другой порт
|
||||||
|
```
|
||||||
|
|
||||||
### Страница не загружается
|
### Страница не загружается
|
||||||
- Убедитесь что оба сервера запущены
|
- Убедитесь что оба сервера запущены
|
||||||
- Проверьте `.env` файл в frontend
|
- Проверьте `.env.local` файл в frontend
|
||||||
- Откройте DevTools → Network
|
- Откройте DevTools → Network и посмотрите статусы запросов
|
||||||
|
|
||||||
## 📚 Документация
|
## 📚 Документация
|
||||||
|
|
||||||
|
|||||||
@ -51,15 +51,22 @@ export function useWebSocket({
|
|||||||
isConnectingRef.current = true;
|
isConnectingRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
// Простую логику построения URL
|
||||||
const host = process.env.NEXT_PUBLIC_WS_URL || 'localhost:9000';
|
let wsUrl = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:9000/ws/';
|
||||||
|
|
||||||
// Получаем JWT токен для аутентификации WebSocket
|
// // Если нет протокола - добавляем
|
||||||
|
// if (!wsUrl.startsWith('ws://') && !wsUrl.startsWith('wss://')) {
|
||||||
|
// const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
// wsUrl = `${protocol}//${wsUrl}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Добавляем токен
|
||||||
const token = localStorage.getItem('access_token');
|
const token = localStorage.getItem('access_token');
|
||||||
const wsUrl = token
|
if (token) {
|
||||||
? `${protocol}//${host}/ws/?token=${token}`
|
wsUrl += `${wsUrl.includes('?') ? '&' : '?'}token=${token}`;
|
||||||
: `${protocol}//${host}/ws/`;
|
}
|
||||||
|
|
||||||
|
console.log('🔄 WebSocket URL:', wsUrl);
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user