This commit is contained in:
yenminh269 2025-10-18 17:46:54 -05:00
commit 5a9da85f07
31 changed files with 2831 additions and 54 deletions

39
.dockerignore Normal file
View file

@ -0,0 +1,39 @@
# Node modules
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist
build
target
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Git
.git
.gitignore
# Documentation
README.md
DEVELOPMENT.md
*.md
# Scripts
setup-check.ps1

14
.env.example Normal file
View file

@ -0,0 +1,14 @@
# .env.example
# --- PLEASE FILL THIS OUT WITH THE SECURELY PROVIDED CREDENTIALS ---
# Application Database Credentials
MYSQL_DATABASE=astra
MYSQL_USER=astraadmin
MYSQL_PASSWORD=
# MySQL Container Root Password (for first-time setup only, not used by the app)
MYSQL_ROOT_PASSWORD=
# API Keys
GEMINI_API_KEY=

View file

@ -1,56 +1,80 @@
# For information on GITHUB_TOKEN: https://docs.github.com/en/actions/concepts/security/github_token # .github/workflows/build-and-deploy.yml
# For information on github.actor: https://github.com/orgs/community/discussions/62108
name: Build and Deploy name: Build and Deploy
# This workflow runs only on pushes to the 'main' branch
on: on:
pull_request:
branches: ["main"]
push: push:
branches: ["main"] branches: ["main"]
env:
CONTAINER_NAME: codered-astra
CONTAINER_TAG: ghcr.io/${{ github.repository_owner }}/codered-astra:latest
jobs: jobs:
# Set permissions for the job build-and-deploy:
build: # Set permissions for the job to read contents and write to GitHub Packages
permissions: permissions:
contents: read contents: read
packages: write packages: write
attestations: write
id-token: write
name: Build Docker Image name: Build Images and Deploy to Server
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Login to Docker Hub - name: Log in to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} # User that commits username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
# --- NEW STEP TO FIX THE CACHING ERROR ---
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build and push - name: Extract metadata (tags, labels) for Docker
uses: docker/build-push-action@v6 id: meta
uses: docker/metadata-action@v5
with: with:
context: . images: |
platforms: linux/amd64 ghcr.io/${{ github.repository }}/web-app
push: true ghcr.io/${{ github.repository }}/rust-engine
tags: ${{ env.CONTAINER_TAG }}
# WIP: For deployment # --- Build and push one image for each service ---
deploy: - name: Build and push web-app image 🚀
name: Deploy Docker Image to Server uses: docker/build-push-action@v6
runs-on: self-hosted with:
needs: build context: ./web-app
steps: push: true
tags: ${{ steps.meta.outputs.tags_web-app }}
labels: ${{ steps.meta.outputs.labels_web-app }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Rust engine image ⚙️
uses: docker/build-push-action@v6
with:
context: ./rust-engine
push: true
tags: ${{ steps.meta.outputs.tags_rust-engine }}
labels: ${{ steps.meta.outputs.labels_rust-engine }}
cache-from: type=gha
cache-to: type=gha,mode=max
# --- Deploy the new images to your server ---
- name: Deploy to server via SSH ☁️
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/codered-astra
export GEMINI_API_KEY='${{ secrets.GEMINI_API_KEY }}'
export MYSQL_DATABASE='${{ secrets.MYSQL_DATABASE }}'
export MYSQL_USER='${{ secrets.MYSQL_USER }}'
export MYSQL_PASSWORD='${{ secrets.MYSQL_PASSWORD }}'
export MYSQL_ROOT_PASSWORD='${{ secrets.MYSQL_ROOT_PASSWORD }}'
export IMAGE_TAG=${{ github.sha }}
docker-compose pull
docker-compose up -d --force-recreate

3
.gitignore vendored
View file

@ -21,6 +21,9 @@ dist
turbo-build.log turbo-build.log
turbo-host.log turbo-host.log
# rust
rust-engine/target
# debug # debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View file

@ -1,16 +1,75 @@
# React + Vite # CodeRED-Astra 🚀
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. A hackathon-ready project with React frontend and Rust backend engine.
Currently, two official plugins are available: ## Quick Start
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh ```bash
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh # 1. Setup environment
cp .env.example .env
# Edit .env with your credentials
## React Compiler # 2. Start everything with Docker
docker-compose up --build
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). # 3. Access your app
# Frontend: http://localhost
# API: http://localhost:8000
# Database Admin: http://127.0.0.1:8080
```
## Expanding the ESLint configuration ## Development
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. **Frontend (React + Vite)**:
```bash
cd web-app
npm install
npm run dev # http://localhost:5173
```
**Backend (Rust)**:
```bash
cd rust-engine
cargo run # http://localhost:8000
```
## Architecture
- **Frontend**: React 18 + Vite + Tailwind CSS
- **Backend**: Rust + Warp + SQLx
- **Database**: MySQL 8.0 + phpMyAdmin
- **API**: RESTful endpoints with CORS enabled
- **Docker**: Full containerization for easy deployment
## Project Structure
```
├── web-app/ # React frontend
│ ├── src/
│ │ ├── App.jsx # Main component
│ │ └── main.jsx # Entry point
│ └── Dockerfile
├── rust-engine/ # Rust backend
│ ├── src/
│ │ └── main.rs # API server
│ └── Dockerfile
├── docker-compose.yml # Full stack orchestration
└── .env.example # Environment template
```
## Team Workflow
- **Frontend devs**: Work in `web-app/src/`, use `/api/*` for backend calls
- **Backend devs**: Work in `rust-engine/src/`, add endpoints to main.rs
- **Database**: Access phpMyAdmin at http://127.0.0.1:8080
## Features
✅ Hot reload for both frontend and backend
✅ Automatic API proxying from React to Rust
✅ Database connection with graceful fallback
✅ CORS configured for cross-origin requests
✅ Production-ready Docker containers
✅ Health monitoring and status dashboard
Ready for your hackathon! See `DEVELOPMENT.md` for detailed setup instructions.

53
docker-compose.yml Normal file
View file

@ -0,0 +1,53 @@
# docker-compose.yml
version: '3.8'
services:
web-app:
build:
context: ./web-app
restart: always
ports:
- "80:3000"
environment:
# The connection string remains the same, but points to the 'mysql' service
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
- RUST_ENGINE_URL=http://rust-engine:8000
- GEMINI_API_KEY=${GEMINI_API_KEY}
depends_on:
- mysql # <-- Updated dependency
- rust-engine
rust-engine:
build:
context: ./rust-engine
restart: always
environment:
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/${MYSQL_DATABASE}
depends_on:
- mysql # <-- Updated dependency
# --- Key Changes are in this section ---
mysql: # <-- Renamed service for clarity
image: mysql:8.0 # <-- CHANGED: Using the official MySQL 8.0 image
restart: always
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
phpmyadmin:
image: phpmyadmin/phpmyadmin
restart: always
ports:
# CHANGED: Binds port 8080 to localhost ONLY.
- "127.0.0.1:8080:80"
environment:
- PMA_HOST=mysql
depends_on:
- mysql
volumes:
mysql-data: # Renamed volume for clarity (optional but good practice)

View file

@ -1,8 +0,0 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": [
"src"
]
}

