feat: Graphify build worker endpoints + UI trigger
- POST /api/aos/instances/[id]/graph-build: trigger or get build command - POST /api/aos/instances/[id]/graph-status: worker callback (ok/error) - GET /api/aos/graphify-health: check if Graphify service is up - Instance detail: 'Graph neu erstellen' button (visible when demo_active/active) - Graceful degradation: if GRAPHIFY_URL not set, returns build command for external worker
This commit is contained in:
1 parent
7d94fe53ad
commit
25599a5516
4 files changed
+301
-9
No files matched your search
@@ -0,0 +1,32 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createGraphifyClientFromEnv } from '@/lib/graphify'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// GET /api/aos/graphify-health
|
||||
// Checks if the Graphify MCP HTTP service is configured and reachable.
|
||||
// Used by the instance detail UI to show whether graphify is available.
|
||||
export async function GET() {
|
||||
const url = process.env.GRAPHIFY_URL
|
||||
if (!url) {
|
||||
return NextResponse.json({
|
||||
configured: false,
|
||||
ok: false,
|
||||
message: 'Graphify not configured (GRAPHIFY_URL missing)',
|
||||
})
|
||||
}
|
||||
|
||||
const client = createGraphifyClientFromEnv()
|
||||
if (!client) {
|
||||
return NextResponse.json({ configured: true, ok: false, message: 'client init failed' })
|
||||
}
|
||||
|
||||
const health = await client.health()
|
||||
return NextResponse.json({
|
||||
configured: true,
|
||||
ok: health.ok,
|
||||
url,
|
||||
version: health.version,
|
||||
message: health.ok ? 'Graphify service is up' : 'Graphify service unreachable',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { buildGraphCommand, graphPathForInstance } from '@/lib/graphify'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 600 // 10 min — graph build can take a while
|
||||
|
||||
// POST /api/aos/instances/[id]/graph-build
|
||||
// Triggers a Graphify graph build for this instance's codebase.
|
||||
// Can be called by:
|
||||
// 1. The AOS UI (admin "Graph neu erstellen" button)
|
||||
// 2. An external worker (cron job) that polls for queued graphs
|
||||
// 3. The provisioning hook (fire-and-forget)
|
||||
//
|
||||
// The actual build runs via the Graphify CLI in a subprocess. In production,
|
||||
// this should be delegated to a worker container that has Graphify installed.
|
||||
// For dev/demo: we shell out directly (requires graphify installed on host).
|
||||
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const instance = await db.instance.findUnique({
|
||||
where: { id },
|
||||
include: { license: true },
|
||||
})
|
||||
if (!instance) {
|
||||
return NextResponse.json({ error: 'instance not found' }, { status: 404 })
|
||||
}
|
||||
if (instance.status === 'deleted' || instance.status === 'deleting') {
|
||||
return NextResponse.json({ error: 'cannot build graph for deleted instance' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Parse metadata for git repo info
|
||||
const meta = instance.metadata ? JSON.parse(instance.metadata) as { gitRepo?: string; gitBranch?: string } : {}
|
||||
const gitRepo = meta.gitRepo ?? process.env.INSTANCE_GIT_REPO ?? 'https://github.com/cetinmustafa83/restov1'
|
||||
const gitBranch = meta.gitBranch ?? process.env.INSTANCE_GIT_BRANCH ?? 'main'
|
||||
const graphPath = graphPathForInstance(instance.id)
|
||||
|
||||
await db.instanceEvent.create({
|
||||
data: {
|
||||
instanceId: id,
|
||||
type: 'graphify.build.start',
|
||||
step: 'graphify',
|
||||
result: 'pending',
|
||||
details: JSON.stringify({ gitRepo, gitBranch, graphPath }),
|
||||
},
|
||||
})
|
||||
|
||||
// If Graphify service is not configured, return the command for an
|
||||
// external worker to execute. This allows the build to be picked up
|
||||
// by a separate worker container.
|
||||
if (!process.env.GRAPHIFY_URL) {
|
||||
const cmd = buildGraphCommand({ gitRepo, gitBranch, outputPath: graphPath })
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
status: 'queued',
|
||||
message: 'Graphify service not configured — build command generated for external worker.',
|
||||
graphPath,
|
||||
command: cmd,
|
||||
gitRepo,
|
||||
gitBranch,
|
||||
})
|
||||
}
|
||||
|
||||
// Graphify is configured — check if the MCP HTTP service is reachable
|
||||
const { createGraphifyClientFromEnv } = await import('@/lib/graphify')
|
||||
const client = createGraphifyClientFromEnv()
|
||||
if (!client) {
|
||||
return NextResponse.json({ error: 'Graphify client init failed' }, { status: 500 })
|
||||
}
|
||||
|
||||
const health = await client.health()
|
||||
if (!health.ok) {
|
||||
await db.instanceEvent.create({
|
||||
data: {
|
||||
instanceId: id,
|
||||
type: 'graphify.build.error',
|
||||
step: 'graphify',
|
||||
result: 'error',
|
||||
details: JSON.stringify({ error: 'Graphify service unreachable' }),
|
||||
},
|
||||
})
|
||||
return NextResponse.json({
|
||||
ok: false,
|
||||
error: 'Graphify service unreachable',
|
||||
command: buildGraphCommand({ gitRepo, gitBranch, outputPath: graphPath }),
|
||||
}, { status: 503 })
|
||||
}
|
||||
|
||||
// The MCP HTTP service is query-only. The actual build must be done
|
||||
// via the CLI in a worker. We return the command so the caller (or a
|
||||
// cron job) can execute it.
|
||||
const cmd = buildGraphCommand({ gitRepo, gitBranch, outputPath: graphPath })
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
status: 'command-ready',
|
||||
message: 'Graphify service is up. Execute the build command in a worker.',
|
||||
graphPath,
|
||||
command: cmd,
|
||||
gitRepo,
|
||||
gitBranch,
|
||||
graphifyVersion: health.version,
|
||||
})
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// GET — returns the current graph build status for this instance
|
||||
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const events = await db.instanceEvent.findMany({
|
||||
where: { instanceId: id, type: { startsWith: 'graphify.' } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
})
|
||||
|
||||
const buildStarted = events.some((e) => e.type === 'graphify.build.start' || e.type === 'graphify.build.queued')
|
||||
const buildDone = events.some((e) => e.type === 'graphify.build.ok')
|
||||
const buildFailed = events.some((e) => e.type === 'graphify.build.error')
|
||||
|
||||
let status: 'none' | 'queued' | 'building' | 'ready' | 'failed'
|
||||
if (buildDone) status = 'ready'
|
||||
else if (buildFailed) status = 'failed'
|
||||
else if (buildStarted) status = 'building'
|
||||
else status = 'none'
|
||||
|
||||
return NextResponse.json({
|
||||
status,
|
||||
events: events.map((e) => ({
|
||||
type: e.type,
|
||||
result: e.result,
|
||||
details: e.details,
|
||||
createdAt: e.createdAt,
|
||||
})),
|
||||
})
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// POST /api/aos/instances/[id]/graph-status
|
||||
// Called by the graph build worker after it finishes (success or failure).
|
||||
// Body: { status: 'ok' | 'error', stats?: {...}, error?: '...' }
|
||||
//
|
||||
// This is the callback endpoint that completes the graph build lifecycle:
|
||||
// 1. AOS queues build (graphify.build.queued)
|
||||
// 2. Worker picks up, calls POST /graph-build, gets the command
|
||||
// 3. Worker executes graphify CLI
|
||||
// 4. Worker calls this endpoint to report success/failure
|
||||
// 5. AOS records graphify.build.ok / graphify.build.error
|
||||
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await req.json().catch(() => ({}))
|
||||
const status = body.status === 'ok' ? 'ok' : 'error'
|
||||
|
||||
const instance = await db.instance.findUnique({ where: { id } })
|
||||
if (!instance) {
|
||||
return NextResponse.json({ error: 'instance not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (status === 'ok') {
|
||||
await db.instanceEvent.create({
|
||||
data: {
|
||||
instanceId: id,
|
||||
type: 'graphify.build.ok',
|
||||
step: 'graphify',
|
||||
result: 'ok',
|
||||
details: JSON.stringify({
|
||||
stats: body.stats ?? null,
|
||||
graphPath: body.graphPath ?? null,
|
||||
builtAt: new Date().toISOString(),
|
||||
}),
|
||||
},
|
||||
})
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
actor: 'graphify-worker',
|
||||
action: 'graphify.build.ok',
|
||||
entity: 'Instance',
|
||||
entityId: id,
|
||||
after: JSON.stringify({ stats: body.stats }),
|
||||
hash: 'sha256:' + Math.random().toString(36).slice(2),
|
||||
},
|
||||
})
|
||||
return NextResponse.json({ ok: true, status: 'ok' })
|
||||
} else {
|
||||
await db.instanceEvent.create({
|
||||
data: {
|
||||
instanceId: id,
|
||||
type: 'graphify.build.error',
|
||||
step: 'graphify',
|
||||
result: 'error',
|
||||
details: JSON.stringify({ error: body.error ?? 'unknown build error' }),
|
||||
},
|
||||
})
|
||||
await db.instance.update({
|
||||
where: { id },
|
||||
data: {
|
||||
lastError: `graphify build failed: ${body.error ?? 'unknown'}`,
|
||||
lastErrorAt: new Date(),
|
||||
},
|
||||
})
|
||||
return NextResponse.json({ ok: false, status: 'error', error: body.error })
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'internal server error'
|
||||
return NextResponse.json({ error: msg }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user