Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions components/backend/handlers/github_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -209,6 +210,20 @@ func HandleGitHubUserOAuthCallback(c *gin.Context) {
if retURL == "" {
retURL = "/integrations"
}

// Check if we need to preserve onboarding parameter
onboarding := c.Query("onboarding")
if onboarding != "" {
// Parse the URL to add query parameter properly
parsedURL, err := url.Parse(retURL)
if err == nil {
q := parsedURL.Query()
q.Set("onboarding", onboarding)
parsedURL.RawQuery = q.Encode()
retURL = parsedURL.String()
}
}

c.Redirect(http.StatusFound, retURL)
}

Expand Down
2 changes: 1 addition & 1 deletion components/frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# GitHub App identifier used to initiate installation
# This may be your GitHub App slug or Client ID, depending on your setup
GITHUB_APP_SLUG=ambient-code-vteam
NEXT_PUBLIC_GITHUB_APP_SLUG=ambient-code-local

# Direct backend base URL (used by server-side code where applicable)
# Default local backend URL
Expand Down
2 changes: 2 additions & 0 deletions components/frontend/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXT_PUBLIC_GITHUB_APP_SLUG=ambient-code-local
NEXT_PUBLIC_VTEAM_VERSION=v0.0.3
1 change: 0 additions & 1 deletion components/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]
13 changes: 13 additions & 0 deletions components/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ npm run dev
# Open http://localhost:3000
```

### Mock Mode (No Backend Required)
npm ci
npm run dev
# Open http://localhost:3000
```

**Mock Mode Features:**
- Pre-populated projects, sessions, and RFE workflows
- Auto-logged in as `developer` user with admin permissions
- GitHub integration pre-connected with mock repositories
- Full onboarding wizard support
- Configurable network delay simulation

### Header forwarding model (dev and prod)
Next.js API routes forward incoming headers to the backend. They do not auto-inject user identity. In development, you can optionally provide values via environment or `oc`:

Expand Down
2 changes: 1 addition & 1 deletion components/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "NEXT_PUBLIC_USE_MOCKS=true NEXT_PUBLIC_MOCK_DELAY=300 GITHUB_APP_SLUG=ambient-code NEXT_PUBLIC_LOG_MOCKS=true next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
Expand Down
7 changes: 7 additions & 0 deletions components/frontend/src/app/api/auth/github/install/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'
import { USE_MOCKS } from '@/lib/mock-config'
import { handleInstallGitHub } from '@/lib/mocks/handlers'

