From 1cd7be465ce26f6dfe653b0efc920d6c1127772c Mon Sep 17 00:00:00 2001 From: Ricardo Servilla Date: Thu, 18 Sep 2025 19:33:18 -0400 Subject: [PATCH 1/4] Ready for EC2 deployment --- app/api/conversation/[id]/route.ts | 70 ----------- app/api/conversation/route.ts | 165 ------------------------- app/conversation/[id]/page.tsx | 190 ----------------------------- app/page.tsx | 79 ++---------- next.config.ts | 9 +- 5 files changed, 16 insertions(+), 497 deletions(-) delete mode 100644 app/api/conversation/[id]/route.ts delete mode 100644 app/api/conversation/route.ts delete mode 100644 app/conversation/[id]/page.tsx diff --git a/app/api/conversation/[id]/route.ts b/app/api/conversation/[id]/route.ts deleted file mode 100644 index 054fa74..0000000 --- a/app/api/conversation/[id]/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getConversationRecord } from '@/lib/db/conversations'; -import { s3Client } from '@/lib/storage/s3'; -import { dbClient } from '@/lib/db/client'; -import { loadConfig } from '@/lib/config'; - -let isInitialized = false; - -/** - * Initialize services if not already initialized - */ -async function ensureInitialized() { - if (!isInitialized) { - try { - const config = loadConfig(); - await dbClient.initialize(config.database); - s3Client.initialize(config.s3); - isInitialized = true; - } catch (error) { - // If S3 client is already initialized, that's fine - if (error instanceof Error && error.message.includes('already initialized')) { - isInitialized = true; - } else { - throw error; - } - } - } -} - -/** - * GET /api/conversation/[id] - * - * Retrieves the full conversation data including content and metadata - * - * @param request - The incoming request - * @param context - Route context containing the conversation ID - * - * Response: - * - 200: { conversation: ConversationRecord, content: string } - The conversation data and content - * - 404: { error: string } - Conversation not found - * - 500: { error: string } - Server error - */ -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } -): Promise { - try { - await ensureInitialized(); - const id = (await params).id; - - // Get conversation record from database - const record = await getConversationRecord(id); - - // Get conversation content from S3 - const content = await s3Client.getConversationContent(record.contentKey); - - return NextResponse.json({ - conversation: record, - content: content, - }); - } catch (error) { - console.error('Error retrieving conversation:', error); - - if (error instanceof Error && error.message.includes('not found')) { - return NextResponse.json({ error: error.message }, { status: 404 }); - } - - return NextResponse.json({ error: 'Internal error, see logs' }, { status: 500 }); - } -} diff --git a/app/api/conversation/route.ts b/app/api/conversation/route.ts deleted file mode 100644 index 990e9bd..0000000 --- a/app/api/conversation/route.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { parseHtmlToConversation } from '@/lib/parsers'; -import { dbClient } from '@/lib/db/client'; -import { s3Client } from '@/lib/storage/s3'; -import { CreateConversationInput } from '@/lib/db/types'; -import { createConversationRecord, getAllConversationRecords } from '@/lib/db/conversations'; -import { randomUUID } from 'crypto'; -import { loadConfig } from '@/lib/config'; - -let isInitialized = false; - -/** - * Initialize services if not already initialized - */ -async function ensureInitialized() { - if (!isInitialized) { - try { - const config = loadConfig(); - await dbClient.initialize(config.database); - s3Client.initialize(config.s3); - isInitialized = true; - } catch (error) { - // If S3 client is already initialized, that's fine - if (error instanceof Error && error.message.includes('already initialized')) { - isInitialized = true; - } else { - throw error; - } - } - } -} - -const ALLOWED_ORIGIN = '*'; - -export async function OPTIONS() { - // Preflight handler - return new NextResponse(null, { - status: 204, - headers: { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }, - }); -} - -/** - * POST /api/conversation - * - * Handles storing a new conversation from HTML input - * - * Request body (multipart/form-data): - * - htmlDoc: File - The HTML document containing the conversation - * - model: string - The AI model used (e.g., "ChatGPT", "Claude") - * - * Response: - * - 201: { url: string } - The permalink URL for the conversation - * - 400: { error: string } - Invalid request - * - 500: { error: string } - Server error - */ -export async function POST(req: NextRequest) { - try { - // Initialize services on first request - await ensureInitialized(); - - const formData = await req.formData(); - const file = formData.get('htmlDoc'); - const model = formData.get('model')?.toString() ?? 'ChatGPT'; - - // Validate input - if (!(file instanceof Blob)) { - return NextResponse.json({ error: '`htmlDoc` must be a file field' }, { status: 400 }); - } - - // Parse the conversation from HTML - const html = await file.text(); - const conversation = await parseHtmlToConversation(html, model); - - // Generate a unique ID for the conversation - const conversationId = randomUUID(); - - // Store only the conversation content in S3 - const contentKey = await s3Client.storeConversation(conversationId, conversation.content); - - // Create the database record with metadata - const dbInput: CreateConversationInput = { - model: conversation.model, - scrapedAt: new Date(conversation.scrapedAt), - sourceHtmlBytes: conversation.sourceHtmlBytes, - views: 0, - contentKey, - }; - - const record = await createConversationRecord(dbInput); - - // Generate the permalink using the database-generated ID - const permalink = `${process.env.NEXT_PUBLIC_BASE_URL}/conversation/${record.id}`; - - return NextResponse.json( - { url: permalink }, - { - status: 201, - headers: { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - }, - } - ); - } catch (err) { - console.error('Error processing conversation:', err); - return NextResponse.json({ error: 'Internal error, see logs' }, { status: 500 }); - } -} - -/** - * GET /api/conversation - * - * Retrieves a list of all conversations with pagination - * - * Query parameters: - * - limit: number (optional) - Maximum number of records to return (default: 50) - * - offset: number (optional) - Number of records to skip (default: 0) - * - * Response: - * - 200: { conversations: ConversationRecord[] } - Array of conversation records - * - 400: { error: string } - Invalid request parameters - * - 500: { error: string } - Server error - */ -export async function GET(req: NextRequest) { - try { - // Initialize services on first request - await ensureInitialized(); - - const { searchParams } = new URL(req.url); - const limitParam = searchParams.get('limit'); - const offsetParam = searchParams.get('offset'); - - // Parse and validate query parameters - const limit = limitParam ? parseInt(limitParam, 10) : 50; - const offset = offsetParam ? parseInt(offsetParam, 10) : 0; - - if (isNaN(limit) || limit < 1 || limit > 100) { - return NextResponse.json({ error: 'Invalid limit parameter. Must be between 1 and 100.' }, { status: 400 }); - } - - if (isNaN(offset) || offset < 0) { - return NextResponse.json({ error: 'Invalid offset parameter. Must be non-negative.' }, { status: 400 }); - } - - // Retrieve conversations from database - const conversations = await getAllConversationRecords(limit, offset); - - return NextResponse.json( - { conversations }, - { - status: 200, - headers: { - 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, - }, - } - ); - } catch (err) { - console.error('Error retrieving conversations:', err); - return NextResponse.json({ error: 'Internal error, see logs' }, { status: 500 }); - } -} diff --git a/app/conversation/[id]/page.tsx b/app/conversation/[id]/page.tsx deleted file mode 100644 index f9082a6..0000000 --- a/app/conversation/[id]/page.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { ArrowLeft, Eye, Calendar, MessageSquare } from 'lucide-react'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import Link from 'next/link'; -import { ConversationRecord } from '@/lib/db/types'; - -/** - * Interface for the conversation detail data - */ -interface ConversationDetailData { - conversation: ConversationRecord; - content: string; -} - -/** - * Fetches conversation detail from the API - * - * @param id - The conversation ID - * @returns Promise - The conversation data and content - * @throws Error if the API request fails - */ -async function fetchConversationDetail(id: string): Promise { - try { - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; - const response = await fetch(`${baseUrl}/api/conversation/${id}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - cache: 'no-store', // Disable caching to get fresh data - }); - - if (!response.ok) { - if (response.status === 404) { - throw new Error('Conversation not found'); - } - throw new Error(`Failed to fetch conversation: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - return { - conversation: data.conversation, - content: data.content, - }; - } catch (error) { - console.error('Error fetching conversation detail:', error); - throw error; - } -} - -/** - * Formats the conversation content for display - * - * @param content - Raw conversation content - * @returns string - Formatted content for display - */ -function formatConversationContent(content: string): string { - // For now, return the content as-is - // TODO: Implement proper formatting based on the content structure - return content; -} - -/** - * Conversation detail page component - */ -const ConversationDetailPage = async ({ params }: { params: Promise<{ id: string }> }) => { - const { id } = await params; - - try { - const { conversation, content } = await fetchConversationDetail(id); - const formattedContent = formatConversationContent(content); - - // Calculate days since creation - const createdAt = new Date(conversation.createdAt); - const now = new Date(); - const daysDiff = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)); - - return ( -
-
-
- - - -
-
-
- - - - AI Archives -
-
-
- -
- {/* Conversation Header */} - - -
-
- - {conversation.model} - - ID: {conversation.id} -
-
-
- - {conversation.views} views -
-
- - {daysDiff} days ago -
-
-
-
-
- - {/* Conversation Content */} - - -
- -

Conversation

-
- -
-
-
- - -
-
- ); - } catch { - return ( -
-
-
- - - -
-
-
- - - - AI Archives -
-
-
- -
- - -

Conversation Not Found

-

- The conversation you're looking for doesn't exist or has been removed. -

- - - -
-
-
-
- ); - } -}; - -export default ConversationDetailPage; diff --git a/app/page.tsx b/app/page.tsx index 9619f19..4bb6a59 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,11 +3,10 @@ import { Card, CardContent, CardFooter } from '@/components/ui/card'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { ConversationRecord } from '@/lib/db/types'; import Link from 'next/link'; /** - * Interface for the conversation data displayed in cards + * Interface for the conversation card data */ interface ConversationCardData { id: string; @@ -19,71 +18,19 @@ interface ConversationCardData { related: number; } -/** - * Fetches conversations from the API - * - * @returns Promise - Array of conversation records - * @throws Error if the API request fails - */ -async function fetchConversations(): Promise { - try { - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; - const response = await fetch(`${baseUrl}/api/conversation?limit=50`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - cache: 'no-store', // Disable caching to get fresh data - }); - - if (!response.ok) { - throw new Error(`Failed to fetch conversations: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - return data.conversations || []; - } catch (error) { - console.error('Error fetching conversations:', error); - return []; - } -} - -/** - * Transforms conversation records into card data format - * - * @param conversations - Array of conversation records from the database - * @returns ConversationCardData[] - Array of formatted card data - */ -function transformConversationsToCardData(conversations: ConversationRecord[]): ConversationCardData[] { - return conversations.map((conversation) => { - // Calculate days since creation - const createdAt = new Date(conversation.createdAt); - const now = new Date(); - const daysDiff = Math.floor((now.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)); - - // Generate avatar from model name - const avatar = conversation.model.charAt(0).toUpperCase(); - - return { - id: conversation.id, - avatar, - username: 'Anonymous', - platform: conversation.model, - views: conversation.views, - days: daysDiff, - related: 0, - }; - }); -} +// ==================== +// Dados estΓ‘ticos de exemplo +// ==================== +const conversations: ConversationCardData[] = [ + { id: '1', avatar: 'C', username: 'Anonymous', platform: 'ChatGPT', views: 120, days: 16, related: 0 }, + { id: '2', avatar: 'G', username: 'Anonymous', platform: 'Gemini', views: 45, days: 12, related: 0 }, + { id: '3', avatar: 'L', username: 'Anonymous', platform: 'Claude', views: 78, days: 7, related: 0 }, +]; /** - * Home page component that displays a list of AI conversations + * Home page component */ -const Home = async () => { - // Fetch conversations from the API - const conversations = await fetchConversations(); - const cardData = transformConversationsToCardData(conversations); - +const Home = () => { return (
@@ -116,7 +63,7 @@ const Home = async () => { - {cardData.length === 0 ? ( + {conversations.length === 0 ? (

No conversations found. Be the first to share a conversation!

@@ -124,7 +71,7 @@ const Home = async () => {
) : (
- {cardData.map((card) => ( + {conversations.map((card) => ( diff --git a/next.config.ts b/next.config.ts index c1462d1..ceed066 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,4 @@ -import type { NextConfig } from 'next'; - -const nextConfig: NextConfig = { - devIndicators: false, +const nextConfig = { + output: 'export', }; - -export default nextConfig; +module.exports = nextConfig; From ecdf99dbdc52c61ae02622accb072840f157f9b4 Mon Sep 17 00:00:00 2001 From: Ricardo Servilla Date: Sun, 5 Oct 2025 20:49:03 -0400 Subject: [PATCH 2/4] Create test-secrets.yml --- .github/workflows/test-secrets.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/test-secrets.yml diff --git a/.github/workflows/test-secrets.yml b/.github/workflows/test-secrets.yml new file mode 100644 index 0000000..a54aceb --- /dev/null +++ b/.github/workflows/test-secrets.yml @@ -0,0 +1,24 @@ +name: Test GitHub Secrets + +on: + workflow_dispatch: # permite rodar manualmente + +jobs: + test-secrets: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Print DB secrets + run: | + echo "DB_HOST = ${{ secrets.DB_HOST }}" + echo "DB_NAME = ${{ secrets.DB_NAME }}" + echo "DB_USER = ${{ secrets.DB_USER }}" + echo "DB_PASSWORD = ${{ secrets.DB_PASSWORD }}" + + - name: Print AWS secrets + run: | + echo "AWS_ACCESS_KEY_ID = ${{ secrets.AWS_ACCESS_KEY_ID }}" + echo "AWS_SECRET_ACCESS_KEY = ${{ secrets.AWS_SECRET_ACCESS_KEY }}" From 765a0026fe0c47b920b0d5a4ced5ea5fa03f44e7 Mon Sep 17 00:00:00 2001 From: Ricardo Servilla Date: Sun, 5 Oct 2025 21:04:17 -0400 Subject: [PATCH 3/4] Update deploy.yml --- .github/workflows/deploy.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f2d0a7b..2d0f219 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,6 +11,12 @@ jobs: SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }} HOST: ${{ secrets.SERVER_HOST }} USER: ${{ secrets.SERVER_USER }} + DB_HOST: ${{ secrets.DB_HOST }} + DB_NAME: ${{ secrets.DB_NAME }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} steps: - uses: actions/checkout@v3 From 75901508e26850cf2afec2c11dc9aabeaeae3962 Mon Sep 17 00:00:00 2001 From: Ricardo Servilla Date: Mon, 6 Oct 2025 23:32:34 -0400 Subject: [PATCH 4/4] Add TechX Internship section to README Added detailed information about my Software Engineering Internship at TechX Labs, including overview, key activities, skills demonstrated, and conclusion. --- README.md | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e215bc4..3f8e44f 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,37 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# πŸ’» Software Engineering Internship (TechX Labs, Inc.) -## Getting Started +πŸ“… **Duration:** September 08, 2025 – November 07, 2025 +πŸ–₯️ **Location:** Remote (Boston, MA) -First, run the development server: +--- -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +## πŸ“ Internship Overview -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Participated in the **Software Engineering Intern** program at TechX Labs, gaining hands-on experience with real-world projects focused on **large-scale web architecture and distributed systems**. Developed technical skills under the guidance of experienced engineers while contributing to enterprise-grade solutions. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +--- -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## βš™οΈ Key Activities -## Learn More +πŸ§ͺ Assisted in building and optimizing cloud-native infrastructure +πŸ•΅οΈβ€β™‚οΈ Contributed to high-availability system design and performance improvements +πŸ” Applied security best practices and worked with monitoring/logging systems +🧬 Collaborated with mentors and peers on real-world engineering projects +πŸ› οΈ Gained practical experience with technologies such as **VMSS, load balancers, and Redis caching** -To learn more about Next.js, take a look at the following resources: +--- -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## πŸ’‘ Skills Demonstrated -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +πŸ›‘οΈ Cloud and Distributed Systems +🚨 Performance Optimization & High-Availability Design +πŸ“Š Enterprise-Grade Software Development +🌐 Security Best Practices Implementation +πŸ“ Technical Documentation & Collaboration +🧠 Critical Thinking in Software Engineering -## Deploy on Vercel +--- -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## 🏁 Conclusion -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +This internship provided me with valuable practical experience in software engineering, particularly in large-scale web architecture and distributed systems. It strengthened my technical and problem-solving skills while giving me confidence to contribute effectively to professional engineering teams. I look forward to applying these skills in future projects and advancing my knowledge in software development and cloud technologies.