feat: add all application source code
- apps/api: FastAPI backend with Dockerfile - apps/web: Next.js frontend with Dockerfile - apps/sensor: Signal collection agent - packages: shared packages Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
226
apps/web/src/lib/api-client.ts
Normal file
226
apps/web/src/lib/api-client.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* AWOOOI API Client
|
||||
* ADR-005: 所有請求經過 BFF
|
||||
*
|
||||
* 統帥鐵律: 禁止任何 Fallback IP,環境變數缺失即噴錯
|
||||
*/
|
||||
|
||||
// 絕對純化: 環境變數缺失時直接拋出致命錯誤,嚴禁任何 Fallback
|
||||
const getApiBaseUrl = (): string => {
|
||||
const url = process.env.NEXT_PUBLIC_API_URL
|
||||
if (!url) {
|
||||
const fatalMsg = '[AWOOOI FATAL] Missing NEXT_PUBLIC_API_URL configuration.'
|
||||
console.error(fatalMsg)
|
||||
if (typeof window !== 'undefined') {
|
||||
console.error('%c' + fatalMsg, 'color: #ef4444; font-weight: bold; font-size: 16px;')
|
||||
}
|
||||
throw new Error(fatalMsg)
|
||||
}
|
||||
return url.endsWith('/api/v1') ? url : `${url}/api/v1`
|
||||
}
|
||||
|
||||
const API_BASE_URL = getApiBaseUrl()
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public code: string,
|
||||
message: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}))
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
error.code || 'UNKNOWN_ERROR',
|
||||
error.message || response.statusText
|
||||
)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
// Health
|
||||
async getHealth() {
|
||||
const res = await fetch(`${API_BASE_URL}/health`)
|
||||
return handleResponse<{
|
||||
status: 'healthy' | 'degraded' | 'unhealthy'
|
||||
version: string
|
||||
timestamp: string
|
||||
components: Record<string, 'up' | 'down'>
|
||||
}>(res)
|
||||
},
|
||||
|
||||
// Agent
|
||||
async getAgentStatus() {
|
||||
const res = await fetch(`${API_BASE_URL}/agent/status`)
|
||||
return handleResponse<{
|
||||
status: 'idle' | 'thinking' | 'executing' | 'waiting_approval'
|
||||
active_conversations: number
|
||||
current_task: string | null
|
||||
last_activity: string | null
|
||||
}>(res)
|
||||
},
|
||||
|
||||
async chat(message: string, conversationId?: string) {
|
||||
const res = await fetch(`${API_BASE_URL}/agent/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, conversation_id: conversationId }),
|
||||
})
|
||||
return handleResponse<{
|
||||
message: string
|
||||
conversation_id: string
|
||||
requires_approval: boolean
|
||||
approval_id?: string
|
||||
}>(res)
|
||||
},
|
||||
|
||||
// Plugins
|
||||
async listPlugins(category?: string) {
|
||||
const params = category ? `?category=${category}` : ''
|
||||
const res = await fetch(`${API_BASE_URL}/plugins${params}`)
|
||||
return handleResponse<Array<{
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
category: string
|
||||
enabled: boolean
|
||||
description?: string
|
||||
}>>(res)
|
||||
},
|
||||
|
||||
// Approvals
|
||||
async listApprovals(status?: string) {
|
||||
const params = status ? `?status=${status}` : ''
|
||||
const res = await fetch(`${API_BASE_URL}/approvals${params}`)
|
||||
return handleResponse<{
|
||||
items: Array<{
|
||||
id: string
|
||||
type: string
|
||||
status: string
|
||||
action: {
|
||||
plugin_id: string
|
||||
operation: string
|
||||
risk_level: string
|
||||
}
|
||||
requested_at: string
|
||||
}>
|
||||
}>(res)
|
||||
},
|
||||
|
||||
async approveApproval(approvalId: string, reason?: string) {
|
||||
const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/approve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason }),
|
||||
})
|
||||
return handleResponse<{ id: string; status: string }>(res)
|
||||
},
|
||||
|
||||
async rejectApproval(approvalId: string, reason?: string) {
|
||||
const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason }),
|
||||
})
|
||||
return handleResponse<{ id: string; status: string }>(res)
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Phase 7: Incidents API (真實血脈)
|
||||
// =========================================================================
|
||||
|
||||
async listIncidents() {
|
||||
const res = await fetch(`${API_BASE_URL}/incidents`)
|
||||
return handleResponse<IncidentListResponse>(res)
|
||||
},
|
||||
|
||||
async getIncident(incidentId: string) {
|
||||
const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}`)
|
||||
return handleResponse<IncidentResponse>(res)
|
||||
},
|
||||
|
||||
async generateProposal(incidentId: string) {
|
||||
const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}/proposal`, {
|
||||
method: 'POST',
|
||||
})
|
||||
return handleResponse<ProposalGenerateResponse>(res)
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Phase 7: Pending Approvals API (真實血脈)
|
||||
// =========================================================================
|
||||
|
||||
async getPendingApprovals() {
|
||||
const res = await fetch(`${API_BASE_URL}/approvals/pending`)
|
||||
return handleResponse<PendingApprovalsResponse>(res)
|
||||
},
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Type Definitions (Phase 7)
|
||||
// =========================================================================
|
||||
|
||||
export interface IncidentResponse {
|
||||
incident_id: string
|
||||
status: 'investigating' | 'mitigating' | 'resolved' | 'closed'
|
||||
severity: 'P0' | 'P1' | 'P2' | 'P3'
|
||||
signal_count: number
|
||||
affected_services: string[]
|
||||
proposal_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface IncidentListResponse {
|
||||
count: number
|
||||
incidents: IncidentResponse[]
|
||||
}
|
||||
|
||||
export interface BlastRadius {
|
||||
affected_pods: number
|
||||
estimated_downtime: string
|
||||
related_services: string[]
|
||||
data_impact: 'none' | 'read_only' | 'write' | 'destructive'
|
||||
}
|
||||
|
||||
export interface DryRunCheck {
|
||||
name: string
|
||||
passed: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ApprovalResponse {
|
||||
id: string
|
||||
action: string
|
||||
description: string
|
||||
status: 'pending' | 'approved' | 'rejected' | 'expired'
|
||||
risk_level: 'low' | 'medium' | 'high' | 'critical'
|
||||
blast_radius: BlastRadius
|
||||
dry_run_checks: DryRunCheck[]
|
||||
required_signatures: number
|
||||
current_signatures: number
|
||||
signatures: Array<{ signer: string; signed_at: string }>
|
||||
requested_by: string
|
||||
created_at: string
|
||||
expires_at: string | null
|
||||
}
|
||||
|
||||
export interface PendingApprovalsResponse {
|
||||
count: number
|
||||
approvals: ApprovalResponse[]
|
||||
}
|
||||
|
||||
export interface ProposalGenerateResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
incident_id: string
|
||||
proposal: ApprovalResponse | null
|
||||
incident_status: string | null
|
||||
}
|
||||
41
apps/web/src/lib/config.ts
Normal file
41
apps/web/src/lib/config.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* AWOOOI Frontend Configuration
|
||||
* ==============================
|
||||
* 統帥鐵律: 禁止任何 Fallback IP
|
||||
*
|
||||
* 唯一真相來源 - 所有 API URL 必須從環境變數取得
|
||||
* 環境變數缺失即噴錯,絕不姑息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 取得 API Base URL (絕對純化版)
|
||||
*
|
||||
* @throws Error 如果 NEXT_PUBLIC_API_URL 未設定
|
||||
*/
|
||||
function requireApiUrl(): string {
|
||||
const url = process.env.NEXT_PUBLIC_API_URL
|
||||
if (!url) {
|
||||
const errorMsg = '[AWOOOI ERROR] Missing API URL configuration. Set NEXT_PUBLIC_API_URL in .env.local'
|
||||
console.error(errorMsg)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* API Base URL (環境變數必填)
|
||||
*/
|
||||
export const API_BASE_URL = requireApiUrl()
|
||||
|
||||
/**
|
||||
* API Base URL with /api/v1 prefix
|
||||
*/
|
||||
export const API_V1_URL = `${API_BASE_URL}/api/v1`
|
||||
|
||||
/**
|
||||
* Get API URL (for dynamic usage)
|
||||
* @throws Error 如果環境變數未設定
|
||||
*/
|
||||
export function getApiUrl(): string {
|
||||
return requireApiUrl()
|
||||
}
|
||||
9
apps/web/src/lib/utils.ts
Normal file
9
apps/web/src/lib/utils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/**
|
||||
* Merge Tailwind classes with clsx
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user