From f3c88c0d9de81b67ea1ef5cdb7cd5dbed225ce36 Mon Sep 17 00:00:00 2001 From: rock Date: Mon, 31 Aug 2026 20:45:45 -0700 Subject: [PATCH] chore: remove nextjs integration folder --- integrations/nextjs/api-routes.ts | 97 ------------- integrations/nextjs/grafana-client.ts | 180 ------------------------ integrations/nextjs/hooks.ts | 193 -------------------------- integrations/nextjs/index.ts | 114 --------------- integrations/nextjs/llm-client.ts | 131 ----------------- 5 files changed, 715 deletions(-) delete mode 100644 integrations/nextjs/api-routes.ts delete mode 100644 integrations/nextjs/grafana-client.ts delete mode 100644 integrations/nextjs/hooks.ts delete mode 100644 integrations/nextjs/index.ts delete mode 100644 integrations/nextjs/llm-client.ts diff --git a/integrations/nextjs/api-routes.ts b/integrations/nextjs/api-routes.ts deleted file mode 100644 index 73f7a79..0000000 --- a/integrations/nextjs/api-routes.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * NextJS API Route Examples - * - * Copy these to your NextJS app's pages/api/ or app/api/ directory - */ - -// ═══════════════════════════════════════════════════════════════════════════════ -// pages/api/chat.ts (Pages Router) -// ═══════════════════════════════════════════════════════════════════════════════ -/* -import type { NextApiRequest, NextApiResponse } from 'next'; -import { chat, streamChat, type Message } from '@/lib/llm-client'; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } - - const { messages, model = 'reasoning', stream = false } = req.body; - - if (!messages || !Array.isArray(messages)) { - return res.status(400).json({ error: 'messages required' }); - } - - try { - if (stream) { - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - - for await (const chunk of streamChat({ model, messages })) { - res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`); - } - res.write('data: [DONE]\n\n'); - res.end(); - } else { - const response = await chat({ model, messages }); - res.json(response); - } - } catch (err) { - res.status(500).json({ error: err instanceof Error ? err.message : 'Unknown error' }); - } -} -*/ - -// ═══════════════════════════════════════════════════════════════════════════════ -// app/api/chat/route.ts (App Router) -// ═══════════════════════════════════════════════════════════════════════════════ -/* -import { NextRequest } from 'next/server'; -import { chat, createStreamResponse, type Model, type Message } from '@/lib/llm-client'; - -export async function POST(req: NextRequest) { - const { messages, model = 'reasoning', stream = false } = await req.json(); - - if (!messages || !Array.isArray(messages)) { - return Response.json({ error: 'messages required' }, { status: 400 }); - } - - if (stream) { - return createStreamResponse({ model, messages }); - } - - const response = await chat({ model, messages }); - return Response.json(response); -} -*/ - -// ═══════════════════════════════════════════════════════════════════════════════ -// app/api/metrics/route.ts (App Router - Grafana proxy) -// ═══════════════════════════════════════════════════════════════════════════════ -/* -import { NextRequest } from 'next/server'; -import { getLLMMetrics, getDashboard } from '@/lib/grafana-client'; - -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url); - const type = searchParams.get('type') || 'metrics'; - - try { - if (type === 'dashboard') { - const dashboard = await getDashboard('llm-frontend'); - return Response.json(dashboard); - } - - const metrics = await getLLMMetrics(); - return Response.json(metrics); - } catch (err) { - return Response.json( - { error: err instanceof Error ? err.message : 'Unknown error' }, - { status: 500 } - ); - } -} -*/ - -export {}; diff --git a/integrations/nextjs/grafana-client.ts b/integrations/nextjs/grafana-client.ts deleted file mode 100644 index 0f51153..0000000 --- a/integrations/nextjs/grafana-client.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Grafana Dashboard API Client - * - * Fetches dashboard data and panel snapshots from Grafana. - * Requires service account token with Viewer role. - * - * Setup: - * 1. Grafana UI > Administration > Service Accounts > Add - * 2. Create token, set GRAFANA_API_TOKEN env var - * 3. Or use Grafana API directly: POST /api/serviceaccounts - */ - -const GRAFANA_URL = process.env.GRAFANA_URL || 'https://grafana.riotpiao.com'; -const GRAFANA_API_TOKEN = process.env.GRAFANA_API_TOKEN || ''; - -interface GrafanaHeaders { - 'Content-Type': string; - Authorization?: string; -} - -function headers(): GrafanaHeaders { - const h: GrafanaHeaders = { 'Content-Type': 'application/json' }; - if (GRAFANA_API_TOKEN) { - h.Authorization = `Bearer ${GRAFANA_API_TOKEN}`; - } - return h; -} - -export interface Dashboard { - uid: string; - title: string; - panels: Panel[]; -} - -export interface Panel { - id: number; - title: string; - type: string; - gridPos: { h: number; w: number; x: number; y: number }; -} - -export interface DashboardMeta { - uid: string; - title: string; - url: string; - slug: string; -} - -/** - * Get dashboard by UID - */ -export async function getDashboard(uid: string): Promise { - const response = await fetch(`${GRAFANA_URL}/api/dashboards/uid/${uid}`, { - headers: headers(), - }); - - if (!response.ok) { - throw new Error(`Grafana API error: ${response.status} ${await response.text()}`); - } - - const data = await response.json(); - return data.dashboard; -} - -/** - * List all dashboards - */ -export async function listDashboards(): Promise { - const response = await fetch(`${GRAFANA_URL}/api/search?type=dash-db`, { - headers: headers(), - }); - - if (!response.ok) { - throw new Error(`Grafana API error: ${response.status}`); - } - - return response.json(); -} - -/** - * Query Prometheus datasource directly via Grafana proxy - * Bypasses need for direct Prometheus access - */ -export async function queryPrometheus( - query: string, - start?: number, - end?: number, - step?: number -): Promise { - const params = new URLSearchParams({ - query, - start: String(start || Math.floor(Date.now() / 1000) - 3600), - end: String(end || Math.floor(Date.now() / 1000)), - step: String(step || 60), - }); - - const response = await fetch( - `${GRAFANA_URL}/api/datasources/proxy/uid/prometheus/api/v1/query_range?${params}`, - { headers: headers() } - ); - - if (!response.ok) { - throw new Error(`Prometheus query error: ${response.status}`); - } - - return response.json(); -} - -export interface PrometheusResult { - status: string; - data: { - resultType: string; - result: { - metric: Record; - values: [number, string][]; - }[]; - }; -} - -/** - * Get LLM-specific metrics - */ -export async function getLLMMetrics(): Promise { - const [podsReady, cpuUsage, memoryUsage] = await Promise.all([ - queryPrometheus('sum(kube_pod_status_ready{namespace="llm-serving",condition="true"})'), - queryPrometheus('sum(rate(container_cpu_usage_seconds_total{namespace="llm-serving"}[5m])) by (pod)'), - queryPrometheus('sum(container_memory_working_set_bytes{namespace="llm-serving"}) by (pod)'), - ]); - - return { - podsReady: extractLatestValue(podsReady), - cpuByPod: extractSeriesLatest(cpuUsage), - memoryByPod: extractSeriesLatest(memoryUsage), - }; -} - -export interface LLMMetrics { - podsReady: number; - cpuByPod: Record; - memoryByPod: Record; -} - -function extractLatestValue(result: PrometheusResult): number { - const values = result.data?.result?.[0]?.values; - if (!values || values.length === 0) return 0; - return parseFloat(values[values.length - 1][1]); -} - -function extractSeriesLatest(result: PrometheusResult): Record { - const out: Record = {}; - for (const series of result.data?.result || []) { - const pod = series.metric.pod || 'unknown'; - const values = series.values; - if (values && values.length > 0) { - out[pod] = parseFloat(values[values.length - 1][1]); - } - } - return out; -} - -/** - * Generate iframe embed URL for a panel - * Requires Grafana security.allow_embedding=true - */ -export function getPanelEmbedUrl( - dashboardUid: string, - panelId: number, - from = 'now-6h', - to = 'now', - refresh = '30s' -): string { - return `${GRAFANA_URL}/d-solo/${dashboardUid}?panelId=${panelId}&from=${from}&to=${to}&refresh=${refresh}`; -} - -/** - * Generate full dashboard URL - */ -export function getDashboardUrl(dashboardUid: string): string { - return `${GRAFANA_URL}/d/${dashboardUid}`; -} diff --git a/integrations/nextjs/hooks.ts b/integrations/nextjs/hooks.ts deleted file mode 100644 index 1ff24a3..0000000 --- a/integrations/nextjs/hooks.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * React Hooks for LLM & Grafana Integration - * - * Usage: - * import { useChat, useLLMMetrics } from '@/lib/hooks'; - * - * function ChatComponent() { - * const { messages, send, isLoading } = useChat(); - * // ... - * } - */ - -import { useState, useCallback, useRef, useEffect } from 'react'; - -export interface Message { - role: 'system' | 'user' | 'assistant'; - content: string; -} - -export interface UseChatOptions { - model?: 'reasoning' | 'ornith:35b' | 'qwen2.5:3b-instruct'; - systemPrompt?: string; - onError?: (error: Error) => void; -} - -export interface UseChatReturn { - messages: Message[]; - send: (content: string) => Promise; - isLoading: boolean; - error: Error | null; - clear: () => void; -} - -/** - * Chat hook with streaming support - */ -export function useChat(options: UseChatOptions = {}): UseChatReturn { - const { model = 'reasoning', systemPrompt, onError } = options; - - const [messages, setMessages] = useState( - systemPrompt ? [{ role: 'system', content: systemPrompt }] : [] - ); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const abortRef = useRef(null); - - const send = useCallback( - async (content: string) => { - const userMessage: Message = { role: 'user', content }; - setMessages((prev) => [...prev, userMessage]); - setIsLoading(true); - setError(null); - - abortRef.current = new AbortController(); - - try { - const response = await fetch('/api/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model, - messages: [...messages, userMessage], - stream: true, - }), - signal: abortRef.current.signal, - }); - - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - - const reader = response.body?.getReader(); - if (!reader) throw new Error('No response body'); - - const decoder = new TextDecoder(); - let assistantContent = ''; - - // Add placeholder assistant message - setMessages((prev) => [...prev, { role: 'assistant', content: '' }]); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const text = decoder.decode(value, { stream: true }); - const lines = text.split('\n').filter((line) => line.startsWith('data: ')); - - for (const line of lines) { - const data = line.slice(6); - if (data === '[DONE]') break; - - try { - const parsed = JSON.parse(data); - if (parsed.content) { - assistantContent += parsed.content; - setMessages((prev) => { - const updated = [...prev]; - updated[updated.length - 1] = { - role: 'assistant', - content: assistantContent, - }; - return updated; - }); - } - } catch { - // Skip malformed chunks - } - } - } - } catch (err) { - if (err instanceof Error && err.name !== 'AbortError') { - setError(err); - onError?.(err); - } - } finally { - setIsLoading(false); - abortRef.current = null; - } - }, - [messages, model, onError] - ); - - const clear = useCallback(() => { - abortRef.current?.abort(); - setMessages(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []); - setError(null); - }, [systemPrompt]); - - return { messages, send, isLoading, error, clear }; -} - -export interface LLMMetrics { - podsReady: number; - cpuByPod: Record; - memoryByPod: Record; -} - -/** - * Metrics hook with auto-refresh - */ -export function useLLMMetrics(refreshInterval = 30000) { - const [metrics, setMetrics] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchMetrics = useCallback(async () => { - try { - const response = await fetch('/api/metrics'); - if (!response.ok) throw new Error(`API error: ${response.status}`); - const data = await response.json(); - setMetrics(data); - setError(null); - } catch (err) { - setError(err instanceof Error ? err : new Error('Unknown error')); - } finally { - setIsLoading(false); - } - }, []); - - useEffect(() => { - fetchMetrics(); - const interval = setInterval(fetchMetrics, refreshInterval); - return () => clearInterval(interval); - }, [fetchMetrics, refreshInterval]); - - return { metrics, isLoading, error, refresh: fetchMetrics }; -} - -/** - * Grafana iframe embed component props generator - */ -export function useGrafanaEmbed(dashboardUid: string, panelId?: number) { - const baseUrl = process.env.NEXT_PUBLIC_GRAFANA_URL || 'https://grafana.riotpiao.com'; - - const dashboardUrl = `${baseUrl}/d/${dashboardUid}`; - const embedUrl = panelId - ? `${baseUrl}/d-solo/${dashboardUid}?panelId=${panelId}&from=now-6h&to=now&refresh=30s` - : null; - - return { - dashboardUrl, - embedUrl, - iframeProps: embedUrl - ? { - src: embedUrl, - width: '100%', - height: '400', - frameBorder: 0, - style: { border: 'none' }, - } - : null, - }; -} diff --git a/integrations/nextjs/index.ts b/integrations/nextjs/index.ts deleted file mode 100644 index b18cab1..0000000 --- a/integrations/nextjs/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * NextJS Integration for Homelab LLM & Grafana - * - * ═══════════════════════════════════════════════════════════════════════════════ - * SETUP - * ═══════════════════════════════════════════════════════════════════════════════ - * - * 1. Copy files to your NextJS project: - * cp integrations/nextjs/*.ts your-nextjs-app/lib/ - * - * 2. Environment variables (.env.local): - * LLM_BASE_URL=https://api.riotpiao.com/v1 - * GRAFANA_URL=https://grafana.riotpiao.com - * GRAFANA_API_TOKEN= - * NEXT_PUBLIC_GRAFANA_URL=https://grafana.riotpiao.com - * - * 3. Create Grafana service account (for API access): - * - Grafana UI > Administration > Service Accounts - * - Create account with Viewer role - * - Generate token, save to GRAFANA_API_TOKEN - * - * 4. Copy API routes (see api-routes.ts for templates): - * - app/api/chat/route.ts (LLM chat endpoint) - * - app/api/metrics/route.ts (Grafana metrics proxy) - * - * ═══════════════════════════════════════════════════════════════════════════════ - * USAGE EXAMPLES - * ═══════════════════════════════════════════════════════════════════════════════ - * - * Server-side (API routes, server components): - * - * import { chat, streamChat } from '@/lib/llm-client'; - * import { getLLMMetrics, getDashboard } from '@/lib/grafana-client'; - * - * // Non-streaming chat - * const response = await chat({ - * model: 'reasoning', - * messages: [{ role: 'user', content: 'Hello' }], - * }); - * - * // Streaming chat - * for await (const chunk of streamChat({ model: 'reasoning', messages })) { - * process.stdout.write(chunk); - * } - * - * // Get LLM pod metrics - * const metrics = await getLLMMetrics(); - * console.log(`Pods ready: ${metrics.podsReady}`); - * - * Client-side (React components): - * - * import { useChat, useLLMMetrics, useGrafanaEmbed } from '@/lib/hooks'; - * - * function ChatUI() { - * const { messages, send, isLoading } = useChat({ model: 'reasoning' }); - * - * return ( - *
- * {messages.map((m, i) =>

{m.role}: {m.content}

)} - * - *
- * ); - * } - * - * function MetricsDashboard() { - * const { metrics, isLoading } = useLLMMetrics(30000); - * const { iframeProps } = useGrafanaEmbed('llm-frontend', 2); - * - * if (isLoading) return

Loading...

; - * - * return ( - *
- *

Pods ready: {metrics?.podsReady}

- * {iframeProps &&