chore: remove nextjs integration folder

This commit is contained in:
2026-08-31 20:45:45 -07:00
parent 4af000c6ae
commit f3c88c0d9d
5 changed files with 0 additions and 715 deletions
-97
View File
@@ -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 {};
-180
View File
@@ -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<Dashboard> {
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<DashboardMeta[]> {
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<PrometheusResult> {
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<string, string>;
values: [number, string][];
}[];
};
}
/**
* Get LLM-specific metrics
*/
export async function getLLMMetrics(): Promise<LLMMetrics> {
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<string, number>;
memoryByPod: Record<string, number>;
}
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<string, number> {
const out: Record<string, number> = {};
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}`;
}
-193
View File
@@ -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<void>;
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<Message[]>(
systemPrompt ? [{ role: 'system', content: systemPrompt }] : []
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const abortRef = useRef<AbortController | null>(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<string, number>;
memoryByPod: Record<string, number>;
}
/**
* Metrics hook with auto-refresh
*/
export function useLLMMetrics(refreshInterval = 30000) {
const [metrics, setMetrics] = useState<LLMMetrics | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(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,
};
}
-114
View File
@@ -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=<service-account-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 (
* <div>
* {messages.map((m, i) => <p key={i}>{m.role}: {m.content}</p>)}
* <button onClick={() => send('Hello')} disabled={isLoading}>
* Send
* </button>
* </div>
* );
* }
*
* function MetricsDashboard() {
* const { metrics, isLoading } = useLLMMetrics(30000);
* const { iframeProps } = useGrafanaEmbed('llm-frontend', 2);
*
* if (isLoading) return <p>Loading...</p>;
*
* return (
* <div>
* <p>Pods ready: {metrics?.podsReady}</p>
* {iframeProps && <iframe {...iframeProps} />}
* </div>
* );
* }
*
* ═══════════════════════════════════════════════════════════════════════════════
* AVAILABLE MODELS
* ═══════════════════════════════════════════════════════════════════════════════
*
* | model | Engine | Notes |
* |--------------------|---------|--------------------------------|
* | reasoning | vLLM | Qwen3-32B, best for complex |
* | ornith:35b | Ollama | General purpose |
* | qwen2.5:3b-instruct| Ollama | Fast, smaller tasks |
*
* ═══════════════════════════════════════════════════════════════════════════════
* GRAFANA EMBEDDING (optional, requires config change)
* ═══════════════════════════════════════════════════════════════════════════════
*
* To enable iframe embedding, add to grafana.ini:
* security:
* allow_embedding: true
*
* Or via Helm values (k8s/infra/logging/grafana-values.yaml):
* grafana.ini:
* security:
* allow_embedding: true
*
* Panel embed URLs follow pattern:
* https://grafana.riotpiao.com/d-solo/llm-frontend?panelId=2&from=now-6h&to=now
*
* Dashboard UID for LLM: llm-frontend
* Panel IDs: 2 (pods ready), 11 (CPU), 12 (Memory), 13 (Restarts)
*/
// Re-export all modules
export * from './llm-client';
export * from './grafana-client';
export * from './hooks';
-131
View File
@@ -1,131 +0,0 @@
/**
* LLM Client for api.riotpiao.com
*
* Usage:
* import { chat, streamChat } from './llm-client';
*
* // Non-streaming
* const response = await chat({ model: 'reasoning', messages: [...] });
*
* // Streaming
* for await (const chunk of streamChat({ model: 'reasoning', messages: [...] })) {
* process.stdout.write(chunk);
* }
*/
export type Model = 'reasoning' | 'ornith:35b' | 'qwen2.5:3b-instruct';
export interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface ChatRequest {
model: Model;
messages: Message[];
max_tokens?: number;
temperature?: number;
stream?: boolean;
}
export interface ChatResponse {
id: string;
object: string;
created: number;
model: string;
choices: {
index: number;
message: Message;
finish_reason: string;
}[];
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
const LLM_BASE_URL = process.env.LLM_BASE_URL || 'https://api.riotpiao.com/v1';
/**
* Non-streaming chat completion
*/
export async function chat(request: ChatRequest): Promise<ChatResponse> {
const response = await fetch(`${LLM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...request, stream: false }),
});
if (!response.ok) {
throw new Error(`LLM API error: ${response.status} ${await response.text()}`);
}
return response.json();
}
/**
* Streaming chat completion - yields content chunks
*/
export async function* streamChat(request: ChatRequest): AsyncGenerator<string> {
const response = await fetch(`${LLM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...request, stream: true }),
});
if (!response.ok) {
throw new Error(`LLM API error: ${response.status} ${await response.text()}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('No response body');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) yield content;
} catch {
// Skip malformed chunks
}
}
}
}
}
/**
* React hook compatible streaming (returns ReadableStream for Response)
*/
export function createStreamResponse(request: ChatRequest): Response {
const stream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of streamChat(request)) {
controller.enqueue(new TextEncoder().encode(chunk));
}
controller.close();
} catch (err) {
controller.error(err);
}
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
});
}