Adds an events table + logEvent helper for a genuine activity log, and a /api/integrations/resources aggregate endpoint backed by a new optional listResources adapter method (implemented for Docker via its containers API). StatusCards, MiddleRow, BottomRow, and Infrastructure now render real integration/resource/event data instead of hardcoded numbers, with empty states where no data source exists yet (AWS cost, historical trends). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
const TOKEN_KEY = 'archnest_token'
|
|
|
|
export function getToken(): string | null {
|
|
return localStorage.getItem(TOKEN_KEY)
|
|
}
|
|
|
|
export function setToken(token: string | null) {
|
|
if (token) localStorage.setItem(TOKEN_KEY, token)
|
|
else localStorage.removeItem(TOKEN_KEY)
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
status: number
|
|
constructor(status: number, message: string) {
|
|
super(message)
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
export async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const token = getToken()
|
|
const headers: Record<string, string> = {
|
|
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
...(options.headers as Record<string, string> | undefined),
|
|
}
|
|
if (token) headers.Authorization = `Bearer ${token}`
|
|
|
|
const res = await fetch(`/api${path}`, { ...options, headers })
|
|
|
|
if (!res.ok) {
|
|
let message = res.statusText
|
|
try {
|
|
const body = await res.json()
|
|
message = body.error ?? message
|
|
} catch {
|
|
// ignore non-JSON error bodies
|
|
}
|
|
throw new ApiError(res.status, message)
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
export const api = {
|
|
getSetupStatus: () => apiFetch<{ needsSetup: boolean }>('/system/setup-status'),
|
|
setup: (username: string, password: string) =>
|
|
apiFetch<{ token: string }>('/setup', { method: 'POST', body: JSON.stringify({ username, password }) }),
|
|
login: (username: string, password: string) =>
|
|
apiFetch<{ token: string }>('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
|
|
me: () => apiFetch<{ user: { id: number; username: string; display_name: string | null; email: string | null; avatar_data_url: string | null } }>('/auth/me'),
|
|
|
|
listIntegrations: () => apiFetch<{ integrations: Integration[] }>('/integrations'),
|
|
createIntegration: (data: { type: string; name: string; config?: Record<string, string>; secrets?: Record<string, string> }) =>
|
|
apiFetch<{ integration: Integration }>('/integrations', { method: 'POST', body: JSON.stringify(data) }),
|
|
updateIntegration: (id: number, data: Partial<{ name: string; config: Record<string, string>; secrets: Record<string, string> }>) =>
|
|
apiFetch<{ integration: Integration }>(`/integrations/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
|
deleteIntegration: (id: number) => apiFetch<void>(`/integrations/${id}`, { method: 'DELETE' }),
|
|
testIntegration: (id: number) => apiFetch<{ ok: boolean; message: string }>(`/integrations/${id}/test`, { method: 'POST' }),
|
|
|
|
listBookmarks: () => apiFetch<{ bookmarks: Bookmark[] }>('/bookmarks'),
|
|
listBookmarkCategories: () => apiFetch<{ categories: BookmarkCategory[] }>('/bookmarks/categories'),
|
|
createBookmarkCategory: (data: { name: string; icon?: string; sortOrder?: number }) =>
|
|
apiFetch<{ id: number }>('/bookmarks/categories', { method: 'POST', body: JSON.stringify(data) }),
|
|
createBookmark: (data: { categoryId?: number | null; title: string; url: string; icon?: string; favorite?: boolean }) =>
|
|
apiFetch<{ id: number }>('/bookmarks', { method: 'POST', body: JSON.stringify(data) }),
|
|
updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) =>
|
|
apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
|
deleteBookmark: (id: number) => apiFetch<void>(`/bookmarks/${id}`, { method: 'DELETE' }),
|
|
|
|
listEvents: (limit = 20) => apiFetch<{ events: Event[] }>(`/events?limit=${limit}`),
|
|
listResources: () => apiFetch<{ resources: Resource[] }>('/integrations/resources'),
|
|
}
|
|
|
|
export interface Integration {
|
|
id: number
|
|
type: string
|
|
name: string
|
|
enabled: boolean
|
|
status: string
|
|
config: Record<string, string>
|
|
lastCheckedAt: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
export interface Bookmark {
|
|
id: number
|
|
category_id: number | null
|
|
title: string
|
|
url: string
|
|
icon: string | null
|
|
favorite: number
|
|
status: string
|
|
last_checked_at: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface BookmarkCategory {
|
|
id: number
|
|
name: string
|
|
icon: string | null
|
|
sort_order: number
|
|
}
|
|
|
|
export interface Event {
|
|
id: number
|
|
type: string
|
|
title: string
|
|
source: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface Resource {
|
|
name: string
|
|
status: 'healthy' | 'warning' | 'critical' | 'unknown'
|
|
detail?: string
|
|
integration: string
|
|
}
|