diff --git a/kits/production-database-release-planner/.env.example b/kits/production-database-release-planner/.env.example new file mode 100644 index 000000000..e69de29bb diff --git a/kits/production-database-release-planner/README.md b/kits/production-database-release-planner/README.md new file mode 100644 index 000000000..b282d12f0 --- /dev/null +++ b/kits/production-database-release-planner/README.md @@ -0,0 +1,127 @@ +# AI Database Release Planner + +## Overview + +AI Database Release Planner is a multi-agent AI system built with Lamatic AgentKit for analyzing SQL database migrations and producing a final release report. + +The project is designed to help teams review database changes before release, with a focus on understanding the migration, assessing runtime behavior, choosing a deployment strategy, and deciding whether the release should proceed. + +## Problem Statement + +SQL migration scripts can be difficult to evaluate quickly and consistently, especially when a release may affect production availability, locking behavior, rollback complexity, or data safety. + +This project organizes that review into a structured agent pipeline so the migration can be analyzed step by step before a release decision is made. + +## Features + +- Migration Understanding Agent for SQL schema analysis. +- Behavior Analysis Agent for PostgreSQL runtime behavior assessment. +- Deployment Strategy Agent for release planning recommendations. +- Release Decision Agent for approval and rollback guidance. +- Structured JSON contracts between every pipeline stage. +- Multi-agent workflow built with Lamatic AgentKit. + +## Architecture + +The project is implemented as a Lamatic AgentKit workflow with a clear, sequential agent chain. + +The system follows a sequential multi-agent architecture where each agent performs a single responsibility and appends its analysis to a structured JSON output. This separation of concerns improves maintainability, traceability, and extensibility. + +Each agent receives the previous agent's JSON output, appends its own analysis, and passes the enriched result to the next stage. + +![Architecture](assets/diagrams/architecture.svg) + +Current stack information reflected in the project is: + +- Lamatic AgentKit +- React +- TypeScript +- JSON Schema + +## Agent Pipeline + +```text +SQL Migration +↓ +Migration Understanding Agent +↓ +Behavior Analysis Agent +↓ +Deployment Strategy Agent +↓ +Release Decision & Rollback Advisor +↓ +Final Release Report +``` + +## Project Structure + +```text +production-database-release-planner/ +├── apps/ +├── assets/ +│ ├── diagrams/ +│ └── screenshots/ +├── docs/ +├── examples/ +│ ├── input/ +│ └── expected-output/ +├── flows/ +├── prompts/ +├── schemas/ +│ ├── migration-understanding.schema.json +│ ├── behavior-analysis.schema.json +│ ├── deployment-strategy.schema.json +│ └── release-plan.schema.json +├── lamatic.config.ts +└── README.md +``` + +## Example Workflow + +1. Provide an SQL migration input file in examples/input/. +2. Run the migration through the agent pipeline. +3. Review the generated outputs and release recommendation. +4. Compare the result with the reference output in examples/expected-output/. + +## Documentation + +Available documentation: + +- docs/architecture.md +- docs/pipeline.md +- docs/design-decisions.md +- docs/roadmap.md + +Examples directory: + +```text +examples/ +├── input/ +└── expected-output/ +``` + +## Status + +🚧 Active Development + +Current focus: + +- Refining agent prompts +- Improving evaluation accuracy +- Expanding migration test cases +- Enhancing documentation + +## Future Work + +- Support additional database engines beyond PostgreSQL. +- Expand migration pattern coverage. +- Improve agent evaluation accuracy. +- Add automated schema validation. +- Introduce policy-based release approval. +- Support additional database engines (MySQL, SQLite, SQL Server). +- Integrate automated migration validation and policy enforcement. + +## License + +MIT \ No newline at end of file diff --git a/kits/production-database-release-planner/apps/.env.example b/kits/production-database-release-planner/apps/.env.example new file mode 100644 index 000000000..44caa26a5 --- /dev/null +++ b/kits/production-database-release-planner/apps/.env.example @@ -0,0 +1,18 @@ +# Copy this file to .env.local and fill in the real Lamatic values on the server. + +# Paste the Lamatic API key from your Lamatic project settings here. +LAMATIC_API_KEY= + +# Paste the Lamatic project ID for the deployed flow here. +LAMATIC_PROJECT_ID= + +# Paste the Lamatic workflow/flow ID for the deployed release safety pipeline here. +LAMATIC_FLOW_ID= + +# Paste the Lamatic-generated API URL here, for example: +# https://tiyasorganization919-tiyasproject663.lamatic.dev +LAMATIC_API_URL= + +# Optional legacy fallback for older local setups. +# If LAMATIC_API_URL is set, it is used first. +LAMATIC_PROJECT_ENDPOINT= diff --git a/kits/production-database-release-planner/apps/.gitignore b/kits/production-database-release-planner/apps/.gitignore new file mode 100644 index 000000000..38138c6aa --- /dev/null +++ b/kits/production-database-release-planner/apps/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +.env.local +*.tsbuildinfo diff --git a/kits/production-database-release-planner/apps/app/api/analyze-migration/route.ts b/kits/production-database-release-planner/apps/app/api/analyze-migration/route.ts new file mode 100644 index 000000000..2019e0324 --- /dev/null +++ b/kits/production-database-release-planner/apps/app/api/analyze-migration/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; + +import { runLamaticMigrationAnalysis } from "@/lib/lamatic"; + +export const runtime = "nodejs"; +export const maxDuration = 60; + +export async function POST(request: Request) { + try { + const body = (await request.json().catch(() => null)) as { + sql?: unknown; + } | null; + + const sql = typeof body?.sql === "string" ? body.sql.trim() : ""; + + if (!sql) { + return NextResponse.json({ error: "SQL is required." }, { status: 400 }); + } + + const analysis = await runLamaticMigrationAnalysis(sql); + + return NextResponse.json(analysis); + } catch (error) { + const message = error instanceof Error ? error.message : "Migration analysis failed."; + + return NextResponse.json({ error: message }, { status: 502 }); + } +} diff --git a/kits/production-database-release-planner/apps/app/globals.css b/kits/production-database-release-planner/apps/app/globals.css new file mode 100644 index 000000000..8b66997c6 --- /dev/null +++ b/kits/production-database-release-planner/apps/app/globals.css @@ -0,0 +1,148 @@ +@import "tailwindcss"; + +:root { + --bg-canvas: #f4f6fb; + --bg-canvas-soft: #eef2f8; + --bg-panel: rgba(255, 255, 255, 0.88); + --bg-panel-strong: #ffffff; + --bg-editor: #f8fafc; + --bg-editor-muted: #f1f5f9; + --border-subtle: rgba(15, 23, 42, 0.08); + --border-strong: rgba(37, 99, 235, 0.18); + --text-primary: #111827; + --text-secondary: #475569; + --text-muted: #6b7280; + --accent-blue: #2563eb; + --accent-blue-soft: #dbeafe; + --accent-green: #15803d; + --accent-amber: #c2410c; + --accent-red: #b91c1c; + --shadow-panel: 0 18px 60px rgba(15, 23, 42, 0.08); + --shadow-soft: 0 8px 24px rgba(15, 23, 42, 0.06); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at top left, rgba(96, 165, 250, 0.18), transparent 24%), + radial-gradient(circle at top right, rgba(191, 219, 254, 0.55), transparent 30%), + linear-gradient(180deg, #f8fafc 0%, #f2f5fb 42%, #edf2f8 100%); + color: var(--text-primary); + font-family: var(--font-manrope), sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +::selection { + background: rgba(37, 99, 235, 0.16); +} + +::-webkit-scrollbar { + width: 9px; + height: 9px; +} + +::-webkit-scrollbar-track { + background: rgba(148, 163, 184, 0.08); +} + +::-webkit-scrollbar-thumb { + border-radius: 999px; + background: rgba(100, 116, 139, 0.34); +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.48); +} + +.grid-overlay { + background-image: + linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px); + background-size: 52px 52px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.18), transparent 88%); +} + +.panel { + border: 1px solid var(--border-subtle); + background: var(--bg-panel); + backdrop-filter: blur(16px); + box-shadow: var(--shadow-panel); +} + +.panel-strong { + border: 1px solid rgba(15, 23, 42, 0.07); + background: var(--bg-panel-strong); + box-shadow: var(--shadow-soft); +} + +.code-font { + font-family: var(--font-ibm-plex-mono), monospace; +} + +.soft-ring { + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.7), + 0 0 0 1px rgba(15, 23, 42, 0.02); +} + +.surface-glow { + position: relative; + isolation: isolate; +} + +.surface-glow::before { + content: ""; + position: absolute; + inset: -1px; + z-index: -1; + border-radius: inherit; + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.95), + rgba(219, 234, 254, 0.92) 55%, + rgba(255, 255, 255, 0.72) + ); +} + +.spin-slow { + animation: spin-slow 14s linear infinite; +} + +@keyframes spin-slow { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +@keyframes pulse-signal { + 0%, + 100% { + opacity: 0.5; + transform: scale(1); + } + + 50% { + opacity: 1; + transform: scale(1.08); + } +} + +.pulse-signal { + animation: pulse-signal 1.8s ease-in-out infinite; +} diff --git a/kits/production-database-release-planner/apps/app/layout.tsx b/kits/production-database-release-planner/apps/app/layout.tsx new file mode 100644 index 000000000..47ac1dcff --- /dev/null +++ b/kits/production-database-release-planner/apps/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from "next"; +import { IBM_Plex_Mono, Manrope } from "next/font/google"; +import "./globals.css"; + +const manrope = Manrope({ + subsets: ["latin"], + variable: "--font-manrope", +}); + +const ibmPlexMono = IBM_Plex_Mono({ + subsets: ["latin"], + variable: "--font-ibm-plex-mono", + weight: ["400", "500"], +}); + +export const metadata: Metadata = { + title: "Production Database Release Planner | Lamatic AgentKit", + description: + "Frontend workspace for staging SQL migration input before Lamatic AgentKit release safety analysis.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/kits/production-database-release-planner/apps/app/page.tsx b/kits/production-database-release-planner/apps/app/page.tsx new file mode 100644 index 000000000..fcae8e617 --- /dev/null +++ b/kits/production-database-release-planner/apps/app/page.tsx @@ -0,0 +1,52 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import Header from "@/components/Header"; +import ReleasePlanner from "@/components/ReleasePlanner"; +import { + initialSampleFile, + presetDefinitions, + type SqlPreset, +} from "@/lib/presets"; + +const fallbackSql = `ALTER TABLE users +ADD COLUMN last_seen TIMESTAMP;`; + +async function readExampleSql(filename: string) { + const filePath = path.join( + process.cwd(), + "..", + "examples", + "input", + filename, + ); + + return readFile(filePath, "utf8"); +} + +export default async function Home() { + const presets: SqlPreset[] = await Promise.all( + presetDefinitions.map(async (preset) => ({ + ...preset, + sql: await readExampleSql(preset.filename), + })), + ); + + const initialSql = + (await readExampleSql(initialSampleFile).catch(() => null)) ?? + presets[0]?.sql ?? + fallbackSql; + + return ( +
+
+
+
+
+ +
+
+ +
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/Header.tsx b/kits/production-database-release-planner/apps/components/Header.tsx new file mode 100644 index 000000000..96dd49c43 --- /dev/null +++ b/kits/production-database-release-planner/apps/components/Header.tsx @@ -0,0 +1,38 @@ +import { Circle, Database, Workflow } from "lucide-react"; + +export default function Header() { + return ( +
+
+
+
+ +
+ +
+
+

+ Production Database Release Planner +

+ + + Lamatic AgentKit Pipeline + +
+ +

+ Analyze. Plan. Release Safely. +

+
+
+ +
+ + Pipeline Ready +
+
+ +
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/MigrationInput.tsx b/kits/production-database-release-planner/apps/components/MigrationInput.tsx new file mode 100644 index 000000000..6826ab0b6 --- /dev/null +++ b/kits/production-database-release-planner/apps/components/MigrationInput.tsx @@ -0,0 +1,64 @@ +import type { SqlPreset } from "@/lib/presets"; +import PresetButtons from "./PresetButtons"; +import RunPipelineButton from "./RunPipelineButton"; +import SqlEditor from "./SqlEditor"; + +type MigrationInputProps = { + isPresetPending: boolean; + isStarting: boolean; + onPresetSelect: (presetId: string) => void; + onRun: () => void; + onSqlChange: (value: string) => void; + presets: SqlPreset[]; + selectedPresetId: string | null; + sql: string; + validationMessage: string | null; +}; + +export default function MigrationInput({ + isPresetPending, + isStarting, + onPresetSelect, + onRun, + onSqlChange, + presets, + selectedPresetId, + sql, + validationMessage, +}: MigrationInputProps) { + return ( +
+
+
+

+ Migration Input +

+

+ Review and analyze a PostgreSQL DDL migration before production + release. +

+
+ + + + + + +
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/PipelineExecution.tsx b/kits/production-database-release-planner/apps/components/PipelineExecution.tsx new file mode 100644 index 000000000..7389b2eac --- /dev/null +++ b/kits/production-database-release-planner/apps/components/PipelineExecution.tsx @@ -0,0 +1,84 @@ +import { ArrowRight, LoaderCircle } from "lucide-react"; +import type { PipelineNodeState } from "@/lib/pipeline"; + +type PipelineExecutionProps = { + nodes: PipelineNodeState[]; +}; + +export default function PipelineExecution({ + nodes, +}: PipelineExecutionProps) { + return ( +
+
+

+ Pipeline Execution +

+

+ Four sequential AgentKit orchestration nodes. +

+
+ +
+
+ {nodes.map((node, index) => ( +
+
+
+

+ {node.label} +

+ + {node.status === "RUNNING" ? ( + + ) : null} + {node.status} + +
+

+ {node.title} +

+

+ {node.summary ?? ""} +

+
+ + {index < nodes.length - 1 ? ( +
+ +
+ ) : null} +
+ ))} +
+ +

+ Visual execution indicator for the pipeline run. +

+
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/PresetButtons.tsx b/kits/production-database-release-planner/apps/components/PresetButtons.tsx new file mode 100644 index 000000000..ee3ce813d --- /dev/null +++ b/kits/production-database-release-planner/apps/components/PresetButtons.tsx @@ -0,0 +1,73 @@ +import { Database } from "lucide-react"; +import type { SqlPreset } from "@/lib/presets"; + +type PresetButtonsProps = { + isPending: boolean; + onSelect: (presetId: string) => void; + presets: SqlPreset[]; + selectedPresetId: string | null; +}; + +const accentStyles = { + amber: + "border-slate-200 bg-white text-slate-800 hover:border-orange-300 hover:bg-orange-50/70", + indigo: + "border-slate-200 bg-white text-slate-800 hover:border-blue-300 hover:bg-blue-50/70", + rose: + "border-slate-200 bg-white text-slate-800 hover:border-rose-300 hover:bg-rose-50/70", + sky: + "border-slate-200 bg-white text-slate-800 hover:border-sky-300 hover:bg-sky-50/70", +} as const; + +export default function PresetButtons({ + isPending, + onSelect, + presets, + selectedPresetId, +}: PresetButtonsProps) { + return ( +
+
+
+

+ Preset DDL +

+

+ Load one of the existing example migrations into the editor. +

+
+
+ + Examples from `examples/input/` +
+
+ +
+ {presets.map((preset) => { + const isSelected = preset.id === selectedPresetId; + + return ( + + ); + })} +
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/ReleasePlanner.tsx b/kits/production-database-release-planner/apps/components/ReleasePlanner.tsx new file mode 100644 index 000000000..383e7bd3c --- /dev/null +++ b/kits/production-database-release-planner/apps/components/ReleasePlanner.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useEffect, useRef, useState, useTransition } from "react"; +import MigrationInput from "@/components/MigrationInput"; +import PipelineExecution from "@/components/PipelineExecution"; +import ResultsDashboard from "@/components/ResultsDashboard"; +import type { MigrationPipelineResult } from "@/types/migrationPipeline"; +import { + createIdlePipelineNodes, + setPipelineNodeState, + type PipelineNodeState, +} from "@/lib/pipeline"; +import { buildPipelineSummaries } from "@/lib/pipeline"; +import { runMigrationPipeline } from "@/services/migrationPipeline"; +import type { SqlPreset } from "@/lib/presets"; + +type RunState = "IDLE" | "RUNNING" | "COMPLETED" | "ERROR"; + +type ReleasePlannerProps = { + initialSql: string; + presets: SqlPreset[]; +}; + +export default function ReleasePlanner({ + initialSql, + presets, +}: ReleasePlannerProps) { + const [sql, setSql] = useState(initialSql); + const [selectedPresetId, setSelectedPresetId] = useState(null); + const [runState, setRunState] = useState("IDLE"); + const [result, setResult] = useState(null); + const [pipelineNodes, setPipelineNodes] = useState( + () => createIdlePipelineNodes(), + ); + const [validationMessage, setValidationMessage] = useState(null); + const [resultsViewKey, setResultsViewKey] = useState(0); + const timeoutRefs = useRef([]); + const executionIdRef = useRef(0); + const currentNodeIndexRef = useRef(0); + const [isPresetPending, startPresetTransition] = useTransition(); + + useEffect(() => { + return () => { + executionIdRef.current += 1; + timeoutRefs.current.forEach((timeoutId) => window.clearTimeout(timeoutId)); + timeoutRefs.current = []; + }; + }, []); + + const clearPendingExecution = () => { + executionIdRef.current += 1; + timeoutRefs.current.forEach((timeoutId) => window.clearTimeout(timeoutId)); + timeoutRefs.current = []; + }; + + const resetPrototypeState = () => { + clearPendingExecution(); + currentNodeIndexRef.current = 0; + setRunState("IDLE"); + setResult(null); + setPipelineNodes(createIdlePipelineNodes()); + setValidationMessage(null); + setResultsViewKey((current) => current + 1); + }; + + const wait = (durationMs: number) => + new Promise((resolve) => { + const timeoutId = window.setTimeout(() => { + timeoutRefs.current = timeoutRefs.current.filter((id) => id !== timeoutId); + resolve(); + }, durationMs); + + timeoutRefs.current.push(timeoutId); + }); + + const handleSqlChange = (nextSql: string) => { + setSql(nextSql); + resetPrototypeState(); + + if (!selectedPresetId) { + return; + } + + const selectedPreset = presets.find((preset) => preset.id === selectedPresetId); + + if (selectedPreset && selectedPreset.sql !== nextSql) { + setSelectedPresetId(null); + } + }; + + const handlePresetSelect = (presetId: string) => { + const preset = presets.find((item) => item.id === presetId); + + if (!preset) { + return; + } + + startPresetTransition(() => { + resetPrototypeState(); + setSql(preset.sql); + setSelectedPresetId(preset.id); + }); + }; + + const handleRun = async () => { + const trimmedSql = sql.trim(); + + if (!trimmedSql) { + setValidationMessage("Enter a migration before running the pipeline."); + return; + } + + resetPrototypeState(); + setRunState("RUNNING"); + const currentExecutionId = executionIdRef.current; + + const animationPromise = (async () => { + for (let nodeIndex = 0; nodeIndex < 4; nodeIndex += 1) { + if (executionIdRef.current !== currentExecutionId) { + return false; + } + + currentNodeIndexRef.current = nodeIndex; + setPipelineNodes((currentNodes) => + setPipelineNodeState(currentNodes, nodeIndex, "RUNNING", null), + ); + + await wait(700); + + if (executionIdRef.current !== currentExecutionId) { + return false; + } + + setPipelineNodes((currentNodes) => + setPipelineNodeState(currentNodes, nodeIndex, "COMPLETED", null), + ); + } + + return true; + })(); + + try { + const pipelineResult = await runMigrationPipeline(sql); + + const animationCompleted = await animationPromise; + + if (executionIdRef.current !== currentExecutionId || !animationCompleted) { + return; + } + + const nodeSummaries = buildPipelineSummaries(pipelineResult); + + setPipelineNodes((currentNodes) => + currentNodes.map((node, nodeIndex) => ({ + ...node, + status: "COMPLETED", + summary: nodeSummaries[nodeIndex] ?? node.summary, + })), + ); + + setResult(pipelineResult); + setRunState("COMPLETED"); + setValidationMessage(null); + } catch (error) { + clearPendingExecution(); + + setPipelineNodes((currentNodes) => + currentNodes.map((node, nodeIndex) => { + if (nodeIndex < currentNodeIndexRef.current) { + return { + ...node, + status: "COMPLETED", + }; + } + + if (nodeIndex === currentNodeIndexRef.current) { + return { + ...node, + status: "ERROR", + }; + } + + return { + ...node, + status: "IDLE", + summary: null, + }; + }), + ); + + setResult(null); + setRunState("ERROR"); + setValidationMessage( + error instanceof Error + ? error.message + : "Pipeline execution failed. No release decision was generated.", + ); + + console.error("Release planner execution failed", error); + } + }; + + return ( +
+ + + +
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/ResultsDashboard.tsx b/kits/production-database-release-planner/apps/components/ResultsDashboard.tsx new file mode 100644 index 000000000..6c33026d0 --- /dev/null +++ b/kits/production-database-release-planner/apps/components/ResultsDashboard.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState } from "react"; +import { FileText } from "lucide-react"; +import type { + ConfidenceLevel, + MigrationPipelineResult, + ReleaseStatus, + RiskLevel, + StrategyType, +} from "@/types/migrationPipeline"; +import DeploymentStrategyTab from "@/components/results/DeploymentStrategyTab"; +import IntentScopeTab from "@/components/results/IntentScopeTab"; +import ReleaseRollbackTab from "@/components/results/ReleaseRollbackTab"; +import ResultSummary from "@/components/results/ResultSummary"; +import RiskAnalysisTab from "@/components/results/RiskAnalysisTab"; + +type ResultsDashboardProps = { + result: MigrationPipelineResult | null; +}; + +type ResultTab = "intent" | "risk" | "strategy" | "release"; + +const tabs: Array<{ id: ResultTab; label: string }> = [ + { id: "intent", label: "Intent & Scope" }, + { id: "risk", label: "Lock & Risk Analysis" }, + { id: "strategy", label: "Deployment Strategy" }, + { id: "release", label: "Release & Rollback" }, +]; + +const riskToneMap: Record = { + LOW: "green", + MEDIUM: "amber", + HIGH: "red", + UNKNOWN: "gray", +}; + +const statusToneMap: Record = { + APPROVE: "green", + APPROVE_WITH_CAUTION: "amber", + REJECT: "red", +}; + +const confidenceToneMap: Record = { + LOW: "gray", + MEDIUM: "amber", + HIGH: "green", +}; + +const strategyToneMap: Record = { + DIRECT_MIGRATION: "green", + ONLINE_MIGRATION: "green", + PHASED_ROLLOUT: "amber", + EXPAND_CONTRACT: "amber", +}; + +function EmptyState() { + return ( +
+
+

+ Migration Safety Report +

+

+ Operational risk, deployment strategy, and release readiness for the analyzed migration. +

+
+ +
+
+ +
+

+ No migration analysis available +

+

+ Run the pipeline to generate a release safety report. +

+
+
+ ); +} + +export default function ResultsDashboard({ result }: ResultsDashboardProps) { + const [activeTab, setActiveTab] = useState("intent"); + + if (!result) { + return ; + } + + const releaseStatus = result.release_plan.release_decision.status; + const confidence = result.release_plan.release_decision.confidence; + const rollbackPossible = result.release_plan.rollback_strategy.rollback_possible; + + return ( +
+
+

+ Migration Safety Report +

+

+ Operational risk, deployment strategy, and release readiness for the analyzed migration. +

+
+ +
+ + +
+ {tabs.map((tab) => { + const isActive = activeTab === tab.id; + + return ( + + ); + })} +
+ + {activeTab === "intent" ? ( + + ) : null} + + {activeTab === "risk" ? ( + + ) : null} + + {activeTab === "strategy" ? ( + + ) : null} + + {activeTab === "release" ? ( + + ) : null} +
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/RunPipelineButton.tsx b/kits/production-database-release-planner/apps/components/RunPipelineButton.tsx new file mode 100644 index 000000000..2c3ab05a7 --- /dev/null +++ b/kits/production-database-release-planner/apps/components/RunPipelineButton.tsx @@ -0,0 +1,46 @@ +import { LoaderCircle, Play } from "lucide-react"; + +type RunPipelineButtonProps = { + isLoading: boolean; + onClick: () => void; + validationMessage: string | null; +}; + +export default function RunPipelineButton({ + isLoading, + onClick, + validationMessage, +}: RunPipelineButtonProps) { + return ( +
+
+ + + {validationMessage ? ( +

{validationMessage}

+ ) : null} +
+ +
+

SQL is sent to the pipeline adapter; no database statements are executed here.

+
+
+ ); +} diff --git a/kits/production-database-release-planner/apps/components/SqlEditor.tsx b/kits/production-database-release-planner/apps/components/SqlEditor.tsx new file mode 100644 index 000000000..1d4570c56 --- /dev/null +++ b/kits/production-database-release-planner/apps/components/SqlEditor.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useId, useRef } from "react"; +import { FileCode2 } from "lucide-react"; + +type SqlEditorProps = { + onChange: (value: string) => void; + placeholder: string; + value: string; +}; + +export default function SqlEditor({ + onChange, + placeholder, + value, +}: SqlEditorProps) { + const editorId = useId(); + const gutterRef = useRef(null); + const lineCount = Math.max(12, value.split("\n").length); + const lineNumbers = Array.from({ length: lineCount }, (_, index) => index + 1); + + return ( +
+
+
+ + Migration.sql + +
+ +
+ + postgresql +
+
+ + + +
+ + +