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,65 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { useAuthStore } from './authStore'
describe('authStore', () => {
beforeEach(() => {
// Reset store state before each test
useAuthStore.setState({
user: null,
token: null,
isAuthenticated: false,
})
})
describe('setAuth', () => {
it('should set user and token', () => {
useAuthStore.getState().setAuth('test-token-123', 'Test User', 'testuser')
const state = useAuthStore.getState()
expect(state.user).toEqual({ username: 'testuser', display_name: 'Test User' })
expect(state.token).toBe('test-token-123')
expect(state.isAuthenticated).toBe(true)
})
})
describe('clearAuth', () => {
it('should clear user and token', () => {
// First set some auth data
useAuthStore.setState({
user: { username: 'testuser', display_name: 'Test User' },
token: 'test-token-123',
isAuthenticated: true,
})
useAuthStore.getState().clearAuth()
const state = useAuthStore.getState()
expect(state.user).toBeNull()
expect(state.token).toBeNull()
expect(state.isAuthenticated).toBe(false)
})
})
describe('updateUser', () => {
it('should update user properties', () => {
// First set some auth data
useAuthStore.setState({
user: { username: 'testuser', display_name: 'Test User' },
token: 'test-token-123',
isAuthenticated: true,
})
useAuthStore.getState().updateUser({ display_name: 'Updated Name' })
const state = useAuthStore.getState()
expect(state.user?.display_name).toBe('Updated Name')
expect(state.user?.username).toBe('testuser')
})
it('should not update if user is null', () => {
useAuthStore.getState().updateUser({ display_name: 'New Name' })
expect(useAuthStore.getState().user).toBeNull()
})
})
})

View File

