diff --git a/README.md b/README.md index 5617078..f7bb7c5 100644 --- a/README.md +++ b/README.md @@ -16,55 +16,86 @@ A full-stack web application for mapping and searching family locations in Yemen - πŸ” **Family Search**: Search for families by name with autocomplete suggestions - πŸ—ΊοΈ **Interactive Map**: Leaflet-based map with multiple tile layer options - πŸ“ **Location Markers**: View family locations with city information -- 🎨 **Modern UI**: Clean and responsive design +- βž• **Add Families**: Admin UI to add new family locations to the database +- πŸ’Ύ **MongoDB Database**: Real database persistence with full CRUD operations +- 🎨 **Modern UI**: Clean and responsive design with modal forms - 🐳 **Docker Ready**: Containerized for easy deployment +- ☸️ **Kubernetes Ready**: Helm chart for production deployment - πŸ’š **Health Checks**: Built-in health monitoring ## πŸ› οΈ Tech Stack **Backend:** -- Node.js -- Express.js -- JSON data storage +- Node.js 20 Alpine +- Express.js 4.18 +- MongoDB 7.0 +- Mongoose ODM +- CORS enabled **Frontend:** - HTML5/CSS3 -- JavaScript (ES6+) +- Vanilla JavaScript (ES6+) - Leaflet.js (interactive maps) - Fuse.js (fuzzy search) +- Nginx (production server) **DevOps:** -- Docker -- Docker Compose +- Docker & Docker Compose +- Kubernetes & Helm Charts +- Woodpecker CI/CD +- Harbor Registry -## πŸ“ Project Structure - -``` -oramap/ -β”œβ”€β”€ backend/ -β”‚ β”œβ”€β”€ server.js # Express server +## πŸ“ Project Structure with MongoDB β”‚ β”œβ”€β”€ package.json # Backend dependencies β”‚ β”œβ”€β”€ Dockerfile # Backend container image -β”‚ └── data/ -β”‚ └── families.json # Family location data +β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ └── database.js # MongoDB connection +β”‚ β”œβ”€β”€ models/ +β”‚ β”‚ └── Family.js # Mongoose schema +β”‚ └── scripts/ +β”‚ └── seed.js # Database seeding script β”œβ”€β”€ frontend/ β”‚ β”œβ”€β”€ Dockerfile # Frontend Nginx container β”‚ β”œβ”€β”€ nginx.conf # Nginx configuration β”‚ └── public/ -β”‚ β”œβ”€β”€ index.html # Frontend HTML -β”‚ β”œβ”€β”€ script.js # Frontend JavaScript -β”‚ └── style.css # Styles -β”œβ”€β”€ public/ -β”‚ β”œβ”€β”€ index.html # Shared frontend files +β”‚ β”œβ”€β”€ index.html # Frontend HTML with modal form +β”‚ β”œβ”€β”€ script.js # Frontend JavaScript with CRUD +β”‚ └── style.css # Styles including modal +β”œβ”€β”€ oramap/ # Helm Chart +β”‚ β”œβ”€β”€ Chart.yaml # Chart metadata (v0.3.0) +β”‚ β”œβ”€β”€ values.yaml # Configuration values +β”‚ β”œβ”€β”€ README.md # Helm documentation +β”‚ └── templates/ +β”‚ β”œβ”€β”€ deployment.yaml # Backend & Frontend deployments +β”‚ β”œβ”€β”€ service.yaml # Services +β”‚ β”œβ”€β”€ ingress.yaml # Ingress rules +β”‚ β”œβ”€β”€ configmap.yaml # Nginx config +β”‚ β”œβ”€β”€ mongodb-statefulset.yaml # MongoDB StatefulSet +β”‚ └── mongodb-service.yaml # MongoDB service +β”œβ”€β”€ old/ # Legacy files +β”‚ β”œβ”€β”€ server.js +β”‚ β”œβ”€β”€ package.json +β”‚ └── data/families.json +β”œβ”€β”€ Dockerfile # Monolith Docker build +β”œβ”€β”€ docker-compose.yml # Monolith deployment +β”œβ”€β”€ docker-compose.microservices.yml # Microservices with MongoDB β”‚ β”œβ”€β”€ 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 -``` +β”œβ”€β”€ MongoDB**: Database for family locations (Port 27017) +- **Backend**: Express API server with MongoDB integration (Internal Port 3000) +- **Frontend**: Nginx serving static files with API proxy (Port 80) +- Nginx proxies `/api/*` requests to backend +- Best for: Production, scalability, CI/CD pipelines +- Use: `docker-compose -f docker-compose.microservices.yml up` +### 3. **Kubernetes Mode** (Enterprise) +- Helm chart deployment with StatefulSet for MongoDB +- Persistent storage for database +- Horizontal scaling for backend/frontend +- Ingress with TLS support +- See: `oramap/README.md` for Helm chart documentation ## πŸ—οΈ Architecture The application supports two deployment modes: @@ -89,18 +120,33 @@ The application supports two deployment modes: ### Local Development -1. **Install backend dependencies:** +1. **Start MongoDB (optional - uses local instance if not running):** +```bash +# Using Docker +docker run -d -p 27017:27017 --name mongodb mongo:7.0 + +# Or install MongoDB locally +``` + +2. **Install backend dependencies:** ```bash cd backend npm install ``` -2. **Start the server:** +3. **Seed initial data:** ```bash -npm start +npm run seed ``` -3. **Open your browser:** +4. **Start the server:** +```bash +npm start +# Or for development with auto-reload: +npm run dev +``` + +5. **Open your browser:** Navigate to `http://localhost:3000` ## 🐳 Docker Deployment @@ -121,22 +167,30 @@ docker-compose down ### Microservices Mode (Recommended for Production) -1. **Build and start:** +1. **Build and start all services (Backend + Frontend + MongoDB):** ```bash docker-compose -f docker-compose.microservices.yml up -d ``` -2. **Access:** http://localhost (Port 80) +2. **Seed the database:** +```bash +docker exec -it oramap-backend npm run seed +``` -3. **View logs:** +3. **Access:** http://localhost (Port 80) + +4. **View logs:** ```bash docker logs oramap-frontend docker logs oramap-backend +docker logs oramap-mongo ``` -4. **Stop:** +5. **Stop:** ```bash docker-compose -f docker-compose.microservices.yml down +# To remove volumes: +docker-compose -f docker-compose.microservices.yml down -v ``` ### Using Docker directly @@ -184,36 +238,144 @@ cd backend npm run dev ``` +### Database Management + +**Seed initial data:** +```bash +npm run seed +``` + +**Force re-seed (clears existing data):** +```bash +npm run seed:force +``` + ### Adding New Families -Edit `backend/data/families.json` and add entries in the following format: +Use the web UI "Add Family" button, or make API calls: -```json -{ - "family": "Family Name (Hebrew)", - "city": "City Name (Hebrew)", - "lat": 15.3545, - "lng": 44.2064 -} +```bash +curl -X POST http://localhost:3000/api/families \ + -H "Content-Type: application/json" \ + -d '{ + "family": "Family Name", + "city": "City Name", + "lat": 15.3545, + "lng": 44.2064 + }' ``` ## πŸ“‘ API Endpoints +### Get All Families +``` +GET /api/families +``` +Returns all families sorted by name. + +**Example:** +```bash +curl http://localhost:3000/api/families +``` + +**Response:** +```json +[ + { + "_id": "507f1f77bcf86cd799439011", + "family": "ΧžΧ©Χ€Χ—Χͺ Χ›Χ€Χ”", + "city": "צנגא", + "lat": 15.3545, + "lng": 44.2064, + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + } +] +``` + ### Search Families ``` GET /api/search?family={familyName} ``` -Returns matching family records with location data. +Returns matching family records using MongoDB text search. **Example:** ```bash curl "http://localhost:3000/api/search?family=Kafe" ``` +### Create Family +``` +POST /api/families +Content-Type: application/json +``` +Creates a new family location. + +**Request Body:** +```json +{ + "family": "ΧžΧ©Χ€Χ—Χͺ Χ“Χ•Χ“", + "city": "Χ’Χ“ΧŸ", + "lat": 12.7855, + "lng": 45.0187 +} +``` + +**Response:** +```json +{ + "_id": "507f1f77bcf86cd799439011", + "family": "ΧžΧ©Χ€Χ—Χͺ Χ“Χ•Χ“", + "city": "Χ’Χ“ΧŸ", + "lat": 12.7855, + "lng": 45.0187, + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" +} +``` + +### Update Family +``` +PUT /api/families/:id +Content-Type: application/json +``` +Updates an existing family. + +**Request Body:** +```json +{ + "family": "ΧžΧ©Χ€Χ—Χͺ Χ›Χ”ΧŸ", + "city": "צנגא", + "lat": 15.3695, + "lng": 44.1910 +} +``` + +### Delete Family +``` +DELETE /api/families/:id +``` +Deletes a family by ID. + +**Example:** +```bash +curl -X DELETE http://localhost:3000/api/families/507f1f77bcf86cd799439011 +``` + ### Health Check ``` GET /api/health ``` +Returns server and database health status. + +**Response:** +```json +{ + "status": "ok", + "timestamp": "2024-01-01T00:00:00.000Z", + "database": "connected" +} +``` Returns server health status. **Example Response:** diff --git a/backend/config/database.js b/backend/config/database.js new file mode 100644 index 0000000..269da09 --- /dev/null +++ b/backend/config/database.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); + +const connectDB = async () => { + try { + const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/oramap'; + + await mongoose.connect(mongoURI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + console.log('βœ… MongoDB connected successfully'); + console.log(`πŸ“ Database: ${mongoose.connection.name}`); + } catch (error) { + console.error('❌ MongoDB connection error:', error.message); + process.exit(1); + } +}; + +module.exports = connectDB; diff --git a/backend/models/Family.js b/backend/models/Family.js new file mode 100644 index 0000000..79b7842 --- /dev/null +++ b/backend/models/Family.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose'); + +const familySchema = new mongoose.Schema({ + family: { + type: String, + required: true, + trim: true + }, + city: { + type: String, + required: true, + trim: true + }, + lat: { + type: Number, + required: true, + min: -90, + max: 90 + }, + lng: { + type: Number, + required: true, + min: -180, + max: 180 + } +}, { + timestamps: true +}); + +// Index for faster searches +familySchema.index({ family: 'text', city: 'text' }); + +module.exports = mongoose.model('Family', familySchema); diff --git a/backend/package.json b/backend/package.json index c8ec596..38f0a1b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,13 +5,16 @@ "main": "server.js", "scripts": { "start": "node server.js", - "dev": "node server.js" + "dev": "node server.js", + "seed": "node scripts/seed.js", + "seed:force": "node scripts/seed.js --force" }, "keywords": ["map", "family", "leaflet", "express"], "author": "", "license": "ISC", "dependencies": { "express": "^4.18.2", - "cors": "^2.8.5" + "cors": "^2.8.5", + "mongoose": "^7.6.0" } } diff --git a/backend/scripts/seed.js b/backend/scripts/seed.js new file mode 100644 index 0000000..506a0db --- /dev/null +++ b/backend/scripts/seed.js @@ -0,0 +1,59 @@ +const mongoose = require('mongoose'); +const Family = require('../models/Family'); + +const initialData = [ + { "family": "Kafe (קא׀ח)", "city": "Sana'a (צנגא)", "lat": 15.3545, "lng": 44.2064 }, + { "family": "Shiheb (Χ©Χ—Χ‘-Χ©Χ‘Χ—)", "city": "Sana'a (צנגא)", "lat": 15.3545, "lng": 44.2064 }, + { "family": "Uzeyri (Χ’Χ–Χ™Χ¨Χ™-Χ’Χ•Χ–Χ¨Χ™)", "city": "Sana'a (צנגא)", "lat": 15.3545, "lng": 44.2064 }, + { "family": "Uzeyri (Χ’Χ–Χ™Χ¨Χ™-Χ’Χ•Χ–Χ¨Χ™)", "city": "Manakhah (ΧžΧ ΧΧ›Χ”)", "lat": 15.3019, "lng": 43.5983 }, + { "family": "Uzeyri (Χ’Χ–Χ™Χ¨Χ™-Χ’Χ•Χ–Χ¨Χ™)", "city": "Dhamar (Χ“'מאר)", "lat": 14.5424, "lng": 44.4056 }, + { "family": "Salumi (Χ‘ΧœΧ•ΧžΧ™-Χ©ΧœΧ•ΧžΧ™)", "city": "Al Kafla (אל Χ§Χ€ΧœΧ”)", "lat": 16.0240, "lng": 43.9790 }, + { "family": "Afgin (Χ’Χ€Χ’'Χ™ΧŸ)", "city": "Sa'dah (Χ¦Χ’Χ“Χ”)", "lat": 16.9402, "lng": 43.7639 }, + { "family": "Eraki (גראקי)", "city": "Sana'a (צנגא)", "lat": 15.3545, "lng": 44.2064 } +]; + +async function seedDatabase() { + try { + const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/oramap'; + + await mongoose.connect(mongoURI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + console.log('βœ… Connected to MongoDB'); + + // Check if data already exists + const existingCount = await Family.countDocuments(); + + if (existingCount > 0) { + console.log(`ℹ️ Database already has ${existingCount} families`); + const answer = process.argv.includes('--force'); + + if (answer) { + console.log('πŸ—‘οΈ Clearing existing data...'); + await Family.deleteMany({}); + } else { + console.log('ℹ️ Skipping seed. Use --force flag to override existing data'); + process.exit(0); + } + } + + // Insert initial data + console.log('πŸ“ Seeding database...'); + const result = await Family.insertMany(initialData); + + console.log(`βœ… Successfully seeded ${result.length} families`); + console.log('\nAdded families:'); + result.forEach(family => { + console.log(` - ${family.family} | ${family.city}`); + }); + + process.exit(0); + } catch (error) { + console.error('❌ Seed error:', error); + process.exit(1); + } +} + +seedDatabase(); diff --git a/backend/server.js b/backend/server.js index 1cb8c37..354e649 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,28 +1,156 @@ const express = require('express'); +const cors = require('cors'); const path = require('path'); -const app = express(); -const families = require('./data/families.json'); +const connectDB = require('./config/database'); +const Family = require('./models/Family'); -// Serve static files from the public directory -app.use(express.static(path.join(__dirname, '../public'))); +const app = express(); + +// Enable CORS for frontend +app.use(cors()); + +// Body parser middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Connect to MongoDB +connectDB(); + +// 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([]); +app.get('/api/search', async (req, res) => { + try { + const query = req.query.family?.toLowerCase(); + + if (!query) { + return res.json([]); + } + + // Search by family name (case-insensitive) + const matches = await Family.find({ + family: { $regex: query, $options: 'i' } + }).select('-__v -createdAt -updatedAt'); + + res.json(matches); + } catch (error) { + console.error('Search error:', error); + res.status(500).json({ error: 'Search failed', message: error.message }); + } +}); + +// Get all families +app.get('/api/families', async (req, res) => { + try { + const families = await Family.find() + .select('-__v -createdAt -updatedAt') + .sort({ family: 1 }); + + res.json(families); + } catch (error) { + console.error('Get families error:', error); + res.status(500).json({ error: 'Failed to fetch families', message: error.message }); + } +}); + +// Create new family +app.post('/api/families', async (req, res) => { + try { + const { family, city, lat, lng } = req.body; + + // Validation + if (!family || !city || lat === undefined || lng === undefined) { + return res.status(400).json({ + error: 'Missing required fields', + required: ['family', 'city', 'lat', 'lng'] + }); + } + + // Validate coordinates + if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { + return res.status(400).json({ + error: 'Invalid coordinates', + message: 'Latitude must be between -90 and 90, Longitude between -180 and 180' + }); + } + + const newFamily = new Family({ family, city, lat, lng }); + await newFamily.save(); + + res.status(201).json({ + message: 'Family added successfully', + family: newFamily + }); + } catch (error) { + console.error('Create family error:', error); + res.status(500).json({ error: 'Failed to create family', message: error.message }); + } +}); + +// Update family +app.put('/api/families/:id', async (req, res) => { + try { + const { id } = req.params; + const { family, city, lat, lng } = req.body; + + const updatedFamily = await Family.findByIdAndUpdate( + id, + { family, city, lat, lng }, + { new: true, runValidators: true } + ); + + if (!updatedFamily) { + return res.status(404).json({ error: 'Family not found' }); + } + + res.json({ + message: 'Family updated successfully', + family: updatedFamily + }); + } catch (error) { + console.error('Update family error:', error); + res.status(500).json({ error: 'Failed to update family', message: error.message }); + } +}); + +// Delete family +app.delete('/api/families/:id', async (req, res) => { + try { + const { id } = req.params; + + const deletedFamily = await Family.findByIdAndDelete(id); + + if (!deletedFamily) { + return res.status(404).json({ error: 'Family not found' }); + } + + res.json({ + message: 'Family deleted successfully', + family: deletedFamily + }); + } catch (error) { + console.error('Delete family error:', error); + res.status(500).json({ error: 'Failed to delete family', message: error.message }); } - 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 mongoose = require('mongoose'); + const dbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected'; + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + database: dbStatus + }); }); const port = process.env.PORT || 3000; app.listen(port, '0.0.0.0', () => { - console.log(`πŸ—ΊοΈ Ora Map Server running at http://localhost:${port}`); - console.log(`πŸ“ API endpoint: http://localhost:${port}/api/search`); + 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 index 1bdf9b6..383825a 100644 --- a/docker-compose.microservices.yml +++ b/docker-compose.microservices.yml @@ -1,6 +1,26 @@ version: '3.8' services: + # MongoDB Database Service + mongo: + image: mongo:7.0 + container_name: oramap-mongo + environment: + - MONGO_INITDB_DATABASE=oramap + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + restart: unless-stopped + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - oramap-network + # Backend API Service backend: build: @@ -11,6 +31,10 @@ services: environment: - NODE_ENV=production - PORT=3000 + - MONGODB_URI=mongodb://mongo:27017/oramap + depends_on: + mongo: + condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] @@ -46,3 +70,7 @@ services: networks: oramap-network: driver: bridge + +volumes: + mongo-data: + driver: local diff --git a/frontend/public/index.html b/frontend/public/index.html index 86d16e9..19a00f8 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -12,9 +12,41 @@ + + +
diff --git a/frontend/public/script.js b/frontend/public/script.js index 2e213a5..1a0ec62 100644 --- a/frontend/public/script.js +++ b/frontend/public/script.js @@ -121,4 +121,89 @@ L.Control.Logo = L.Control.extend({ } }); -L.control.logo({ position: 'bottomleft' }).addTo(map); \ No newline at end of file +L.control.logo({ position: 'bottomleft' }).addTo(map); + +// Add Family Form Functions +function toggleAddFamilyForm() { + const modal = document.getElementById('addFamilyModal'); + const form = document.getElementById('addFamilyForm'); + const message = document.getElementById('formMessage'); + + if (modal.style.display === 'block') { + modal.style.display = 'none'; + form.reset(); + message.textContent = ''; + message.className = 'form-message'; + } else { + modal.style.display = 'block'; + } +} + +async function addFamily(event) { + event.preventDefault(); + + const familyName = document.getElementById('familyName').value.trim(); + const cityName = document.getElementById('cityName').value.trim(); + const latitude = parseFloat(document.getElementById('latitude').value); + const longitude = parseFloat(document.getElementById('longitude').value); + + const messageEl = document.getElementById('formMessage'); + messageEl.textContent = 'Adding family...'; + messageEl.className = 'form-message info'; + + try { + const response = await fetch('/api/families', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + family: familyName, + city: cityName, + lat: latitude, + lng: longitude + }) + }); + + const data = await response.json(); + + if (response.ok) { + messageEl.textContent = 'βœ… Family added successfully!'; + messageEl.className = 'form-message success'; + + // Add marker to map + L.marker([latitude, longitude]).addTo(map) + .bindPopup(`${familyName}
City: ${cityName}`) + .openPopup(); + + map.setView([latitude, longitude], 10); + + // Reset form after 2 seconds + setTimeout(() => { + toggleAddFamilyForm(); + }, 2000); + } else { + messageEl.textContent = `❌ Error: ${data.error || data.message}`; + messageEl.className = 'form-message error'; + } + } catch (error) { + console.error('Add family error:', error); + messageEl.textContent = '❌ Failed to add family. Please try again.'; + messageEl.className = 'form-message error'; + } +} + +// Close modal when clicking outside +window.onclick = function(event) { + const modal = document.getElementById('addFamilyModal'); + if (event.target === modal) { + toggleAddFamilyForm(); + } +} + +// Allow Enter key to trigger search +document.getElementById('searchInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + searchFamily(); + } +}); \ No newline at end of file diff --git a/frontend/public/style.css b/frontend/public/style.css index 12ddc35..29ba7fd 100644 --- a/frontend/public/style.css +++ b/frontend/public/style.css @@ -95,4 +95,144 @@ flex: 1; display: flex; flex-direction: column; + } + + /* Modal styles */ + .modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + } + + .modal-content { + background-color: #fefefe; + margin: 5% auto; + padding: 30px; + border-radius: 10px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + .modal-content h2 { + margin-bottom: 20px; + color: #2c3e50; + } + + .close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + line-height: 20px; + cursor: pointer; + } + + .close:hover, + .close:focus { + color: #000; + } + + .form-group { + margin-bottom: 15px; + } + + .form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #2c3e50; + } + + .form-group input { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 14px; + } + + .form-group input:focus { + outline: none; + border-color: #3498db; + } + + .form-actions { + display: flex; + gap: 10px; + margin-top: 20px; + } + + .form-actions button { + flex: 1; + padding: 10px; + border: none; + border-radius: 5px; + cursor: pointer; + font-weight: bold; + } + + .btn-submit { + background-color: #27ae60; + color: white; + } + + .btn-submit:hover { + background-color: #229954; + } + + .btn-cancel { + background-color: #95a5a6; + color: white; + } + + .btn-cancel:hover { + background-color: #7f8c8d; + } + + .form-message { + padding: 10px; + border-radius: 5px; + margin-bottom: 15px; + text-align: center; + font-weight: bold; + } + + .form-message.success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + .form-message.error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + + .form-message.info { + background-color: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; + } + + /* Add Family button */ + .btn-add-family { + padding: 10px 15px; + border: none; + border-radius: 5px; + background-color: #27ae60; + color: white; + cursor: pointer; + font-weight: bold; + margin-left: 8px; + } + + .btn-add-family:hover { + background-color: #229954; } \ No newline at end of file diff --git a/oramap/Chart.yaml b/oramap/Chart.yaml index 8e91c0a..39a679f 100644 --- a/oramap/Chart.yaml +++ b/oramap/Chart.yaml @@ -1,11 +1,11 @@ apiVersion: v2 name: oramap -description: Ora Map - Family Location Mapping Application (Microservices Architecture) +description: Ora Map - Family Location Mapping Application with MongoDB (Microservices Architecture) type: application # Chart version -version: 0.2.0 +version: 0.3.0 # Application version appVersion: "1.0.0" diff --git a/oramap/README.md b/oramap/README.md index 7693ff5..40c0132 100644 --- a/oramap/README.md +++ b/oramap/README.md @@ -4,10 +4,11 @@ Helm chart for deploying Ora Map application in Kubernetes with microservices ar ## Architecture -This chart deploys two main components: +This chart deploys three main components: -- **Backend**: Node.js Express API server (Port 3000) -- **Frontend**: Nginx serving static files (Port 80) +- **Backend**: Node.js Express API server with MongoDB integration (Port 3000) +- **Frontend**: Nginx serving static files with API proxy (Port 80) +- **MongoDB**: Database for storing family location data (Port 27017) ## Installation @@ -62,6 +63,13 @@ The following table lists the configurable parameters and their default values: | `ingress.enabled` | Enable ingress | `true` | | `ingress.className` | Ingress class name | `traefik` | | `ingress.hosts[0].host` | Ingress hostname | `oramap.dvirlabs.com` | +| `mongodb.enabled` | Enable MongoDB deployment | `true` | +| `mongodb.image.repository` | MongoDB image repository | `mongo` | +| `mongodb.image.tag` | MongoDB image tag | `7.0` | +| `mongodb.persistence.enabled` | Enable persistent storage | `true` | +| `mongodb.persistence.size` | PVC size | `5Gi` | +| `mongodb.persistence.storageClass` | Storage class name | `""` (default) | +| `mongodb.database` | Database name | `oramap` | ### Example Custom Values @@ -70,6 +78,12 @@ The following table lists the configurable parameters and their default values: backend: image: tag: "v1.0.0" +mongodb: + persistence: + enabled: true + size: 10Gi + storageClass: "fast-ssd" + replicaCount: 3 frontend: @@ -107,13 +121,55 @@ ingress: ### Services - **Backend Service**: Internal ClusterIP service on port 3000 -- **Frontend Service**: ClusterIP service on port 80 (exposed via Ingress) +- **Frontend Service**: ClusterIP -### Ingress +### MongoDB StatefulSet -- Routes external traffic to frontend service -- TLS/SSL termination support -- Configurable hostname and paths +- Persistent database storage for family data +- Health checks using mongosh +- Configurable storage size and class +- Automatic PVC creation + +## Database Setup + +### Initial Data Seeding + +After deployment, seed the database with initial family data: + +```bash +# Access the backend pod +kubectl exec -it deployment/oramap-backend -- sh + +# Run the seed script +npm run seed + +# Or force re-seed +npm run seed:force +``` + +### MongoDB Access + +```bash +# Port-forward to MongoDB +kubectl port-forward svc/oramap-mongodb 27017:27017 + +# Check StatefulSet +kubectl get statefulset oramap-mongodb +kubectl get pvc +``` + +### View Logs + +```bash +# Backend logs +kubectl logs -l app=oramap-backend + +# Frontend logs +kubectl logs -l app=oramap-frontend + +# MongoDB logs +kubectl logs oramap-mongodb-0 +``` ## CI/CD Integration diff --git a/oramap/templates/deployment.yaml b/oramap/templates/deployment.yaml index d8ee38a..b6cd85c 100644 --- a/oramap/templates/deployment.yaml +++ b/oramap/templates/deployment.yaml @@ -36,6 +36,10 @@ spec: value: "production" - name: PORT value: "{{ .Values.backend.containerPort }}" + {{- if .Values.mongodb.enabled }} + - name: MONGODB_URI + value: "mongodb://{{ include \"oramap.fullname\" . }}-mongodb:27017/{{ .Values.mongodb.database }}" + {{- end }} livenessProbe: httpGet: path: /api/health diff --git a/oramap/templates/mongodb-service.yaml b/oramap/templates/mongodb-service.yaml new file mode 100644 index 0000000..f8004ab --- /dev/null +++ b/oramap/templates/mongodb-service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.mongodb.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "oramap.fullname" . }}-mongodb + labels: + {{- include "oramap.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + type: ClusterIP + clusterIP: None + ports: + - port: 27017 + targetPort: 27017 + protocol: TCP + name: mongodb + selector: + {{- include "oramap.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: database +{{- end }} diff --git a/oramap/templates/mongodb-statefulset.yaml b/oramap/templates/mongodb-statefulset.yaml new file mode 100644 index 0000000..832316d --- /dev/null +++ b/oramap/templates/mongodb-statefulset.yaml @@ -0,0 +1,75 @@ +{{- if .Values.mongodb.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "oramap.fullname" . }}-mongodb + labels: + {{- include "oramap.labels" . | nindent 4 }} + app.kubernetes.io/component: database +spec: + serviceName: {{ include "oramap.fullname" . }}-mongodb + replicas: 1 + selector: + matchLabels: + {{- include "oramap.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: database + template: + metadata: + labels: + {{- include "oramap.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: database + spec: + containers: + - name: mongodb + image: "{{ .Values.mongodb.image.repository }}:{{ .Values.mongodb.image.tag }}" + imagePullPolicy: {{ .Values.mongodb.image.pullPolicy }} + ports: + - name: mongodb + containerPort: 27017 + protocol: TCP + env: + - name: MONGO_INITDB_DATABASE + value: {{ .Values.mongodb.database }} + livenessProbe: + exec: + command: + - mongosh + - --eval + - "db.adminCommand('ping')" + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + readinessProbe: + exec: + command: + - mongosh + - --eval + - "db.adminCommand('ping')" + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + {{- toYaml .Values.mongodb.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /data/db + {{- if .Values.mongodb.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + {{- if .Values.mongodb.persistence.storageClass }} + storageClassName: {{ .Values.mongodb.persistence.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.mongodb.persistence.size }} + {{- else }} + volumes: + - name: data + emptyDir: {} + {{- end }} +{{- end }} diff --git a/oramap/values.yaml b/oramap/values.yaml index a171e88..7e5a009 100644 --- a/oramap/values.yaml +++ b/oramap/values.yaml @@ -41,6 +41,26 @@ service: port: 80 targetPort: 80 +# MongoDB configuration +mongodb: + enabled: true + image: + repository: mongo + tag: "7.0" + pullPolicy: IfNotPresent + persistence: + enabled: true + storageClass: "" + size: 5Gi + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + database: oramap + ingress: enabled: true className: "traefik"