2390
rust-engine/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

17
rust-engine/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "rust-engine"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
warp = { version = "0.4.2", features = ["server"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "mysql", "chrono"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
dotenv = "0.15"
cors = "0.1.0"
anyhow = "1.0"

30
rust-engine/Dockerfile Normal file
View file

@ -0,0 +1,30 @@
# rust-engine/Dockerfile
# --- Stage 1: Builder ---
FROM rust:1.82-slim AS builder
WORKDIR /usr/src/app
# Install build dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy Cargo files for dependency caching
COPY Cargo.toml Cargo.lock ./
# Create a dummy src/main.rs for dependency build
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release && rm src/main.rs
# Copy source code and build
COPY src ./src
RUN cargo build --release
# --- Stage 2: Final Image ---
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/app/target/release/rust-engine /usr/local/bin/rust-engine
EXPOSE 8000
CMD ["rust-engine"]

132
rust-engine/src/main.rs Normal file
View file

@ -0,0 +1,132 @@
use std::env;
use warp::Filter;
use sqlx::mysql::MySqlPool;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
#[derive(Debug, Serialize, Deserialize)]
struct HealthResponse {
status: String,
timestamp: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
message: Option<String>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing
tracing_subscriber::fmt::init();
// Load environment variables
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL")
.unwrap_or_else(|_| "mysql://astraadmin:password@mysql:3306/astra".to_string());
info!("Starting Rust Engine...");
info!("Connecting to database: {}", database_url);
// Connect to database
let pool = match MySqlPool::connect(&database_url).await {
Ok(pool) => {
info!("Successfully connected to database");
pool
}
Err(e) => {
warn!("Failed to connect to database: {}. Starting without DB connection.", e);
// In a hackathon setting, we might want to continue without DB for initial testing
return start_server_without_db().await;
}
};
// CORS configuration
let cors = warp::cors()
.allow_any_origin()
.allow_headers(vec!["content-type", "authorization"])
.allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]);
// Health check endpoint
let health = warp::path("health")
.and(warp::get())
.map(|| {
let response = HealthResponse {
status: "healthy".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
};
warp::reply::json(&ApiResponse {
success: true,
data: Some(response),
message: None,
})
});
// API routes - you'll expand these for your hackathon needs
let api = warp::path("api")
.and(
health.or(
// Add more routes here as needed
warp::path("version")
.and(warp::get())
.map(|| {
warp::reply::json(&ApiResponse {
success: true,
data: Some("1.0.0"),
message: Some("Rust Engine API".to_string()),
})
})
)
);
let routes = api
.with(cors)
.with(warp::log("rust_engine"));
info!("Rust Engine started on http://0.0.0.0:8000");
warp::serve(routes)
.run(([0, 0, 0, 0], 8000))
.await;
Ok(())
}
async fn start_server_without_db() -> Result<(), Box<dyn std::error::Error>> {
info!("Starting server in DB-less mode for development");
let cors = warp::cors()
.allow_any_origin()
.allow_headers(vec!["content-type", "authorization"])
.allow_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]);
let health = warp::path("health")
.and(warp::get())
.map(|| {
let response = HealthResponse {
status: "healthy (no db)".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
};
warp::reply::json(&ApiResponse {
success: true,
data: Some(response),
message: Some("Running without database connection".to_string()),
})
});
let routes = warp::path("api")
.and(health)
.with(cors)
.with(warp::log("rust_engine"));
info!("Rust Engine started on http://0.0.0.0:8000 (DB-less mode)");
warp::serve(routes)
.run(([0, 0, 0, 0], 8000))
.await;
Ok(())
}

16
web-app/README.md Normal file
View file

@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View file

@ -1,7 +1,5 @@
import js from "@eslint/js"; import js from "@eslint/js";
import globals from "globals"; import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([ export default defineConfig([

6
web-app/jsconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"baseUrl": "./"
},
"include": ["src"]
}

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

@ -1,5 +1,9 @@
import React from "react"; import React from "react";
<<<<<<< HEAD:src/app/index.jsx
import ChatLayout from "../components/ui/ChatLayout"; import ChatLayout from "../components/ui/ChatLayout";
=======
import ChatLayout from "src/components/layouts/chat-layout";
>>>>>>> cb9cff44215b6de81ed81ef2a1c6abe090fbf1b1:web-app/src/app/index.jsx
function App() { function App() {
return ( return (

View file

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import ChatHeader from "./ChatHeader"; import ChatHeader from "src/components/ui/chat/chat-header";
import ChatWindow from "./ChatWindow"; import ChatWindow from "src/components/ui/chat/chat-window";
import MessageInput from "./MessageInput"; import MessageInput from "src/components/ui/chat/message-input";
export default function ChatLayout() { export default function ChatLayout() {
const [messages, setMessages] = useState([ const [messages, setMessages] = useState([