@@ -0,0 +1,51 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { User } from '../types'
interface AuthState {
token: string | null
user: User | null
isAuthenticated: boolean
// Actions
setAuth: (token: string, displayName: string, username: string) => void
clearAuth: () => void
updateUser: (user: Partial<User>) => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isAuthenticated: false,
setAuth: (token: string, displayName: string, username: string) =>
set({
token,
user: { username, display_name: displayName },
isAuthenticated: true,
}),
clearAuth: () =>
set({
token: null,
user: null,
isAuthenticated: false,
}),
updateUser: (userData: Partial<User>) =>
set((state) => ({
user: state.user ? { ...state.user, ...userData } : null,
})),
}),
{
name: 'auth-storage',
partialize: (state) => ({
token: state.token,
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
)
)

View File

@@ -0,0 +1,172 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { useChatStore } from './chatStore'
import type { Message } from '../types'
describe('chatStore', () => {
const mockMessage: Message = {
message_id: 'msg-1',
room_id: 'room-1',
sender_id: 'user-1',
content: 'Hello, world!',
message_type: 'text',
created_at: '2024-01-01T00:00:00Z',
sequence_number: 1,
}
beforeEach(() => {
// Reset store state before each test
useChatStore.setState({
currentRoomId: null,
messages: [],
hasMoreMessages: true,
connectionStatus: 'disconnected',
typingUsers: new Set(),
onlineUsers: new Set(),
})
})
describe('setCurrentRoom', () => {
it('should set current room and clear messages', () => {
useChatStore.setState({ messages: [mockMessage] })
useChatStore.getState().setCurrentRoom('room-2')
const state = useChatStore.getState()
expect(state.currentRoomId).toBe('room-2')
expect(state.messages).toEqual([])
})
it('should clear typing and online users', () => {
useChatStore.setState({
typingUsers: new Set(['user-1']),
onlineUsers: new Set(['user-2']),
})
useChatStore.getState().setCurrentRoom('room-2')
const state = useChatStore.getState()
expect(state.typingUsers.size).toBe(0)
expect(state.onlineUsers.size).toBe(0)
})
})
describe('setMessages', () => {
it('should set messages array', () => {
const messages = [mockMessage, { ...mockMessage, message_id: 'msg-2' }]
useChatStore.getState().setMessages(messages)
expect(useChatStore.getState().messages).toEqual(messages)
})
})
describe('addMessage', () => {
it('should add a new message', () => {
useChatStore.getState().addMessage(mockMessage)
expect(useChatStore.getState().messages).toHaveLength(1)
expect(useChatStore.getState().messages[0]).toEqual(mockMessage)
})
it('should append to existing messages', () => {
useChatStore.getState().addMessage(mockMessage)
useChatStore.getState().addMessage({ ...mockMessage, message_id: 'msg-2' })
expect(useChatStore.getState().messages).toHaveLength(2)
})
})
describe('updateMessage', () => {
it('should update an existing message', () => {
useChatStore.getState().addMessage(mockMessage)
useChatStore.getState().updateMessage('msg-1', { content: 'Updated content' })
const updatedMessage = useChatStore.getState().messages[0]
expect(updatedMessage.content).toBe('Updated content')
})
it('should not update non-existent message', () => {
useChatStore.getState().addMessage(mockMessage)
useChatStore.getState().updateMessage('non-existent', { content: 'New content' })
expect(useChatStore.getState().messages[0].content).toBe('Hello, world!')
})
})
describe('removeMessage', () => {
it('should remove a message by id', () => {
useChatStore.getState().addMessage(mockMessage)
useChatStore.getState().removeMessage('msg-1')
expect(useChatStore.getState().messages).toHaveLength(0)
})
})
describe('setConnectionStatus', () => {
it('should update connection status', () => {
useChatStore.getState().setConnectionStatus('connected')
expect(useChatStore.getState().connectionStatus).toBe('connected')
})
})
describe('setUserTyping', () => {
it('should add user to typing set when typing', () => {
useChatStore.getState().setUserTyping('user-1', true)
expect(useChatStore.getState().typingUsers.has('user-1')).toBe(true)
})
it('should remove user from typing set when not typing', () => {
useChatStore.setState({ typingUsers: new Set(['user-1']) })
useChatStore.getState().setUserTyping('user-1', false)
expect(useChatStore.getState().typingUsers.has('user-1')).toBe(false)
})
})
describe('online users', () => {
it('should add online user', () => {
useChatStore.getState().addOnlineUser('user-1')
expect(useChatStore.getState().onlineUsers.has('user-1')).toBe(true)
})
it('should remove online user', () => {
useChatStore.setState({ onlineUsers: new Set(['user-1']) })
useChatStore.getState().removeOnlineUser('user-1')
expect(useChatStore.getState().onlineUsers.has('user-1')).toBe(false)
})
})
describe('prependMessages', () => {
it('should prepend messages to beginning of list', () => {
useChatStore.getState().addMessage(mockMessage)
const newMessage = { ...mockMessage, message_id: 'msg-0', sequence_number: 0 }
useChatStore.getState().prependMessages([newMessage])
const messages = useChatStore.getState().messages
expect(messages).toHaveLength(2)
expect(messages[0].message_id).toBe('msg-0')
expect(messages[1].message_id).toBe('msg-1')
})
})
describe('clearMessages', () => {
it('should clear all messages', () => {
useChatStore.getState().addMessage(mockMessage)
useChatStore.getState().addMessage({ ...mockMessage, message_id: 'msg-2' })
useChatStore.getState().clearMessages()
expect(useChatStore.getState().messages).toHaveLength(0)
})
})
})

View File

@@ -0,0 +1,125 @@
import { create } from 'zustand'
import type { Message } from '../types'
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error'
interface ChatState {
// Connection state
connectionStatus: ConnectionStatus
currentRoomId: string | null
// Messages
messages: Message[]
hasMoreMessages: boolean
// Typing indicators
typingUsers: Set<string>
// Online users
onlineUsers: Set<string>
// Actions
setConnectionStatus: (status: ConnectionStatus) => void
setCurrentRoom: (roomId: string | null) => void
// Message actions
setMessages: (messages: Message[]) => void
addMessage: (message: Message) => void
updateMessage: (messageId: string, updates: Partial<Message>) => void
removeMessage: (messageId: string) => void
prependMessages: (messages: Message[]) => void
setHasMoreMessages: (hasMore: boolean) => void
clearMessages: () => void
// Typing actions
setUserTyping: (userId: string, isTyping: boolean) => void
clearTypingUsers: () => void
// Online users actions
setOnlineUsers: (users: string[]) => void
addOnlineUser: (userId: string) => void
removeOnlineUser: (userId: string) => void
}
export const useChatStore = create<ChatState>((set) => ({
// Initial state
connectionStatus: 'disconnected',
currentRoomId: null,
messages: [],
hasMoreMessages: true,
typingUsers: new Set(),
onlineUsers: new Set(),
// Connection actions
setConnectionStatus: (status) => set({ connectionStatus: status }),
setCurrentRoom: (roomId) =>
set({
currentRoomId: roomId,
messages: [],
hasMoreMessages: true,
typingUsers: new Set(),
onlineUsers: new Set(),
connectionStatus: 'disconnected',
}),
// Message actions
setMessages: (messages) => set({ messages }),
addMessage: (message) =>
set((state) => ({
messages: [...state.messages, message],
})),
updateMessage: (messageId, updates) =>
set((state) => ({
messages: state.messages.map((msg) =>
msg.message_id === messageId ? { ...msg, ...updates } : msg
),
})),
removeMessage: (messageId) =>
set((state) => ({
messages: state.messages.filter((msg) => msg.message_id !== messageId),
})),
prependMessages: (newMessages) =>
set((state) => ({
messages: [...newMessages, ...state.messages],
})),
setHasMoreMessages: (hasMore) => set({ hasMoreMessages: hasMore }),
clearMessages: () => set({ messages: [], hasMoreMessages: true }),
// Typing actions
setUserTyping: (userId, isTyping) =>
set((state) => {
const newTypingUsers = new Set(state.typingUsers)
if (isTyping) {
newTypingUsers.add(userId)
} else {
newTypingUsers.delete(userId)
}
return { typingUsers: newTypingUsers }
}),
clearTypingUsers: () => set({ typingUsers: new Set() }),
// Online users actions
setOnlineUsers: (users) => set({ onlineUsers: new Set(users) }),
addOnlineUser: (userId) =>
set((state) => {
const newOnlineUsers = new Set(state.onlineUsers)
newOnlineUsers.add(userId)
return { onlineUsers: newOnlineUsers }
}),
removeOnlineUser: (userId) =>
set((state) => {
const newOnlineUsers = new Set(state.onlineUsers)
newOnlineUsers.delete(userId)
return { onlineUsers: newOnlineUsers }
}),
}))

View File

@@ -0,0 +1,2 @@
export { useAuthStore } from './authStore'
export { useChatStore } from './chatStore'