export async function POST(request: Request) {
// Return mock data if enabled
if (USE_MOCKS) {
return handleInstallGitHub(request);
}

const headers = await buildForwardHeadersAsync(request)
const body = await request.text()

Expand Down
7 changes: 7 additions & 0 deletions components/frontend/src/app/api/auth/github/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'
import { USE_MOCKS } from '@/lib/mock-config'
import { handleGetGitHubStatus } from '@/lib/mocks/handlers'

export async function GET(request: Request) {
// Return mock data if enabled
if (USE_MOCKS) {
return handleGetGitHubStatus();
}

const headers = await buildForwardHeadersAsync(request)

const resp = await fetch(`${BACKEND_URL}/auth/github/status`, {
Expand Down
7 changes: 7 additions & 0 deletions components/frontend/src/app/api/cluster-info/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { BACKEND_URL } from '@/lib/config';
import { USE_MOCKS } from '@/lib/mock-config';
import { handleGetClusterInfo } from '@/lib/mocks/handlers';

/**
* GET /api/cluster-info
* Returns cluster information (OpenShift vs vanilla Kubernetes)
* This endpoint does not require authentication as it's public cluster information
*/
export async function GET() {
// Return mock data if enabled
if (USE_MOCKS) {
return handleGetClusterInfo();
}

try {
const response = await fetch(`${BACKEND_URL}/cluster-info`, {
method: 'GET',
Expand Down
7 changes: 7 additions & 0 deletions components/frontend/src/app/api/me/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { buildForwardHeadersAsync } from '@/lib/auth';
import { USE_MOCKS } from '@/lib/mock-config';
import { handleGetMe } from '@/lib/mocks/handlers';

export async function GET(request: Request) {
// Return mock data if enabled
if (USE_MOCKS) {
return handleGetMe();
}

try {
// Use the shared helper so dev oc whoami and env fallbacks apply uniformly
const headers = await buildForwardHeadersAsync(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { BACKEND_URL } from '@/lib/config';
import { buildForwardHeadersAsync } from '@/lib/auth';
import { USE_MOCKS } from '@/lib/mock-config';
import { handleListSessions } from '@/lib/mocks/handlers';

// GET /api/projects/[name]/agentic-sessions - List sessions in a project
export async function GET(
request: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params;

// Return mock data if enabled
if (USE_MOCKS) {
return handleListSessions(name);
}

try {
const { name } = await params;
const headers = await buildForwardHeadersAsync(request);
const response = await fetch(`${BACKEND_URL}/projects/${encodeURIComponent(name)}/agentic-sessions`, { headers });
const text = await response.text();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { BACKEND_URL } from '@/lib/config'
import { buildForwardHeadersAsync } from '@/lib/auth'
import { USE_MOCKS } from '@/lib/mock-config'
import { handleListRFEWorkflows } from '@/lib/mocks/handlers'

export async function GET(
request: Request,
{ params }: { params: Promise<{ name: string }> },
) {
const { name } = await params

// Return mock data if enabled
if (USE_MOCKS) {
return handleListRFEWorkflows(name);
}

const headers = await buildForwardHeadersAsync(request)
const resp = await fetch(`${BACKEND_URL}/projects/${encodeURIComponent(name)}/rfe-workflows`, { headers })
const data = await resp.text()
Expand Down
18 changes: 16 additions & 2 deletions components/frontend/src/app/api/projects/[name]/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { BACKEND_URL } from '@/lib/config';
import { buildForwardHeadersAsync } from '@/lib/auth';
import { USE_MOCKS } from '@/lib/mock-config';
import { handleGetProject, handleDeleteProject } from '@/lib/mocks/handlers';

// GET /api/projects/[name] - Get project by name
export async function GET(
request: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params;

// Return mock data if enabled
if (USE_MOCKS) {
return handleGetProject(name);
}

try {
const { name } = await params;
const headers = await buildForwardHeadersAsync(request);

const response = await fetch(`${BACKEND_URL}/projects/${name}`, { headers });
Expand Down Expand Up @@ -56,8 +64,14 @@ export async function DELETE(
request: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params;

// Return mock data if enabled
if (USE_MOCKS) {
return handleDeleteProject(name);
}

try {
const { name } = await params;
const headers = await buildForwardHeadersAsync(request);

const response = await fetch(`${BACKEND_URL}/projects/${name}`, {
Expand Down
12 changes: 12 additions & 0 deletions components/frontend/src/app/api/projects/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { NextRequest, NextResponse } from "next/server";
import { BACKEND_URL } from "@/lib/config";
import { buildForwardHeadersAsync } from "@/lib/auth";
import { USE_MOCKS } from "@/lib/mock-config";
import { handleListProjects, handleCreateProject } from "@/lib/mocks/handlers";

export async function GET(request: NextRequest) {
// Return mock data if enabled
if (USE_MOCKS) {
return handleListProjects();
}

try {
const headers = await buildForwardHeadersAsync(request);

Expand Down Expand Up @@ -30,6 +37,11 @@ export async function GET(request: NextRequest) {
}

export async function POST(request: NextRequest) {
// Return mock data if enabled
if (USE_MOCKS) {
return handleCreateProject(request);
}

try {
const body = await request.text();

Expand Down
7 changes: 7 additions & 0 deletions components/frontend/src/app/api/version/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { env } from '@/lib/env';
import { USE_MOCKS } from '@/lib/mock-config';
import { handleGetVersion } from '@/lib/mocks/handlers';

export async function GET() {
// Return mock data if enabled
if (USE_MOCKS) {
return handleGetVersion();
}

return Response.json({
version: env.VTEAM_VERSION,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function IntegrationsClient({ appSlug }: Props) {
const handleDisconnect = async () => {
disconnectMutation.mutate(undefined, {
onSuccess: () => {
// TODO actually invalidate the github application remotely
successToast('GitHub disconnected successfully')
refetch()
},
Expand Down
62 changes: 44 additions & 18 deletions components/frontend/src/app/integrations/github/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,61 @@ import React, { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { useConnectGitHub } from '@/services/queries'
import { publicEnv } from '@/lib/env'

export default function GitHubSetupPage() {
const [message, setMessage] = useState<string>('Finalizing GitHub connection...')
const [message, setMessage] = useState<string>('Preparing GitHub connection...')
const [error, setError] = useState<string | null>(null)
const connectMutation = useConnectGitHub()

useEffect(() => {
const url = new URL(window.location.href)
const installationId = url.searchParams.get('installation_id')
const fromOnboarding = url.searchParams.get('from') === 'onboarding'

if (!installationId) {
setMessage('No installation was detected.')
return
}
// If returning from GitHub with installation_id, complete the connection
if (installationId) {
setMessage('Finalizing GitHub connection...')
connectMutation.mutate(
{ installationId: Number(installationId) },
{
onSuccess: () => {
setMessage('GitHub connected successfully! Redirecting...')
setTimeout(() => {
// Redirect back to onboarding or integrations page
window.location.replace(fromOnboarding ? '/onboarding' : '/integrations')
}, 800)
},
onError: (err) => {
setError(err instanceof Error ? err.message : 'Failed to complete setup')
},
}
)
} else {
// No installation_id, redirect to GitHub OAuth
const appSlug = publicEnv.GITHUB_APP_SLUG

if (!appSlug) {
setError('GitHub App is not configured. Please contact your administrator.')
return
}

connectMutation.mutate(
{ installationId: Number(installationId) },
{
onSuccess: () => {
setMessage('GitHub connected. Redirecting...')
setTimeout(() => {
window.location.replace('/integrations')
}, 800)
},
onError: (err) => {
setError(err instanceof Error ? err.message : 'Failed to complete setup')
},
setMessage('Redirecting to GitHub...')

// Build the redirect URI with from parameter to return to the right place
const setupUrl = new URL('/integrations/github/setup', window.location.origin)
if (fromOnboarding) {
setupUrl.searchParams.set('from', 'onboarding')
}
)
const redirectUri = encodeURIComponent(setupUrl.toString())

// Redirect to GitHub OAuth
const githubUrl = `https://github.com/apps/${appSlug}/installations/new?redirect_uri=${redirectUri}`

setTimeout(() => {
window.location.href = githubUrl
}, 500)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

Expand Down
40 changes: 40 additions & 0 deletions components/frontend/src/app/onboarding/OnboardingClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import { useProjects } from '@/services/queries';
import { OnboardingWizard } from './components/OnboardingWizard';
import { Skeleton } from '@/components/ui/skeleton';

export default function OnboardingClient() {
const { data: projects, isLoading } = useProjects();

// If user already has projects, allow them to skip onboarding
const canSkip = !isLoading && (projects?.length ?? 0) > 0;

// Show loading state while checking projects
if (isLoading) {
return (
<div className="min-h-screen bg-background">
<div className="container max-w-3xl mx-auto px-4 py-8">
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-4 w-96" />
</div>
<div className="space-y-4">
<Skeleton className="h-2 w-full" />
<div className="flex gap-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-16 flex-1" />
))}
</div>
</div>
<Skeleton className="h-96 w-full" />
</div>
</div>
</div>
);
}

return <OnboardingWizard canSkip={canSkip} />;
}

Loading
Loading