diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 3d68271..f67c0b4 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -29,7 +29,7 @@ steps: path: include: [ backend/** ] settings: - registry: harbor-core.dev-tools.svc.cluster.local + registry: harbor.dvirlabs.com repo: my-apps/${CI_REPO_NAME}-backend dockerfile: backend/Dockerfile context: backend diff --git a/README.md b/README.md index 24d11e3..5617078 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,43 @@ oramap/ ├── backend/ │ ├── server.js # Express server │ ├── package.json # Backend dependencies +│ ├── Dockerfile # Backend container image │ └── data/ │ └── families.json # Family location data +├── frontend/ +│ ├── Dockerfile # Frontend Nginx container +│ ├── nginx.conf # Nginx configuration +│ └── public/ +│ ├── index.html # Frontend HTML +│ ├── script.js # Frontend JavaScript +│ └── style.css # Styles ├── public/ -│ ├── index.html # Frontend HTML -│ ├── script.js # Frontend JavaScript -│ └── style.css # Styles -├── Dockerfile # Multi-stage Docker build -├── docker-compose.yml # Docker Compose configuration -└── .dockerignore # Docker ignore rules +│ ├── index.html # Shared frontend files +│ ├── script.js +│ └── style.css +├── Dockerfile # Monolith Docker build +├── docker-compose.yml # Monolith deployment +├── docker-compose.microservices.yml # Microservices deployment +├── .woodpecker.yaml # CI/CD pipeline config +└── .dockerignore # Docker ignore rules ``` +## 🏗️ Architecture + +The application supports two deployment modes: + +### 1. **Monolith Mode** (Simple) +- Single container with Express serving both API and static files +- Best for: Development, simple deployments +- Use: `docker-compose up` + +### 2. **Microservices Mode** (Production) +- **Frontend**: Nginx serving static files (Port 80) +- **Backend**: Express API server (Internal Port 3000) +- Nginx proxies `/api/*` requests to backend +- Best for: Production, scalability, CI/CD pipelines +- Use: `docker-compose -f docker-compose.microservices.yml up` + ## 🚀 Getting Started ### Prerequisites @@ -79,40 +105,76 @@ Navigate to `http://localhost:3000` ## 🐳 Docker Deployment -### Using Docker Compose (Recommended) +### Monolith Mode (Recommended for Development) -1. **Build and start the application:** +1. **Build and start:** ```bash docker-compose up -d ``` -2. **View logs:** -```bash -docker-compose logs -f -``` +2. **Access:** http://localhost:3000 -3. **Stop the application:** +3. **Stop:** ```bash docker-compose down ``` -### Using Docker directly +### Microservices Mode (Recommended for Production) -1. **Build the image:** +1. **Build and start:** ```bash -docker build -t oramap:latest . +docker-compose -f docker-compose.microservices.yml up -d ``` -2. **Run the container:** +2. **Access:** http://localhost (Port 80) + +3. **View logs:** ```bash +docker logs oramap-frontend +docker logs oramap-backend +``` + +4. **Stop:** +```bash +docker-compose -f docker-compose.microservices.yml down +``` + +### Using Docker directly + +**Monolith:** +```bash +docker build -t oramap:latest . docker run -d -p 3000:3000 --name oramap-app oramap:latest ``` -3. **Check health:** +**Microservices:** ```bash -docker ps +# Build images +docker build -t oramap-backend -f backend/Dockerfile backend/ +docker build -t oramap-frontend -f frontend/Dockerfile frontend/ + +# Run with network +docker network create oramap-network +docker run -d --name oramap-backend --network oramap-network oramap-backend +docker run -d --name oramap-frontend --network oramap-network -p 80:80 oramap-frontend ``` +## 🔄 CI/CD Pipeline + +The project includes a Woodpecker CI configuration (`.woodpecker.yaml`) that automatically: + +1. **Builds** separate frontend and backend Docker images on push +2. **Tags** images with branch name and commit SHA +3. **Pushes** to Harbor registry (`harbor.dvirlabs.com`) +4. **Updates** Kubernetes manifests with new image tags +5. **Triggers** on changes to `frontend/**` or `backend/**` paths + +### Pipeline Steps: +- `build-frontend`: Build and push frontend Nginx image +- `build-backend`: Build and push backend API image +- `update-values-frontend`: Update frontend image tag in values.yaml +- `update-values-backend`: Update backend image tag in values.yaml + ## 💻 Development ### Running in Development Mode diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..ca82d5d --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ +npm-debug.log* + +# Environment +.env* + +# Build artifacts +dist/ + +# IDE +.vscode/ +.idea/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Documentation +*.md + +# Data not needed in image (using volume or copy) +# Keep data/ folder as it's needed diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..4f8cae2 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,30 @@ +# Backend Dockerfile for Ora Map +FROM node:20-alpine AS base + +# Production stage +FROM base AS production + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install production dependencies +RUN npm install --production + +# Copy application files +COPY . . + +# Expose port +EXPOSE 3000 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start the application +CMD ["node", "server.js"] diff --git a/backend/server-microservice.js b/backend/server-microservice.js new file mode 100644 index 0000000..cde82b5 --- /dev/null +++ b/backend/server-microservice.js @@ -0,0 +1,35 @@ +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const app = express(); +const families = require('./data/families.json'); + +// Enable CORS for frontend +app.use(cors()); + +// Serve static files from the public directory (for standalone mode) +if (process.env.SERVE_STATIC === 'true') { + app.use(express.static(path.join(__dirname, '../public'))); +} + +// API endpoint for family search +app.get('/api/search', (req, res) => { + const query = req.query.family?.toLowerCase(); + if (!query) { + return res.json([]); + } + const matches = families.filter(fam => fam.family.toLowerCase().includes(query)); + res.json(matches); +}); + +// Health check endpoint +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +const port = process.env.PORT || 3000; +app.listen(port, '0.0.0.0', () => { + console.log(`🗺️ Ora Map Backend API running at http://localhost:${port}`); + console.log(`📍 Search endpoint: http://localhost:${port}/api/search`); + console.log(`💚 Health endpoint: http://localhost:${port}/api/health`); +}); diff --git a/docker-compose.microservices.yml b/docker-compose.microservices.yml new file mode 100644 index 0000000..1bdf9b6 --- /dev/null +++ b/docker-compose.microservices.yml @@ -0,0 +1,48 @@ +version: '3.8' + +services: + # Backend API Service + backend: + build: + context: ./backend + dockerfile: Dockerfile + image: oramap-backend:latest + container_name: oramap-backend + environment: + - NODE_ENV=production + - PORT=3000 + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - oramap-network + + # Frontend Nginx Service + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + image: oramap-frontend:latest + container_name: oramap-frontend + ports: + - "80:80" + depends_on: + backend: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 5s + networks: + - oramap-network + +networks: + oramap-network: + driver: bridge diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..1e64f62 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,31 @@ +# Frontend .dockerignore +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Source files (not needed for production) +src/ + +# Build files +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Docker +Dockerfile +.dockerignore + +# Documentation +README.md +*.md diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..cd6c5d2 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +# Frontend Dockerfile for Ora Map (Static files with Nginx) +FROM nginx:alpine + +# Copy static files to nginx html directory +COPY public/ /usr/share/nginx/html/ + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..dfd8092 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # Main location + location / { + try_files $uri $uri/ /index.html; + } + + # API proxy to backend + location /api/ { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Cache static assets + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..86d16e9 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,26 @@ + + +
+ +
+ Shevach
+ `;
+ return div;
+ },
+
+ onRemove: function () {
+ // Nothing to clean up
+ }
+});
+
+L.control.logo({ position: 'bottomleft' }).addTo(map);
\ No newline at end of file
diff --git a/frontend/public/style.css b/frontend/public/style.css
new file mode 100644
index 0000000..12ddc35
--- /dev/null
+++ b/frontend/public/style.css
@@ -0,0 +1,98 @@
+/* Reset some basic styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: #f4f4f4;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ }
+
+ /* Header styles */
+ header {
+ background-color: #2c3e50;
+ padding: 20px;
+ text-align: center;
+ color: white;
+ }
+
+ h1 {
+ margin-bottom: 10px;
+ }
+
+ .search-bar {
+ margin-top: 10px;
+ }
+
+ .search-bar input {
+ padding: 10px;
+ width: 250px;
+ border: none;
+ border-radius: 5px;
+ margin-right: 8px;
+ }
+
+ .search-bar button {
+ padding: 10px 15px;
+ border: none;
+ border-radius: 5px;
+ background-color: #3498db;
+ color: white;
+ cursor: pointer;
+ font-weight: bold;
+ }
+
+ .suggestions {
+ background: #363251;
+ border: 1px solid #ccc;
+ position: absolute;
+ top: 110%; /* Just below the input */
+ left: 48%;
+ transform: translateX(-50%);
+ max-width: fit-content;
+ min-width: 200px; /* Optional: minimum size */
+ max-height: 200px;
+ overflow-y: auto;
+ z-index: 1000;
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ transition: all 0.2s ease-in-out;
+ }
+
+ .suggestions div {
+ padding: 10px;
+ cursor: pointer;
+ }
+
+ .suggestions div:hover {
+ background-color: #95a1a8;
+ }
+
+ .custom-logo {
+ background: rgba(255, 255, 255, 0.9);
+ padding: 6px 10px;
+ border-radius: 8px;
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
+ display: flex;
+ align-items: center;
+ font-family: sans-serif;
+ }
+
+ /* Map container */
+ #map {
+ flex-grow: 1;
+ height: 80vh;
+ width: 100%;
+ }
+
+ /* Main container */
+ main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
\ No newline at end of file
diff --git a/frontend/src/components/Search.jsx b/frontend/src/components/Search.jsx
new file mode 100644
index 0000000..0dfe645
--- /dev/null
+++ b/frontend/src/components/Search.jsx
@@ -0,0 +1,45 @@
+import { useState, useEffect } from 'react';
+
+function Search() {
+ const [query, setQuery] = useState('');
+ const [results, setResults] = useState([]);
+
+ useEffect(() => {
+ if (query.trim() === '') {
+ setResults([]);
+ return;
+ }
+
+ const fetchSuggestions = async () => {
+ try {
+ const res = await fetch(`/search?q=${encodeURIComponent(query)}`);
+ const data = await res.json();
+ setResults(data);
+ } catch (err) {
+ console.error('Failed to fetch suggestions:', err);
+ }
+ };
+
+ fetchSuggestions();
+ }, [query]);
+
+ return (
+