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:
OG T
2026-03-22 18:57:44 +08:00
parent a840bf975b
commit 196d269b92
245 changed files with 42207 additions and 6 deletions

View 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
}

View 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()
}

View 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))
}