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:
Z User committed 2026-10-28 19:38:28 +00:00
1 parent 7d94fe53ad
commit 25599a5516
4 files changed
+301 -9

No files matched your search

+32
View File
@@ -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 })
}
}