Complete documentation for future sessions

- CLAUDE.md for AI agents to understand the codebase
- GITEA-GUIDE.md centralizes all Gitea operations (API, Registry, Auth)
- DEVELOPMENT-WORKFLOW.md explains complete dev process
- ROADMAP.md, NEXT-SESSION.md for planning
- QUICK-REFERENCE.md, TROUBLESHOOTING.md for daily use
- 40+ detailed docs in /docs folder
- Backend as submodule from Gitea

Everything documented for autonomous operation.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hector Ros
2026-01-20 00:36:53 +01:00
commit db71705842
49 changed files with 19162 additions and 0 deletions

504
docs/03-frontend/estado.md Normal file
View File

@@ -0,0 +1,504 @@
# Gestión de Estado
## React Query para Server State
```typescript
// hooks/useTasks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '@/api/client'
import { Task, CreateTaskInput, UpdateTaskInput } from '@/types/task'
import toast from 'react-hot-toast'
export function useTasks(filters?: { projectId?: string; state?: string }) {
return useQuery({
queryKey: ['tasks', filters],
queryFn: async () => {
const { data } = await api.get<{ tasks: Task[] }>('/tasks', { params: filters })
return data.tasks
},
})
}
export function useTask(taskId: string) {
return useQuery({
queryKey: ['tasks', taskId],
queryFn: async () => {
const { data } = await api.get<{ task: Task }>(`/tasks/${taskId}`)
return data.task
},
enabled: !!taskId,
})
}
export function useCreateTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: CreateTaskInput) => {
const { data } = await api.post('/tasks', input)
return data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
export function useUpdateTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ taskId, updates }: { taskId: string; updates: UpdateTaskInput }) => {
const { data } = await api.patch(`/tasks/${taskId}`, updates)
return data
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
queryClient.invalidateQueries({ queryKey: ['tasks', variables.taskId] })
},
})
}
export function useRespondToQuestion() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({
taskId,
questionId,
response,
}: {
taskId: string
questionId: string
response: string
}) => {
const { data } = await api.post(`/tasks/${taskId}/respond`, {
questionId,
response,
})
return data
},
onSuccess: (_, variables) => {
toast.success('Respuesta enviada')
queryClient.invalidateQueries({ queryKey: ['tasks', variables.taskId] })
},
})
}
export function useApproveTask() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (taskId: string) => {
const { data } = await api.post(`/tasks/${taskId}/approve`)
return data
},
onSuccess: () => {
toast.success('Tarea aprobada')
queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
}
```
```typescript
// hooks/useProjects.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '@/api/client'
import { Project, CreateProjectInput } from '@/types/project'
export function useProjects() {
return useQuery({
queryKey: ['projects'],
queryFn: async () => {
const { data } = await api.get<{ projects: Project[] }>('/projects')
return data.projects
},
})
}
export function useProject(projectId: string) {
return useQuery({
queryKey: ['projects', projectId],
queryFn: async () => {
const { data } = await api.get<{ project: Project }>(`/projects/${projectId}`)
return data.project
},
enabled: !!projectId,
})
}
export function useCreateProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (input: CreateProjectInput) => {
const { data } = await api.post('/projects', input)
return data
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] })
},
})
}
```
```typescript
// hooks/useAgents.ts
import { useQuery } from '@tanstack/react-query'
import { api } from '@/api/client'
import { Agent } from '@/types/agent'
export function useAgents() {
return useQuery({
queryKey: ['agents'],
queryFn: async () => {
const { data } = await api.get<{ agents: Agent[] }>('/agents')
return data.agents
},
refetchInterval: 5000, // Refetch every 5s
})
}
export function useAgent(agentId: string) {
return useQuery({
queryKey: ['agents', agentId],
queryFn: async () => {
const { data } = await api.get<{ agent: Agent }>(`/agents/${agentId}`)
return data.agent
},
enabled: !!agentId,
refetchInterval: 3000,
})
}
export function useAgentLogs(agentId: string, limit = 100) {
return useQuery({
queryKey: ['agents', agentId, 'logs', limit],
queryFn: async () => {
const { data } = await api.get(`/agents/${agentId}/logs`, {
params: { limit },
})
return data.logs
},
enabled: !!agentId,
})
}
```
## Zustand para Client State
```typescript
// store/authStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface User {
id: string
email: string
name: string
}
interface AuthState {
user: User | null
token: string | null
isAuthenticated: boolean
login: (token: string, user: User) => void
logout: () => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
login: (token, user) => {
set({ token, user, isAuthenticated: true })
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false })
},
}),
{
name: 'auth-storage',
}
)
)
```
```typescript
// store/uiStore.ts
import { create } from 'zustand'
interface UIState {
sidebarOpen: boolean
activeModal: string | null
toggleSidebar: () => void
openModal: (modalId: string) => void
closeModal: () => void
}
export const useUIStore = create<UIState>((set) => ({
sidebarOpen: true,
activeModal: null,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
openModal: (modalId) => set({ activeModal: modalId }),
closeModal: () => set({ activeModal: null }),
}))
```
```typescript
// store/terminalStore.ts
import { create } from 'zustand'
interface TerminalTab {
id: string
agentId: string
podName: string
isActive: boolean
}
interface TerminalState {
tabs: TerminalTab[]
activeTabId: string | null
openTerminal: (agentId: string, podName: string) => void
closeTerminal: (tabId: string) => void
setActiveTab: (tabId: string) => void
}
export const useTerminalStore = create<TerminalState>((set) => ({
tabs: [],
activeTabId: null,
openTerminal: (agentId, podName) =>
set((state) => {
const existingTab = state.tabs.find((t) => t.agentId === agentId)
if (existingTab) {
return {
tabs: state.tabs.map((t) => ({
...t,
isActive: t.id === existingTab.id,
})),
activeTabId: existingTab.id,
}
}
const newTab: TerminalTab = {
id: `term-${Date.now()}`,
agentId,
podName,
isActive: true,
}
return {
tabs: [
...state.tabs.map((t) => ({ ...t, isActive: false })),
newTab,
],
activeTabId: newTab.id,
}
}),
closeTerminal: (tabId) =>
set((state) => {
const newTabs = state.tabs.filter((t) => t.id !== tabId)
const newActiveTab = newTabs.length > 0 ? newTabs[0].id : null
return {
tabs: newTabs.map((t, i) => ({
...t,
isActive: i === 0,
})),
activeTabId: newActiveTab,
}
}),
setActiveTab: (tabId) =>
set((state) => ({
tabs: state.tabs.map((t) => ({
...t,
isActive: t.id === tabId,
})),
activeTabId: tabId,
})),
}))
```
## WebSocket Hook
```typescript
// hooks/useWebSocket.ts
import { useEffect } from 'use'
import { useQueryClient } from '@tanstack/react-query'
import { io, Socket } from 'socket.io-client'
import { useAuthStore } from '@/store/authStore'
import toast from 'react-hot-toast'
let socket: Socket | null = null
export function useWebSocket() {
const queryClient = useQueryClient()
const token = useAuthStore((state) => state.token)
useEffect(() => {
if (!token) return
// Initialize socket
socket = io(import.meta.env.VITE_WS_URL || 'http://localhost:3000', {
auth: { token },
})
socket.on('connect', () => {
console.log('WebSocket connected')
})
socket.on('disconnect', () => {
console.log('WebSocket disconnected')
})
// Task events
socket.on('task:created', (data) => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
toast.success(`Nueva tarea: ${data.title}`)
})
socket.on('task:status_changed', (data) => {
queryClient.invalidateQueries({ queryKey: ['tasks'] })
queryClient.invalidateQueries({ queryKey: ['tasks', data.taskId] })
if (data.newState === 'ready_to_test') {
toast.success('Tarea lista para probar!', {
duration: 5000,
})
}
})
socket.on('task:needs_input', (data) => {
queryClient.invalidateQueries({ queryKey: ['tasks', data.taskId] })
toast((t) => (
<div>
<p className="font-medium">El agente necesita información</p>
<p className="text-sm text-gray-600 mt-1">{data.question}</p>
<button
onClick={() => {
// Navigate to task
window.location.href = `/tasks/${data.taskId}`
toast.dismiss(t.id)
}}
className="mt-2 text-sm text-primary-600 hover:underline"
>
Ver tarea
</button>
</div>
), {
duration: 10000,
icon: '❓',
})
})
socket.on('task:pr_created', (data) => {
toast.success('Pull Request creado!', {
action: {
label: 'Ver PR',
onClick: () => window.open(data.prUrl, '_blank'),
},
})
})
socket.on('task:ready_to_test', (data) => {
toast.success('Preview deploy completado!', {
action: {
label: 'Ver Preview',
onClick: () => window.open(data.previewUrl, '_blank'),
},
})
})
// Agent events
socket.on('agent:status', (data) => {
queryClient.invalidateQueries({ queryKey: ['agents'] })
})
// Deploy events
socket.on('deploy:started', (data) => {
toast.loading(`Desplegando a ${data.environment}...`, {
id: `deploy-${data.deploymentId}`,
})
})
socket.on('deploy:completed', (data) => {
toast.success(`Deploy completado: ${data.environment}`, {
id: `deploy-${data.deploymentId}`,
action: {
label: 'Abrir',
onClick: () => window.open(data.url, '_blank'),
},
})
})
socket.on('deploy:failed', (data) => {
toast.error(`Deploy falló: ${data.environment}`, {
id: `deploy-${data.deploymentId}`,
})
})
return () => {
if (socket) {
socket.disconnect()
socket = null
}
}
}, [token, queryClient])
return socket
}
// Export for manual usage
export function getSocket() {
return socket
}
```
## API Client
```typescript
// api/client.ts
import axios from 'axios'
import { useAuthStore } from '@/store/authStore'
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
timeout: 30000,
})
// Request interceptor
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Response interceptor
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
```