feat(FN-005): implement frontend foundation with auth and routing

- Add OIDC authentication with PKCE flow
- Create dashboard shell with sidebar and header
- Implement project management UI (list, create, detail)
- Add tool spawn page with tool/project selection
- Create tool instance detail page with status and controls
- Set up React Router with route guards
- Add Zustand auth store and API client with types
This commit is contained in:
2026-05-14 17:28:15 +02:00
parent 6aea953734
commit 6f18dd24a5
27 changed files with 1469 additions and 43 deletions
+144
View File
@@ -0,0 +1,144 @@
import type { Project, ProjectCreate, ProjectUpdate, ToolDefinition, ToolInstance, User } from '../types/api.ts'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
export class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
public data?: unknown
) {
super(`API Error ${status}: ${statusText}`)
this.name = 'ApiError'
}
}
function getAuthToken(): string | null {
return localStorage.getItem('access_token')
}
async function fetchWithAuth(
endpoint: string,
options: RequestInit = {}
): Promise<Response> {
const url = `${API_URL}/api/v1${endpoint}`
const token = getAuthToken()
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...((options.headers as Record<string, string>) || {}),
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
if (import.meta.env.DEV) {
console.log(`[API] ${options.method || 'GET'} ${url}`)
}
const response = await fetch(url, {
...options,
headers,
})
if (import.meta.env.DEV) {
console.log(`[API] ${response.status} ${response.statusText}`)
}
if (!response.ok) {
const data = await response.json().catch(() => undefined)
throw new ApiError(response.status, response.statusText, data)
}
return response
}
export const api = {
// Auth
getCurrentUser: async (): Promise<User> => {
const response = await fetchWithAuth('/users/me')
return response.json()
},
// Projects
getProjects: async (): Promise<Project[]> => {
const response = await fetchWithAuth('/projects')
return response.json()
},
getProject: async (id: string): Promise<Project> => {
const response = await fetchWithAuth(`/projects/${id}`)
return response.json()
},
createProject: async (data: ProjectCreate): Promise<Project> => {
const response = await fetchWithAuth('/projects', {
method: 'POST',
body: JSON.stringify(data),
})
return response.json()
},
updateProject: async (id: string, data: ProjectUpdate): Promise<Project> => {
const response = await fetchWithAuth(`/projects/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
return response.json()
},
deleteProject: async (id: string): Promise<void> => {
await fetchWithAuth(`/projects/${id}`, {
method: 'DELETE',
})
},
getToolInstances: async (projectId: string): Promise<ToolInstance[]> => {
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances`)
return response.json()
},
getToolInstance: async (projectId: string, instanceId: string): Promise<ToolInstance> => {
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}`)
return response.json()
},
createToolInstance: async (projectId: string, data: { tool_definition_id: string; name: string }): Promise<ToolInstance> => {
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances`, {
method: 'POST',
body: JSON.stringify(data),
})
return response.json()
},
deleteToolInstance: async (projectId: string, instanceId: string): Promise<void> => {
await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}`, {
method: 'DELETE',
})
},
stopToolInstance: async (projectId: string, instanceId: string): Promise<ToolInstance> => {
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}/stop`, {
method: 'POST',
})
return response.json()
},
startToolInstance: async (projectId: string, instanceId: string): Promise<ToolInstance> => {
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}/start`, {
method: 'POST',
})
return response.json()
},
getToolInstanceStatus: async (projectId: string, instanceId: string): Promise<{ status: string; subdomain: string; container_id: string }> => {
const response = await fetchWithAuth(`/projects/${projectId}/tool-instances/${instanceId}/status`)
return response.json()
},
getToolRegistry: async (): Promise<ToolDefinition[]> => {
const response = await fetchWithAuth('/tools')
return response.json()
},
}