feat: Initial commit - Task Reporter incident response system

Complete implementation of the production line incident response system (生產線異常即時反應系統) including:

Backend (FastAPI):
- User authentication with AD integration and session management
- Chat room management (create, list, update, members, roles)
- Real-time messaging via WebSocket (typing indicators, reactions)
- File storage with MinIO (upload, download, image preview)

Frontend (React + Vite):
- Authentication flow with token management
- Room list with filtering, search, and pagination
- Real-time chat interface with WebSocket
- File upload with drag-and-drop and image preview
- Member management and room settings
- Breadcrumb navigation
- 53 unit tests (Vitest)

Specifications:
- authentication: AD auth, sessions, JWT tokens
- chat-room: rooms, members, templates
- realtime-messaging: WebSocket, messages, reactions
- file-storage: MinIO integration, file management
- frontend-core: React SPA structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-01 17:42:52 +08:00
commit c8966477b9
135 changed files with 23269 additions and 0 deletions

View File

@@ -0,0 +1,262 @@
"""Pydantic schemas for WebSocket messages and REST API"""
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
from datetime import datetime
from enum import Enum
class MessageTypeEnum(str, Enum):
"""Message type enumeration for validation"""
TEXT = "text"
IMAGE_REF = "image_ref"
FILE_REF = "file_ref"
SYSTEM = "system"
INCIDENT_DATA = "incident_data"
class WebSocketMessageType(str, Enum):
"""WebSocket message type for protocol"""
MESSAGE = "message"
EDIT_MESSAGE = "edit_message"
DELETE_MESSAGE = "delete_message"
ADD_REACTION = "add_reaction"
REMOVE_REACTION = "remove_reaction"
TYPING = "typing"
SYSTEM = "system"
class SystemEventType(str, Enum):
"""System event types"""
USER_JOINED = "user_joined"
USER_LEFT = "user_left"
ROOM_STATUS_CHANGED = "room_status_changed"
MEMBER_ADDED = "member_added"
MEMBER_REMOVED = "member_removed"
FILE_UPLOADED = "file_uploaded"
FILE_DELETED = "file_deleted"
# WebSocket Incoming Messages (from client)
class WebSocketMessageIn(BaseModel):
"""Incoming WebSocket message from client"""
type: WebSocketMessageType
content: Optional[str] = None
message_type: Optional[MessageTypeEnum] = MessageTypeEnum.TEXT
message_id: Optional[str] = None # For edit/delete/reaction operations
emoji: Optional[str] = None # For reactions
metadata: Optional[Dict[str, Any]] = None # For mentions, file refs, etc.
class TextMessageIn(BaseModel):
"""Text message input"""
content: str = Field(..., min_length=1, max_length=10000)
mentions: Optional[List[str]] = None
class ImageRefMessageIn(BaseModel):
"""Image reference message input"""
content: str # Description
file_id: str
file_url: str
class FileRefMessageIn(BaseModel):
"""File reference message input"""
content: str # Description
file_id: str
file_url: str
file_name: str
class IncidentDataMessageIn(BaseModel):
"""Structured incident data message input"""
content: Dict[str, Any] # Structured data (temperature, pressure, etc.)
# WebSocket Outgoing Messages (to client)
class MessageBroadcast(BaseModel):
"""Message broadcast to all room members"""
type: str = "message"
message_id: str
room_id: str
sender_id: str
content: str
message_type: MessageTypeEnum
metadata: Optional[Dict[str, Any]] = None
created_at: datetime
edited_at: Optional[datetime] = None
deleted_at: Optional[datetime] = None
sequence_number: int
class SystemMessageBroadcast(BaseModel):
"""System message broadcast"""
type: str = "system"
event: SystemEventType
user_id: Optional[str] = None
room_id: Optional[str] = None
timestamp: datetime
data: Optional[Dict[str, Any]] = None
class TypingBroadcast(BaseModel):
"""Typing indicator broadcast"""
type: str = "typing"
room_id: str
user_id: str
is_typing: bool
class MessageAck(BaseModel):
"""Message acknowledgment"""
type: str = "ack"
message_id: str
sequence_number: int
timestamp: datetime
class ErrorMessage(BaseModel):
"""Error message"""
type: str = "error"
error: str
code: str
details: Optional[Dict[str, Any]] = None
# REST API Schemas
class MessageCreate(BaseModel):
"""Create message via REST API"""
content: str = Field(..., min_length=1, max_length=10000)
message_type: MessageTypeEnum = MessageTypeEnum.TEXT
metadata: Optional[Dict[str, Any]] = None
class MessageUpdate(BaseModel):
"""Update message content"""
content: str = Field(..., min_length=1, max_length=10000)
class MessageResponse(BaseModel):
"""Message response"""
message_id: str
room_id: str
sender_id: str
content: str
message_type: MessageTypeEnum
metadata: Optional[Dict[str, Any]] = Field(None, alias="message_metadata")
created_at: datetime
edited_at: Optional[datetime] = None
deleted_at: Optional[datetime] = None
sequence_number: int
reaction_counts: Optional[Dict[str, int]] = None # emoji -> count
class Config:
from_attributes = True
populate_by_name = True # Allow both 'metadata' and 'message_metadata'
class MessageListResponse(BaseModel):
"""Paginated message list response"""
messages: List[MessageResponse]
total: int
limit: int
offset: int
has_more: bool
class ReactionCreate(BaseModel):
"""Add reaction to message"""
emoji: str = Field(..., min_length=1, max_length=10)
class ReactionResponse(BaseModel):
"""Reaction response"""
reaction_id: int
message_id: str
user_id: str
emoji: str
created_at: datetime
class Config:
from_attributes = True
class ReactionSummary(BaseModel):
"""Reaction summary for a message"""
emoji: str
count: int
users: List[str] # List of user IDs who reacted
class OnlineUser(BaseModel):
"""Online user in a room"""
user_id: str
connected_at: datetime
# File Upload WebSocket Schemas
class FileUploadedBroadcast(BaseModel):
"""Broadcast when a file is uploaded to a room"""
type: str = "file_uploaded"
file_id: str
room_id: str
uploader_id: str
filename: str
file_type: str # image, document, log
file_size: int
mime_type: str
download_url: Optional[str] = None
uploaded_at: datetime
def to_dict(self) -> dict:
"""Convert to dictionary for WebSocket broadcast"""
return {
"type": self.type,
"file_id": self.file_id,
"room_id": self.room_id,
"uploader_id": self.uploader_id,
"filename": self.filename,
"file_type": self.file_type,
"file_size": self.file_size,
"mime_type": self.mime_type,
"download_url": self.download_url,
"uploaded_at": self.uploaded_at.isoformat()
}
class FileUploadAck(BaseModel):
"""Acknowledgment sent to uploader after successful upload"""
type: str = "file_upload_ack"
file_id: str
status: str # success, error
download_url: Optional[str] = None
error_message: Optional[str] = None
def to_dict(self) -> dict:
"""Convert to dictionary for WebSocket message"""
return {
"type": self.type,
"file_id": self.file_id,
"status": self.status,
"download_url": self.download_url,
"error_message": self.error_message
}
class FileDeletedBroadcast(BaseModel):
"""Broadcast when a file is deleted from a room"""
type: str = "file_deleted"
file_id: str
room_id: str
deleted_by: str
deleted_at: datetime
def to_dict(self) -> dict:
"""Convert to dictionary for WebSocket broadcast"""
return {
"type": self.type,
"file_id": self.file_id,
"room_id": self.room_id,
"deleted_by": self.deleted_by,
"deleted_at": self.deleted_at.isoformat()
}