(plan) convert original implement plan to mock api

This commit is contained in:
Story Crater Bot
2026-08-19 09:47:14 -07:00
parent c42040319f
commit 0aef2281bc
65 changed files with 307 additions and 627 deletions
+50
View File
@@ -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',
},
})
}
+12
View File
@@ -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 },
})
}
+10
View File
@@ -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 },
})
}
+67
View File
@@ -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 })
}
+42
View File
@@ -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',
},
})
}
+10
View File
@@ -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 },
})
}