(plan) convert original implement plan to mock api
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Mock stand-in for the real atlas `POST /api/chat`. Streams the same SSE event shape
|
||||
* (`queue` → `reasoning` → `content` → `done`) as the real endpoint would, but replays a
|
||||
* canned reply instead of calling vLLM — no rate limiting, no budget, no real model.
|
||||
*/
|
||||
const REPLY_REASONING =
|
||||
'The user is asking about a gap in the wave column display. Argo CD sync waves are integers used purely for ordering; there is no requirement that they be contiguous. The cluster uses 0,1,2,3,5,6,7,8. This is normal and usually happens when a wave is retired or intentionally reserved.'
|
||||
|
||||
const REPLY =
|
||||
'Wave 4 is empty. Waves are sort keys, not a sequence — Argo orders by value and skips gaps, so 3 is followed directly by 5. Nothing is missing.'
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
export async function POST() {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const send = (type: string, payload: Record<string, unknown> = {}) => {
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type, ...payload })}\n\n`))
|
||||
}
|
||||
|
||||
for (let position = 3; position > 0; position--) {
|
||||
send('queue', { position })
|
||||
await wait(500)
|
||||
}
|
||||
|
||||
send('reasoning', { text: REPLY_REASONING })
|
||||
await wait(200)
|
||||
|
||||
let i = 0
|
||||
while (i < REPLY.length) {
|
||||
i += 3
|
||||
send('content', { text: REPLY.slice(0, i) })
|
||||
await wait(40)
|
||||
}
|
||||
|
||||
send('done')
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { DEFAULT_KINDS, resourceKinds } from '@/lib/clusterMock'
|
||||
|
||||
/** Mock stand-in for the real atlas `GET /api/delivery/{app}/resources`. No real pagination — fixture is small. */
|
||||
export async function GET(_req: Request, { params }: { params: Promise<{ app: string }> }) {
|
||||
const { app } = await params
|
||||
const kinds = resourceKinds[app] ?? DEFAULT_KINDS
|
||||
return NextResponse.json({
|
||||
data: { app, kinds },
|
||||
meta: { cursor: null, hasMore: false },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { apps, cluster, WAVES } from '@/lib/clusterMock'
|
||||
|
||||
/** Mock stand-in for the real atlas `GET /api/delivery`. Serves fixture data, not a live Argo watch. */
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
data: { apps, waves: WAVES },
|
||||
meta: { snapshotAge: cluster.snapshotAge, generation: cluster.generation, truncated: false },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { apps, namespaces, nodes } from '@/lib/clusterMock'
|
||||
|
||||
/**
|
||||
* Mock stand-in for the real atlas `POST /api/exec`. Same closed-command-enum shape the real
|
||||
* endpoint must have — anything unmatched rejects here too — but reads fixture data, not a live
|
||||
* snapshot, and has no rate limiting or session state.
|
||||
*/
|
||||
const COMMANDS = ['get nodes', 'get pods <ns>', 'get apps', 'top nodes', 'describe pod <ns> <name>', 'help']
|
||||
const pad = (s: string, n: number) => s.padEnd(n, ' ')
|
||||
|
||||
type Line = { kind: 'output' | 'reject'; text: string }
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const raw = typeof body.cmd === 'string' ? body.cmd : ''
|
||||
const trimmed = raw.trim().toLowerCase()
|
||||
|
||||
const reject = (text: string): Line => {
|
||||
console.warn(`[api/exec] rejected: "${raw}" — ${text}`)
|
||||
return { kind: 'reject', text }
|
||||
}
|
||||
|
||||
let lines: Line[]
|
||||
|
||||
if (trimmed === 'help') {
|
||||
lines = [{ kind: 'output', text: COMMANDS.map((c) => ` ${c}`).join('\n') }]
|
||||
} else if (trimmed === 'get nodes') {
|
||||
const head = `${pad('NAME', 14)}${pad('ROLE', 16)}${pad('CPU', 6)}${pad('MEM', 8)}GPU`
|
||||
const rows = nodes.map(
|
||||
(n) => `${pad(n.name, 14)}${pad(n.role, 16)}${pad(String(n.cpu), 6)}${pad(`${n.memoryGi}Gi`, 8)}${n.gpu}`,
|
||||
)
|
||||
lines = [{ kind: 'output', text: [head, ...rows].join('\n') }]
|
||||
} else if (trimmed === 'get apps') {
|
||||
const head = `${pad('NAME', 24)}${pad('WAVE', 7)}${pad('SYNC', 12)}HEALTH`
|
||||
const rows = apps.map((a) => `${pad(a.name, 24)}${pad(String(a.wave ?? '-'), 7)}${pad(a.sync, 12)}${a.health}`)
|
||||
lines = [{ kind: 'output', text: [head, ...rows].join('\n') }]
|
||||
} else if (trimmed === 'top nodes') {
|
||||
const head = `${pad('NAME', 14)}${pad('CPU%', 8)}MEM%`
|
||||
const rows = nodes.map((n) => `${pad(n.name, 14)}${pad(n.gpu ? '61%' : '18%', 8)}${n.gpu ? '74%' : '42%'}`)
|
||||
lines = [{ kind: 'output', text: [head, ...rows].join('\n') }]
|
||||
} else if (trimmed.startsWith('get pods')) {
|
||||
const ns = trimmed.split(/\s+/)[2]
|
||||
if (!ns) {
|
||||
lines = [reject('get pods requires a namespace. Try: get pods llm-serving')]
|
||||
} else {
|
||||
const known = namespaces.find((n) => n.name === ns)
|
||||
if (!known) {
|
||||
lines = [reject(`unknown namespace "${ns}" — rejected on snapshot membership, no lookup performed`)]
|
||||
} else {
|
||||
const rows = Array.from({ length: Math.min(known.total, 8) }, (_, i) => {
|
||||
const running = i < known.running
|
||||
return `${pad(`${ns}-workload-${i + 1}`, 30)}${pad(running ? '1/1' : '0/1', 6)}${running ? 'Running' : 'Pending'}`
|
||||
})
|
||||
lines = [{ kind: 'output', text: [`${pad('NAME', 30)}${pad('READY', 6)}STATUS`, ...rows].join('\n') }]
|
||||
}
|
||||
}
|
||||
} else if (trimmed.startsWith('describe pod')) {
|
||||
lines = [{ kind: 'output', text: 'phase: Running\nrestarts: 0\nage: 17h\nnode: worker-1\nready: true' }]
|
||||
} else {
|
||||
lines = [
|
||||
reject(`"${raw.trim()}" is not in the command set. Input parses to a closed enum; nothing else reaches the cluster. Type help.`),
|
||||
]
|
||||
}
|
||||
|
||||
return NextResponse.json({ lines })
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { cluster, namespaces } from '@/lib/clusterMock'
|
||||
|
||||
/**
|
||||
* Mock stand-in for the real atlas `GET /api/stream`. Emits synthetic `topology` deltas on an
|
||||
* interval and a keepalive comment frame, matching the SSE shape the frontend expects — no real
|
||||
* informer, no real cluster.
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
const encoder = new TextEncoder()
|
||||
let seq = cluster.generation
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const send = (event: string, data: unknown) => {
|
||||
controller.enqueue(encoder.encode(`event: ${event}\nid: ${seq++}\ndata: ${JSON.stringify(data)}\n\n`))
|
||||
}
|
||||
|
||||
const keepalive = setInterval(() => {
|
||||
controller.enqueue(encoder.encode(': keepalive\n\n'))
|
||||
}, 30_000)
|
||||
|
||||
const delta = setInterval(() => {
|
||||
const ns = namespaces[Math.floor(Math.random() * namespaces.length)]
|
||||
send('topology', { namespace: ns.name, running: ns.running, total: ns.total })
|
||||
}, 5_000)
|
||||
|
||||
req.signal.addEventListener('abort', () => {
|
||||
clearInterval(keepalive)
|
||||
clearInterval(delta)
|
||||
controller.close()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cluster, models, namespaces, nodes } from '@/lib/clusterMock'
|
||||
|
||||
/** Mock stand-in for the real atlas `GET /api/topology`. Serves fixture data, not a live cluster. */
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
data: { nodes, namespaces, models },
|
||||
meta: { snapshotAge: cluster.snapshotAge, generation: cluster.generation, truncated: false },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user