← back to jerof__job-tracker

Function bodies 234 total

All specs Real LLM only Function bodies
POST function · typescript · L6-L77 (72 LOC)
app/api/applications/fix-status/route.ts
export async function POST(request: NextRequest) {
  try {
    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const { company, role, status } = await request.json();

    if (!company || !status) {
      return NextResponse.json({ error: 'Company and status required' }, { status: 400 });
    }

    // Valid statuses
    const validStatuses = ['saved', 'applied', 'interviewing', 'offer', 'closed'];
    if (!validStatuses.includes(status)) {
      return NextResponse.json({ error: 'Invalid status', validStatuses }, { status: 400 });
    }

    // Find the application
    let query = supabase
      .from('applications')
      .select('id, company, role, status')
      .ilike('company', `%${company}%`);

    if (role) {
      query = query.ilike('role', `%${role}%`);
    }

    const { data: apps } = await query;

    if (!apps || apps.length === 0) {
      return NextResponse.jso
GET function · typescript · L15-L105 (91 LOC)
app/api/applications/[id]/emails/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const { id: applicationId } = await params;

    // First verify the application exists
    const { data: application, error: appError } = await supabase
      .from('applications')
      .select('id')
      .eq('id', applicationId)
      .single();

    if (appError) {
      if (appError.code === 'PGRST116') {
        return NextResponse.json(
          { error: 'Application not found' },
          { status: 404 }
        );
      }
      console.error('Application fetch error:', appError);
      return NextResponse.json(
        { error: 'Failed to fetch application' },
        { status: 500 }
      );
    }

    if (!application) {
      return NextResponse.json(
        { error: 'Applica
GET function · typescript · L7-L38 (32 LOC)
app/api/applications/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const { id } = await params;

    const { data, error } = await supabase
      .from('applications')
      .select('*')
      .eq('id', id)
      .single();

    if (error) {
      if (error.code === 'PGRST116') {
        return NextResponse.json({ error: 'Application not found' }, { status: 404 });
      }
      return NextResponse.json({ error: 'Failed to fetch application' }, { status: 500 });
    }

    return NextResponse.json({ application: dbToApplication(data) });
  } catch (error) {
    console.error('Error fetching application:', error);
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}
PATCH function · typescript · L41-L90 (50 LOC)
app/api/applications/[id]/route.ts
export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const { id } = await params;
    const body = await request.json();

    // Build update object with snake_case keys
    const updateData: Record<string, unknown> = {};
    if (body.company !== undefined) updateData.company = body.company;
    if (body.role !== undefined) updateData.role = body.role;
    if (body.status !== undefined) updateData.status = body.status;
    if (body.closeReason !== undefined) updateData.close_reason = body.closeReason;
    if (body.notes !== undefined) updateData.notes = body.notes;
    if (body.appliedDate !== undefined) updateData.applied_date = body.appliedDate;
    if (body.jobUrl !== undefined) updateData.job_url = body.jobUrl;
    if (body.location !== undefined) updateData.lo
DELETE function · typescript · L93-L120 (28 LOC)
app/api/applications/[id]/route.ts
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const { id } = await params;

    const { error } = await supabase
      .from('applications')
      .delete()
      .eq('id', id);

    if (error) {
      return NextResponse.json({ error: 'Failed to delete application' }, { status: 500 });
    }

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Error deleting application:', error);
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}
GET function · typescript · L20-L75 (56 LOC)
app/api/applications/[id]/tailored-cv/route.ts
export async function GET(
  request: NextRequest,
  context: RouteContext
): Promise<NextResponse> {
  try {
    const { id } = await context.params;

    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Fetch application to get tailored CV info
    const { data: application, error } = await supabase
      .from('applications')
      .select('tailored_cv_url, tailored_cv_filename, tailored_cv_generated_at')
      .eq('id', id)
      .single();

    if (error) {
      if (error.code === 'PGRST116') {
        return NextResponse.json(
          { error: 'Application not found' },
          { status: 404 }
        );
      }
      console.error('Database error:', error);
      return NextResponse.json(
        { error: 'Failed to fetch application' },
        { status: 500 }
      );
    }

    // Return null if no tailored CV exists
    if (!application
POST function · typescript · L81-L271 (191 LOC)
app/api/applications/[id]/tailored-cv/route.ts
export async function POST(
  request: NextRequest,
  context: RouteContext
): Promise<NextResponse> {
  try {
    const { id } = await context.params;

    // Parse request body
    let feedback: string | undefined;
    try {
      const body = await request.json();
      feedback = body?.feedback;
    } catch {
      // Empty body is OK
    }

    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Step 1: Fetch the application
    const { data: application, error: appError } = await supabase
      .from('applications')
      .select('id, company, role, job_url')
      .eq('id', id)
      .single();

    if (appError) {
      if (appError.code === 'PGRST116') {
        return NextResponse.json(
          { error: 'Application not found' },
          { status: 404 }
        );
      }
      console.error('Database error:', appError);
      return NextRes
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
GET function · typescript · L7-L46 (40 LOC)
app/api/applications/route.ts
export async function GET() {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured', applications: [], stats: { total: 0, responseRate: 0 } },
        { status: 503 }
      );
    }

    // For now, get all applications (auth will be added later)
    // Order by created_at since saved jobs may not have applied_date
    const { data, error } = await supabase
      .from('applications')
      .select('*')
      .order('created_at', { ascending: false });

    if (error) {
      console.error('Database error:', error);
      return NextResponse.json({ error: 'Failed to fetch applications' }, { status: 500 });
    }

    const applications = (data || []).map(dbToApplication);

    // Calculate stats (exclude saved jobs - they're not applications yet)
    const applied = applications.filter(a => a.status !== 'saved');
    const total = applied.length;
    const responded = applied.filter(a => a.sta
POST function · typescript · L49-L83 (35 LOC)
app/api/applications/route.ts
export async function POST(request: NextRequest) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const body = await request.json();

    const { data, error } = await supabase
      .from('applications')
      .insert(applicationToDb(body))
      .select()
      .single();

    if (error) {
      console.error('Database error:', error);
      if (error.code === '23505') {
        return NextResponse.json({ error: 'Application already exists' }, { status: 409 });
      }
      return NextResponse.json({ error: 'Failed to create application' }, { status: 500 });
    }

    // Auto-generate CV if job URL provided
    if (body.jobUrl) {
      triggerCVGeneration(data.id);
    }

    return NextResponse.json({ application: dbToApplication(data) }, { status: 201 });
  } catch (error) {
    console.error('Error creating application:', error);
    return NextResponse.jso
GET function · typescript · L6-L54 (49 LOC)
app/api/auth/gmail/callback/route.ts
export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const code = searchParams.get('code');
  const error = searchParams.get('error');

  if (error) {
    console.error('OAuth error:', error);
    return NextResponse.redirect(new URL('/?error=oauth_denied', request.url));
  }

  if (!code) {
    return NextResponse.redirect(new URL('/?error=no_code', request.url));
  }

  try {
    // Exchange code for tokens
    const tokens = await getTokensFromCode(code);

    if (!tokens.access_token || !tokens.refresh_token) {
      throw new Error('Missing tokens in response');
    }

    // Store tokens in Supabase
    const supabase = createServerClient();
    if (supabase) {
      // For now, store with a placeholder user_id (we'll add proper auth later)
      const { error: dbError } = await supabase
        .from('gmail_tokens')
        .upsert({
          user_id: '00000000-0000-0000-0000-000000000000', // Placeholder
          access_toke
GET function · typescript · L5-L16 (12 LOC)
app/api/auth/gmail/route.ts
export async function GET() {
  try {
    const authUrl = getAuthUrl();
    return NextResponse.redirect(authUrl);
  } catch (error) {
    console.error('Gmail auth error:', error);
    return NextResponse.json(
      { error: 'Gmail authentication not configured' },
      { status: 500 }
    );
  }
}
GET function · typescript · L7-L57 (51 LOC)
app/api/credits/route.ts
export async function GET(request: NextRequest) {
  try {
    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Get user from auth header or cookie
    const authHeader = request.headers.get('authorization');
    let userId: string | null = null;

    if (authHeader?.startsWith('Bearer ')) {
      const token = authHeader.substring(7);
      const { data: { user } } = await supabase.auth.getUser(token);
      userId = user?.id || null;
    }

    // For development/testing, allow a demo user
    if (!userId && process.env.NODE_ENV === 'development') {
      userId = 'demo-user-id';
    }

    if (!userId) {
      return NextResponse.json(
        { error: 'Unauthorized. Please log in.' },
        { status: 401 }
      );
    }

    // Check and reset free credits if eligible (monthly reset)
    await checkAndResetFreeCredits(userId);

    // Get credit 
POST function · typescript · L14-L86 (73 LOC)
app/api/credits/use/route.ts
export async function POST(request: NextRequest) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Get user from auth header
    const authHeader = request.headers.get('authorization');
    let userId: string | null = null;

    if (authHeader?.startsWith('Bearer ')) {
      const token = authHeader.substring(7);
      const { data: { user } } = await supabase.auth.getUser(token);
      userId = user?.id || null;
    }

    // For development/testing, allow a demo user
    if (!userId && process.env.NODE_ENV === 'development') {
      userId = 'demo-user-id';
    }

    if (!userId) {
      return NextResponse.json(
        { error: 'Unauthorized. Please log in.' },
        { status: 401 }
      );
    }

    // Check if user has credits available
    const hasCredits = await canUseCredit(userId);
    if (!hasCredits) {
      return NextR
PUT function · typescript · L6-L56 (51 LOC)
app/api/cv/[id]/route.ts
export async function PUT(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const { id } = await params;
    const body: UpdateCVRequest = await request.json();

    // Build update object
    const updateData: Record<string, unknown> = {
      updated_at: new Date().toISOString(),
    };

    if (body.rawText !== undefined) {
      if (body.rawText.trim().length === 0) {
        return NextResponse.json({ error: 'CV text cannot be empty' }, { status: 400 });
      }
      updateData.raw_text = body.rawText.trim();
    }

    if (body.fileName !== undefined) {
      updateData.file_name = body.fileName;
    }

    const { data, error } = await supabase
      .from('cvs')
      .update(updateData)
      .eq('id', id)
      .select()
      .single();

    if (error) {
      if (error.code ==
DELETE function · typescript · L59-L87 (29 LOC)
app/api/cv/[id]/route.ts
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const { id } = await params;

    const { error } = await supabase
      .from('cvs')
      .delete()
      .eq('id', id);

    if (error) {
      console.error('Database error:', error);
      return NextResponse.json({ error: 'Failed to delete CV' }, { status: 500 });
    }

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Error deleting CV:', error);
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}
Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
POST function · typescript · L19-L122 (104 LOC)
app/api/cv/parse-pdf/route.ts
export async function POST(request: NextRequest): Promise<NextResponse<ParsePDFResponse>> {
  try {
    const formData = await request.formData();
    const file = formData.get('file');

    // Validate file exists
    if (!file || !(file instanceof File)) {
      return NextResponse.json(
        {
          success: false,
          error: 'No file uploaded. Please provide a PDF file.',
        },
        { status: 400 }
      );
    }

    // Validate file type
    const allowedTypes = ['application/pdf'];
    if (!allowedTypes.includes(file.type)) {
      return NextResponse.json(
        {
          success: false,
          error: 'Invalid file type. Please upload a PDF file.',
        },
        { status: 400 }
      );
    }

    // Validate file size
    if (file.size > MAX_FILE_SIZE) {
      return NextResponse.json(
        {
          success: false,
          error: 'File too large. Maximum size is 10MB.',
        },
        { status: 400 }
      );
    }

    // Convert f
GET function · typescript · L6-L40 (35 LOC)
app/api/cv/route.ts
export async function GET() {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured', cv: null },
        { status: 503 }
      );
    }

    // For now, get the first CV (single user mode)
    // TODO: Add proper user authentication
    const { data, error } = await supabase
      .from('cvs')
      .select('*')
      .order('created_at', { ascending: false })
      .limit(1)
      .single();

    if (error) {
      // No CV found is not an error - return null
      if (error.code === 'PGRST116') {
        return NextResponse.json({ cv: null });
      }
      console.error('Database error:', error);
      return NextResponse.json({ error: 'Failed to fetch CV' }, { status: 500 });
    }

    return NextResponse.json({ cv: dbToCV(data) });
  } catch (error) {
    console.error('Error fetching CV:', error);
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
  }
}
POST function · typescript · L43-L83 (41 LOC)
app/api/cv/route.ts
export async function POST(request: NextRequest) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const body: CreateCVRequest = await request.json();

    if (!body.rawText || body.rawText.trim().length === 0) {
      return NextResponse.json({ error: 'CV text is required' }, { status: 400 });
    }

    // Create new CV
    // Using a fixed UUID for single-user mode until auth is implemented
    const cvData = cvToDb({
      userId: '00000000-0000-0000-0000-000000000001', // TODO: Get from auth
      rawText: body.rawText.trim(),
      fileName: body.fileName || null,
      pdfStoragePath: null,
      fileSizeBytes: null,
    });

    const { data, error } = await supabase
      .from('cvs')
      .insert(cvData)
      .select()
      .single();

    if (error) {
      console.error('Database error:', error);
      return NextResponse.json({ error: 'Failed to cre
DELETE function · typescript · L12-L106 (95 LOC)
app/api/cvs/[id]/route.ts
export async function DELETE(
  request: NextRequest,
  context: RouteContext
): Promise<NextResponse> {
  try {
    const { id } = await context.params;

    // Only allow deleting tailored CVs (id format: tailored-{applicationId})
    if (!id.startsWith('tailored-')) {
      return NextResponse.json(
        { error: 'Cannot delete master CV from this endpoint' },
        { status: 400 }
      );
    }

    const applicationId = id.replace('tailored-', '');

    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Get the application to find the storage path
    const { data: application, error: fetchError } = await supabase
      .from('applications')
      .select('tailored_cv_url, tailored_cv_filename')
      .eq('id', applicationId)
      .single();

    if (fetchError) {
      if (fetchError.code === 'PGRST116') {
        return NextResponse.json(
 
GET function · typescript · L9-L81 (73 LOC)
app/api/cvs/route.ts
export async function GET(): Promise<NextResponse> {
  try {
    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const cvs: CV[] = [];

    // Fetch master CV from cvs table
    const { data: masterCV, error: masterError } = await supabase
      .from('cvs')
      .select('id, raw_text, created_at, updated_at')
      .order('created_at', { ascending: false })
      .limit(1)
      .single();

    if (masterError && masterError.code !== 'PGRST116') {
      console.error('Error fetching master CV:', masterError);
    }

    if (masterCV) {
      cvs.push({
        id: masterCV.id,
        type: 'master',
        filename: 'Master CV',
        url: null,
        createdAt: masterCV.created_at,
        updatedAt: masterCV.updated_at || masterCV.created_at,
      });
    }

    // Fetch tailored CVs from applications table
    const { data: applications, erro
GET function · typescript · L5-L19 (15 LOC)
app/api/debug/cvs/route.ts
export async function GET(request: NextRequest) {
  const supabase = createServerClient();
  if (!supabase) {
    return NextResponse.json({ error: 'No DB' }, { status: 503 });
  }

  const company = request.nextUrl.searchParams.get('company') || 'Stripe';

  const { data, error } = await supabase
    .from('applications')
    .select('id, company, role, tailored_cv_url, tailored_cv_filename, tailored_cv_generated_at')
    .ilike('company', `%${company}%`);

  return NextResponse.json({ applications: data, error });
}
GET function · typescript · L5-L52 (48 LOC)
app/api/debug/emails/route.ts
export async function GET(request: NextRequest) {
  try {
    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json({ error: 'Database not configured' }, { status: 503 });
    }

    const company = request.nextUrl.searchParams.get('company');

    if (!company) {
      return NextResponse.json({ error: 'Company required' }, { status: 400 });
    }

    // Get applications
    const { data: apps } = await supabase
      .from('applications')
      .select('id, company, role, status')
      .ilike('company', `%${company}%`);

    if (!apps) {
      return NextResponse.json({ error: 'No applications' }, { status: 404 });
    }

    // Get emails for each application
    const result = [];
    for (const app of apps) {
      const { data: emails } = await supabase
        .from('application_emails')
        .select('id, subject, email_date, email_type')
        .eq('application_id', app.id)
        .order('email_date', { ascending: false });

      res
htmlToText function · typescript · L16-L23 (8 LOC)
app/api/emails/[messageId]/route.ts
function htmlToText(html: string): string {
  return html
    .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
    .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
    .replace(/<[^>]+>/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}
Open data scored by Repobility · https://repobility.com
GET function · typescript · L26-L172 (147 LOC)
app/api/emails/[messageId]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ messageId: string }> }
) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const { messageId } = await params;

    // Get stored Gmail tokens
    const { data: tokenData, error: tokenError } = await supabase
      .from('gmail_tokens')
      .select('*')
      .single();

    if (tokenError) {
      console.error('Token fetch error:', tokenError);
      return NextResponse.json(
        { error: 'Gmail not connected', needsAuth: true },
        { status: 401 }
      );
    }

    if (!tokenData) {
      return NextResponse.json(
        { error: 'Gmail not connected', needsAuth: true },
        { status: 401 }
      );
    }

    // Create Gmail client with stored tokens
    const gmail = createGmailClient(tokenData.access_token, tokenData.refresh_token);

GET function · typescript · L6-L106 (101 LOC)
app/api/extension/check-duplicate/route.ts
export async function GET(request: NextRequest) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const { searchParams } = new URL(request.url);
    const url = searchParams.get('url');
    const company = searchParams.get('company');
    const role = searchParams.get('role');

    // Must provide either url or both company and role
    if (!url && (!company || !role)) {
      return NextResponse.json(
        { error: 'Must provide either url or both company and role' },
        { status: 400 }
      );
    }

    // Check by URL first (most reliable)
    if (url) {
      const { data: existingByUrl, error: urlError } = await supabase
        .from('applications')
        .select('id, company, role, job_url, status, created_at')
        .eq('job_url', url)
        .maybeSingle();

      if (urlError) {
        console.error('Database error 
POST function · typescript · L15-L159 (145 LOC)
app/api/extension/save/route.ts
export async function POST(request: NextRequest) {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const body: ExtensionSaveRequest = await request.json();

    // Validate required fields
    if (!body.company || typeof body.company !== 'string' || !body.company.trim()) {
      return NextResponse.json(
        { error: 'Company is required' },
        { status: 400 }
      );
    }

    if (!body.role || typeof body.role !== 'string' || !body.role.trim()) {
      return NextResponse.json(
        { error: 'Role is required' },
        { status: 400 }
      );
    }

    if (!body.jobUrl || typeof body.jobUrl !== 'string' || !body.jobUrl.trim()) {
      return NextResponse.json(
        { error: 'Job URL is required' },
        { status: 400 }
      );
    }

    // Validate URL format
    try {
      new URL(body.jobUrl);
    } catch {
   
POST function · typescript · L4-L46 (43 LOC)
app/api/extract-url/route.ts
export async function POST(request: NextRequest) {
  try {
    const { url } = await request.json();

    if (!url) {
      return NextResponse.json({ error: 'URL is required' }, { status: 400 });
    }

    // Validate URL
    let parsedUrl: URL;
    try {
      parsedUrl = new URL(url);
    } catch {
      return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
    }

    // Fetch the page
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; Canopy/1.0)',
        'Accept': 'text/html',
      },
      signal: AbortSignal.timeout(10000), // 10s timeout
    });

    if (!response.ok) {
      return NextResponse.json({
        error: 'Failed to fetch URL',
        company: extractCompanyFromUrl(parsedUrl.hostname, parsedUrl.pathname),
      }, { status: 200 }); // Still return partial data
    }

    const html = await response.text();

    // Extract metadata from HTML
    const metadata = extractMetadata(html, parsedUrl.h
extractMetadata function · typescript · L48-L87 (40 LOC)
app/api/extract-url/route.ts
function extractMetadata(html: string, hostname: string, pathname: string) {
  const result: {
    company: string | null;
    role: string | null;
    location: string | null;
    description: string | null;
  } = {
    company: null,
    role: null,
    location: null,
    description: null,
  };

  // Extract OpenGraph tags
  const ogTitle = extractMeta(html, 'og:title');
  const ogDescription = extractMeta(html, 'og:description');
  const ogSiteName = extractMeta(html, 'og:site_name');

  // Extract standard meta tags
  const title = extractTitle(html);
  const description = extractMeta(html, 'description');

  // Try to extract company name - prioritize URL-based extraction for job boards
  const urlCompany = extractCompanyFromUrl(hostname, pathname);
  // Don't use ogSiteName if it's a job board name
  const jobBoards = ['lever', 'greenhouse', 'ashby', 'workable', 'smartrecruiters', 'hibob', 'workday'];
  const isJobBoardSiteName = ogSiteName && jobBoards.some(jb => ogSiteName.to
extractMeta function · typescript · L89-L105 (17 LOC)
app/api/extract-url/route.ts
function extractMeta(html: string, name: string): string | null {
  // Match both property="og:..." and name="..." patterns
  const patterns = [
    new RegExp(`<meta[^>]*property=["']${name}["'][^>]*content=["']([^"']+)["']`, 'i'),
    new RegExp(`<meta[^>]*content=["']([^"']+)["'][^>]*property=["']${name}["']`, 'i'),
    new RegExp(`<meta[^>]*name=["']${name}["'][^>]*content=["']([^"']+)["']`, 'i'),
    new RegExp(`<meta[^>]*content=["']([^"']+)["'][^>]*name=["']${name}["']`, 'i'),
  ];

  for (const pattern of patterns) {
    const match = html.match(pattern);
    if (match) {
      return decodeHtmlEntities(match[1]);
    }
  }
  return null;
}
extractCompanyFromUrl function · typescript · L121-L147 (27 LOC)
app/api/extract-url/route.ts
function extractCompanyFromUrl(hostname: string, pathname: string): string | null {
  // FIRST: Check for subdomain pattern like "leboncoin.careers.hibob.com" or "company.jobs.lever.co"
  // This must come first because hibob/etc use subdomain for company name
  const subdomainMatch = hostname.match(/^([^.]+)\.(careers|jobs|apply|hire)\./);
  if (subdomainMatch) {
    const subdomain = subdomainMatch[1].toLowerCase();
    // Make sure it's not a generic subdomain
    if (!['www', 'app', 'api', 'jobs', 'careers', 'apply'].includes(subdomain)) {
      const company = subdomain.replace(/-/g, ' ');
      return company.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
    }
  }

  // SECOND: Check job boards where company is in the PATH (lever, greenhouse, etc)
  for (const [domain, pattern] of Object.entries(JOB_BOARD_DOMAINS)) {
    if (hostname.includes(domain)) {
      const match = pathname.match(pattern);
      if (match) {
        const company = match[1].replac
extractCompanyFromDomain function · typescript · L149-L162 (14 LOC)
app/api/extract-url/route.ts
function extractCompanyFromDomain(hostname: string): string | null {
  // Skip known job board domains
  const jobBoards = ['lever', 'greenhouse', 'ashbyhq', 'workable', 'smartrecruiters', 'bamboohr', 'workday', 'icims', 'taleo', 'successfactors', 'myworkdayjobs', 'hibob', 'breezy', 'recruitee', 'jazz'];

  // Remove common prefixes/suffixes
  let domain = hostname
    .replace(/^(www\.|careers\.|jobs\.|apply\.|hire\.|recruiting\.|boards\.)/, '')
    .replace(/\.(com|org|net|io|co|hr).*$/, '');

  if (!domain || jobBoards.includes(domain.toLowerCase())) return null;

  // Capitalize first letter
  return domain.charAt(0).toUpperCase() + domain.slice(1);
}
Powered by Repobility — scan your code at https://repobility.com
extractCompanyFromTitle function · typescript · L164-L182 (19 LOC)
app/api/extract-url/route.ts
function extractCompanyFromTitle(title: string | null): string | null {
  if (!title) return null;

  // Common patterns: "Role at Company", "Role - Company", "Company | Role"
  const patterns = [
    / at ([A-Z][A-Za-z0-9\s&]+?)(?:\s*[-|]|$)/,
    / - ([A-Z][A-Za-z0-9\s&]+?)(?:\s*[-|]|$)/,
    /^([A-Z][A-Za-z0-9\s&]+?) \| /,
  ];

  for (const pattern of patterns) {
    const match = title.match(pattern);
    if (match) {
      return match[1].trim();
    }
  }

  return null;
}
extractRoleFromTitle function · typescript · L184-L200 (17 LOC)
app/api/extract-url/route.ts
function extractRoleFromTitle(title: string | null): string | null {
  if (!title) return null;

  // Remove company name and common suffixes
  let role = title
    .replace(/ at .+$/, '')
    .replace(/ - .+$/, '')
    .replace(/ \| .+$/, '')
    .replace(/\s*\([^)]+\)\s*$/, '')
    .replace(/\s*[-–—]\s*.*$/, '')
    .trim();

  // Don't return if it's too short or looks like a company name only
  if (role.length < 3 || role.length > 100) return null;

  return role;
}
extractLocation function · typescript · L202-L221 (20 LOC)
app/api/extract-url/route.ts
function extractLocation(text: string): string | null {
  if (!text) return null;

  // Common location patterns
  const patterns = [
    /\b(Remote|Hybrid|On-?site)\b/i,
    /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*,\s*[A-Z]{2})\b/, // City, ST
    /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*,\s*[A-Z][a-z]+)\b/, // City, Country
    /\bLocation:\s*([^.|\n]+)/i,
  ];

  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (match) {
      return match[1].trim();
    }
  }

  return null;
}
decodeHtmlEntities function · typescript · L223-L233 (11 LOC)
app/api/extract-url/route.ts
function decodeHtmlEntities(text: string): string {
  return text
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&nbsp;/g, ' ')
    .replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)))
    .replace(/&#x([a-fA-F0-9]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
}
POST function · typescript · L17-L56 (40 LOC)
app/api/onboarding/analyze-cv/route.ts
export async function POST(request: NextRequest): Promise<NextResponse<AnalyzeCVResponse>> {
  try {
    const body: AnalyzeCVRequest = await request.json();

    // Validate input - cvText can be empty for "no CV" path
    if (typeof body.cvText !== 'string') {
      return NextResponse.json(
        {
          success: false,
          error: 'cvText must be a string',
          questions: [],
          cvQuality: 'none' as const,
        },
        { status: 400 }
      );
    }

    // Analyze the CV
    const result = await analyzeCV(body.cvText);

    return NextResponse.json({
      success: true,
      ...result,
    });
  } catch (error) {
    console.error('Error analyzing CV:', error);

    const errorMessage = error instanceof Error ? error.message : 'Failed to analyze CV';

    return NextResponse.json(
      {
        success: false,
        error: errorMessage,
        questions: [],
        cvQuality: 'none' as const,
      },
      { status: 500 }
    );
  }
}
POST function · typescript · L30-L155 (126 LOC)
app/api/onboarding/complete/route.ts
export async function POST(request: NextRequest): Promise<NextResponse<CompleteOnboardingResponse>> {
  try {
    const supabase = createServerClient();

    if (!supabase) {
      return NextResponse.json(
        {
          success: false,
          error: 'Database not configured',
        },
        { status: 503 }
      );
    }

    const body: CompleteOnboardingRequest = await request.json();

    // Validate master CV
    if (!body.masterCV || typeof body.masterCV !== 'string' || body.masterCV.trim().length < 50) {
      return NextResponse.json(
        {
          success: false,
          error: 'Master CV is required and must be at least 50 characters',
        },
        { status: 400 }
      );
    }

    // Save master CV to database
    const cvData = cvToDb({
      userId: DEFAULT_USER_ID,
      rawText: body.masterCV.trim(),
      fileName: 'Master CV (Onboarding)',
      pdfStoragePath: null,
      fileSizeBytes: null,
    });

    const { data: savedCV, error: cvEr
extractCompanyFromUrl function · typescript · L160-L202 (43 LOC)
app/api/onboarding/complete/route.ts
function extractCompanyFromUrl(url: string): string | null {
  if (!url) return null;

  try {
    const urlObj = new URL(url);
    const hostname = urlObj.hostname.toLowerCase();

    // Common job board patterns
    const patterns: Record<string, RegExp> = {
      lever: /jobs\.lever\.co\/([^/]+)/,
      greenhouse: /boards\.greenhouse\.io\/([^/]+)/,
      ashby: /jobs\.ashbyhq\.com\/([^/]+)/,
      workday: /([^.]+)\.wd\d+\.myworkdayjobs\.com/,
      linkedin: /linkedin\.com\/jobs\/view/,
    };

    // Check for job board patterns
    for (const [, regex] of Object.entries(patterns)) {
      const match = url.match(regex);
      if (match && match[1]) {
        return formatCompanyName(match[1]);
      }
    }

    // LinkedIn doesn't have company in URL reliably
    if (hostname.includes('linkedin.com')) {
      return null;
    }

    // Fallback: use hostname without common prefixes
    const cleanHostname = hostname
      .replace('www.', '')
      .replace('jobs.', '')
      .
formatCompanyName function · typescript · L207-L212 (6 LOC)
app/api/onboarding/complete/route.ts
function formatCompanyName(slug: string): string {
  return slug
    .split('-')
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ');
}
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
POST function · typescript · L19-L83 (65 LOC)
app/api/onboarding/enhance-cv/route.ts
export async function POST(request: NextRequest): Promise<NextResponse<EnhanceCVResponse>> {
  try {
    const body: EnhanceCVRequest = await request.json();

    // Validate input
    if (typeof body.cvText !== 'string') {
      return NextResponse.json(
        {
          success: false,
          error: 'cvText must be a string',
        },
        { status: 400 }
      );
    }

    if (!body.answers || typeof body.answers !== 'object') {
      return NextResponse.json(
        {
          success: false,
          error: 'answers must be an object',
        },
        { status: 400 }
      );
    }

    // Check if we have enough input to generate a CV
    const hasCV = body.cvText.trim().length >= 50;
    const hasAnswers = Object.values(body.answers).some(
      (v) => typeof v === 'string' && v.trim().length > 0
    );

    if (!hasCV && !hasAnswers) {
      return NextResponse.json(
        {
          success: false,
          error: 'Please provide either a CV or answer som
GET function · typescript · L8-L48 (41 LOC)
app/api/research/chat/[applicationId]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ applicationId: string }> }
) {
  try {
    const { applicationId } = await params;

    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    const { data, error } = await supabase
      .from('research_chats')
      .select('messages')
      .eq('application_id', applicationId)
      .single();

    if (error && error.code !== 'PGRST116') {
      console.error('Failed to fetch chat history:', error);
      return NextResponse.json(
        { error: 'Failed to load chat history' },
        { status: 500 }
      );
    }

    return NextResponse.json({
      messages: data?.messages || [],
    });

  } catch (error) {
    console.error('Chat history error:', error);
    return NextResponse.json(
      { error: 'Failed to load chat history' },
      { status: 500 }
    );
  }
}
getAnthropic function · typescript · L9-L16 (8 LOC)
app/api/research/chat/route.ts
function getAnthropic(): Anthropic {
  if (!anthropicClient) {
    anthropicClient = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
  }
  return anthropicClient;
}
fetchJobPosting function · typescript · L43-L90 (48 LOC)
app/api/research/chat/route.ts
async function fetchJobPosting(url: string): Promise<string | null> {
  try {
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
      },
    });
    if (!response.ok) return null;

    const html = await response.text();
    const contentParts: string[] = [];

    // Extract meta description
    const metaDesc = html.match(/<meta[^>]*name="description"[^>]*content="([^"]*)"[^>]*>/i);
    if (metaDesc?.[1]) contentParts.push(`Description: ${metaDesc[1]}`);

    // Extract JSON-LD
    const jsonLdMatch = html.match(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi);
    if (jsonLdMatch) {
      for (const match of jsonLdMatch) {
        const jsonContent = match.replace(/<script[^>]*>|<\/script>/gi, '');
        try {
          const data = JSON.parse(jsonContent);
          if (data['@
POST function · typescript · L96-L230 (135 LOC)
app/api/research/chat/route.ts
export async function POST(request: NextRequest) {
  try {
    const body: ResearchChatRequest = await request.json();
    const { applicationId, message, context } = body;

    if (!applicationId || !message || !context?.company) {
      return NextResponse.json(
        { error: 'Missing required fields' },
        { status: 400 }
      );
    }

    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Load existing chat history
    const { data: chatData } = await supabase
      .from('research_chats')
      .select('messages')
      .eq('application_id', applicationId)
      .single();

    const existingMessages: ChatMessage[] = chatData?.messages || [];

    // Fetch job posting content if URL provided and first message
    let jobPostingContent: string | null = null;
    if (context.jobUrl && existingMessages.length === 0) {
      jobPostingContent 
getOpenAI function · typescript · L8-L15 (8 LOC)
app/api/research/route.ts
function getOpenAI(): OpenAI {
  if (!openaiClient) {
    openaiClient = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
    });
  }
  return openaiClient;
}
fetchJobPosting function · typescript · L24-L99 (76 LOC)
app/api/research/route.ts
async function fetchJobPosting(url: string): Promise<string | null> {
  try {
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
      },
    });
    if (!response.ok) {
      console.warn(`Failed to fetch job posting: ${response.status}`);
      return null;
    }

    const html = await response.text();

    // Extract meaningful content from various sources
    const contentParts: string[] = [];

    // 1. Extract from meta description
    const metaDesc = html.match(/<meta[^>]*name="description"[^>]*content="([^"]*)"[^>]*>/i);
    if (metaDesc?.[1]) {
      contentParts.push(`Description: ${metaDesc[1]}`);
    }

    // 2. Extract from og:description
    const ogDesc = html.match(/<meta[^>]*property="og:description"[^>]*content="([^"]*)"[^>]*>/i);
    
fetchEmailContext function · typescript · L104-L131 (28 LOC)
app/api/research/route.ts
async function fetchEmailContext(applicationId: string, supabase: ReturnType<typeof createServerClient>): Promise<string | null> {
  if (!supabase) return null;

  try {
    // Get the application to find the source email
    const { data: app } = await supabase
      .from('applications')
      .select('source_email_id')
      .eq('id', applicationId)
      .single();

    if (!app?.source_email_id) return null;

    // Get the email content
    const { data: email } = await supabase
      .from('emails')
      .select('subject, body, snippet')
      .eq('gmail_message_id', app.source_email_id)
      .single();

    if (!email) return null;

    return `Email Subject: ${email.subject}\nEmail Content: ${email.body || email.snippet || ''}`;
  } catch (error) {
    console.warn('Failed to fetch email context:', error);
    return null;
  }
}
Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
generateResearch function · typescript · L167-L309 (143 LOC)
app/api/research/route.ts
async function generateResearch(
  company: string,
  role: string,
  jobUrl?: string,
  context?: ResearchContext
): Promise<ResearchResponse> {
  // Build context section with real data
  let contextSection = '';

  if (context?.jobPostingContent) {
    contextSection += `\n\n## ACTUAL JOB POSTING CONTENT (USE THIS AS PRIMARY SOURCE):\n${context.jobPostingContent}\n`;
  }

  if (context?.emailContent) {
    contextSection += `\n\n## EMAIL FROM COMPANY:\n${context.emailContent}\n`;
  }

  if (jobUrl) {
    contextSection += `\n\nJob posting URL: ${jobUrl}`;
  }

  const prompt = `You are an expert career coach and company researcher. Research the following company and role to help someone prepare for a job interview.

Company: ${company}
Role: ${role}
${contextSection}

IMPORTANT: If job posting content is provided above, extract REAL information from it. Do NOT make up information - use what's actually in the job posting. For any fields where you don't have real data, use null or lea
POST function · typescript · L315-L438 (124 LOC)
app/api/research/route.ts
export async function POST(request: NextRequest) {
  try {
    // Validate API key is configured
    if (!process.env.OPENAI_API_KEY) {
      return NextResponse.json(
        { error: 'OpenAI API key not configured' },
        { status: 503 }
      );
    }

    // Parse request body
    const body: ResearchRequest = await request.json();

    // Validate required fields
    if (!body.company || typeof body.company !== 'string') {
      return NextResponse.json(
        { error: 'Company name is required' },
        { status: 400 }
      );
    }

    if (!body.role || typeof body.role !== 'string') {
      return NextResponse.json(
        { error: 'Role is required' },
        { status: 400 }
      );
    }

    const company = body.company.trim();
    const role = body.role.trim();
    const jobUrl = body.jobUrl?.trim();
    const applicationId = body.applicationId;

    // Check cache in Supabase
    const supabase = createServerClient();

    if (supabase) {
      try {
        c
POST function · typescript · L17-L147 (131 LOC)
app/api/stripe/create-checkout/route.ts
export async function POST(request: NextRequest) {
  // Check if Stripe is configured
  if (!isStripeConfigured() || !stripe) {
    return NextResponse.json(
      { error: 'Stripe is not configured' },
      { status: 503 }
    );
  }

  try {
    // Parse request body
    const body: CheckoutRequest = await request.json();
    const { priceId, successUrl, cancelUrl } = body;

    // Validate priceId
    if (!priceId) {
      return NextResponse.json(
        { error: 'priceId is required' },
        { status: 400 }
      );
    }

    // Validate that priceId is one of our valid bundle prices
    if (!isValidPriceId(priceId)) {
      return NextResponse.json(
        { error: 'Invalid priceId' },
        { status: 400 }
      );
    }

    // Get the Supabase client
    const supabase = createServerClient();
    if (!supabase) {
      return NextResponse.json(
        { error: 'Database not configured' },
        { status: 503 }
      );
    }

    // Get user from auth header or coo
page 1 / 5next ›