Add MongoDB integration with CRUD UI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- Integrated MongoDB 7.0 with Mongoose ODM
- Added CRUD API endpoints (GET, POST, PUT, DELETE)
- Created Family model with validation
- Added database seeding script with initial data
- Implemented Add Family modal form in frontend
- Updated docker-compose with MongoDB service
- Updated Helm chart to v0.3.0 with MongoDB StatefulSet
- Updated documentation with MongoDB setup instructions
This commit is contained in:
dvirlabs 2026-03-25 01:51:46 +02:00
parent f3da9b66c3
commit 02074aa4a6
16 changed files with 932 additions and 67 deletions

244
README.md
View File

@ -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 - 🔍 **Family Search**: Search for families by name with autocomplete suggestions
- 🗺️ **Interactive Map**: Leaflet-based map with multiple tile layer options - 🗺️ **Interactive Map**: Leaflet-based map with multiple tile layer options
- 📍 **Location Markers**: View family locations with city information - 📍 **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 - 🐳 **Docker Ready**: Containerized for easy deployment
- ☸️ **Kubernetes Ready**: Helm chart for production deployment
- 💚 **Health Checks**: Built-in health monitoring - 💚 **Health Checks**: Built-in health monitoring
## 🛠️ Tech Stack ## 🛠️ Tech Stack
**Backend:** **Backend:**
- Node.js - Node.js 20 Alpine
- Express.js - Express.js 4.18
- JSON data storage - MongoDB 7.0
- Mongoose ODM
- CORS enabled
**Frontend:** **Frontend:**
- HTML5/CSS3 - HTML5/CSS3
- JavaScript (ES6+) - Vanilla JavaScript (ES6+)
- Leaflet.js (interactive maps) - Leaflet.js (interactive maps)
- Fuse.js (fuzzy search) - Fuse.js (fuzzy search)
- Nginx (production server)
**DevOps:** **DevOps:**
- Docker - Docker & Docker Compose
- Docker Compose - Kubernetes & Helm Charts
- Woodpecker CI/CD
- Harbor Registry
## 📁 Project Structure ## 📁 Project Structure with MongoDB
```
oramap/
├── backend/
│ ├── server.js # Express server
│ ├── package.json # Backend dependencies │ ├── package.json # Backend dependencies
│ ├── Dockerfile # Backend container image │ ├── Dockerfile # Backend container image
│ └── data/ │ ├── config/
│ └── families.json # Family location data │ │ └── database.js # MongoDB connection
│ ├── models/
│ │ └── Family.js # Mongoose schema
│ └── scripts/
│ └── seed.js # Database seeding script
├── frontend/ ├── frontend/
│ ├── Dockerfile # Frontend Nginx container │ ├── Dockerfile # Frontend Nginx container
│ ├── nginx.conf # Nginx configuration │ ├── nginx.conf # Nginx configuration
│ └── public/ │ └── public/
│ ├── index.html # Frontend HTML │ ├── index.html # Frontend HTML with modal form
│ ├── script.js # Frontend JavaScript │ ├── script.js # Frontend JavaScript with CRUD
│ └── style.css # Styles │ └── style.css # Styles including modal
├── public/ ├── oramap/ # Helm Chart
│ ├── index.html # Shared frontend files │ ├── 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 │ ├── script.js
│ └── style.css │ └── style.css
├── Dockerfile # Monolith Docker build ├── Dockerfile # Monolith Docker build
├── docker-compose.yml # Monolith deployment ├── docker-compose.yml # Monolith deployment
├── docker-compose.microservices.yml # Microservices deployment ├── MongoDB**: Database for family locations (Port 27017)
├── .woodpecker.yaml # CI/CD pipeline config - **Backend**: Express API server with MongoDB integration (Internal Port 3000)
└── .dockerignore # Docker ignore rules - **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 ## 🏗️ Architecture
The application supports two deployment modes: The application supports two deployment modes:
@ -89,18 +120,33 @@ The application supports two deployment modes:
### Local Development ### 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 ```bash
cd backend cd backend
npm install npm install
``` ```
2. **Start the server:** 3. **Seed initial data:**
```bash ```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` Navigate to `http://localhost:3000`
## 🐳 Docker Deployment ## 🐳 Docker Deployment
@ -121,22 +167,30 @@ docker-compose down
### Microservices Mode (Recommended for Production) ### Microservices Mode (Recommended for Production)
1. **Build and start:** 1. **Build and start all services (Backend + Frontend + MongoDB):**
```bash ```bash
docker-compose -f docker-compose.microservices.yml up -d 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 ```bash
docker logs oramap-frontend docker logs oramap-frontend
docker logs oramap-backend docker logs oramap-backend
docker logs oramap-mongo
``` ```
4. **Stop:** 5. **Stop:**
```bash ```bash
docker-compose -f docker-compose.microservices.yml down docker-compose -f docker-compose.microservices.yml down
# To remove volumes:
docker-compose -f docker-compose.microservices.yml down -v
``` ```
### Using Docker directly ### Using Docker directly
@ -184,36 +238,144 @@ cd backend
npm run dev 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 ### 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 ```bash
{ curl -X POST http://localhost:3000/api/families \
"family": "Family Name (Hebrew)", -H "Content-Type: application/json" \
"city": "City Name (Hebrew)", -d '{
"lat": 15.3545, "family": "Family Name",
"lng": 44.2064 "city": "City Name",
} "lat": 15.3545,
"lng": 44.2064
}'
``` ```
## 📡 API Endpoints ## 📡 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 ### Search Families
``` ```
GET /api/search?family={familyName} GET /api/search?family={familyName}
``` ```
Returns matching family records with location data. Returns matching family records using MongoDB text search.
**Example:** **Example:**
```bash ```bash
curl "http://localhost:3000/api/search?family=Kafe" 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 ### Health Check
``` ```
GET /api/health 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. Returns server health status.
**Example Response:** **Example Response:**

View File

@ -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;

33
backend/models/Family.js Normal file
View File

@ -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);

View File

@ -5,13 +5,16 @@
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "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"], "keywords": ["map", "family", "leaflet", "express"],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"cors": "^2.8.5" "cors": "^2.8.5",
"mongoose": "^7.6.0"
} }
} }

59
backend/scripts/seed.js Normal file
View File

@ -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();

View File

@ -1,28 +1,156 @@
const express = require('express'); const express = require('express');
const cors = require('cors');
const path = require('path'); const path = require('path');
const app = express(); const connectDB = require('./config/database');
const families = require('./data/families.json'); const Family = require('./models/Family');
// Serve static files from the public directory const app = express();
app.use(express.static(path.join(__dirname, '../public')));
// 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 // API endpoint for family search
app.get('/api/search', (req, res) => { app.get('/api/search', async (req, res) => {
const query = req.query.family?.toLowerCase(); try {
if (!query) { const query = req.query.family?.toLowerCase();
return res.json([]);
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 // Health check endpoint
app.get('/api/health', (req, res) => { 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; const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => { app.listen(port, '0.0.0.0', () => {
console.log(`🗺️ Ora Map Server running at http://localhost:${port}`); console.log(`🗺️ Ora Map Backend API running at http://localhost:${port}`);
console.log(`📍 API endpoint: http://localhost:${port}/api/search`); console.log(`📍 Search endpoint: http://localhost:${port}/api/search`);
console.log(`💚 Health endpoint: http://localhost:${port}/api/health`);
}); });

View File

@ -1,6 +1,26 @@
version: '3.8' version: '3.8'
services: 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 API Service
backend: backend:
build: build:
@ -11,6 +31,10 @@ services:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=3000
- MONGODB_URI=mongodb://mongo:27017/oramap
depends_on:
mongo:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] 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: networks:
oramap-network: oramap-network:
driver: bridge driver: bridge
volumes:
mongo-data:
driver: local

View File

@ -12,9 +12,41 @@
<div class="search-bar"> <div class="search-bar">
<input type="text" id="searchInput" placeholder="Enter family name..." /> <input type="text" id="searchInput" placeholder="Enter family name..." />
<button onclick="searchFamily()">Search</button> <button onclick="searchFamily()">Search</button>
<button onclick="toggleAddFamilyForm()" class="add-btn"> Add Family</button>
</div> </div>
</header> </header>
<!-- Add Family Form Modal -->
<div id="addFamilyModal" class="modal">
<div class="modal-content">
<span class="close" onclick="toggleAddFamilyForm()">&times;</span>
<h2>Add New Family Location</h2>
<form id="addFamilyForm" onsubmit="addFamily(event)">
<div class="form-group">
<label for="familyName">Family Name:</label>
<input type="text" id="familyName" required placeholder="e.g., Cohen (כהן)" />
</div>
<div class="form-group">
<label for="cityName">City:</label>
<input type="text" id="cityName" required placeholder="e.g., Jerusalem (ירושלים)" />
</div>
<div class="form-group">
<label for="latitude">Latitude:</label>
<input type="number" id="latitude" required step="0.0001" min="-90" max="90" placeholder="e.g., 31.7683" />
</div>
<div class="form-group">
<label for="longitude">Longitude:</label>
<input type="number" id="longitude" required step="0.0001" min="-180" max="180" placeholder="e.g., 35.2137" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Family</button>
<button type="button" class="btn btn-secondary" onclick="toggleAddFamilyForm()">Cancel</button>
</div>
</form>
<div id="formMessage" class="form-message"></div>
</div>
</div>
<main> <main>
<div id="map"></div> <div id="map"></div>
</main> </main>

View File

@ -121,4 +121,89 @@ L.Control.Logo = L.Control.extend({
} }
}); });
L.control.logo({ position: 'bottomleft' }).addTo(map); 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(`<strong>${familyName}</strong><br>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();
}
});

View File

@ -95,4 +95,144 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; 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;
} }

View File

@ -1,11 +1,11 @@
apiVersion: v2 apiVersion: v2
name: oramap name: oramap
description: Ora Map - Family Location Mapping Application (Microservices Architecture) description: Ora Map - Family Location Mapping Application with MongoDB (Microservices Architecture)
type: application type: application
# Chart version # Chart version
version: 0.2.0 version: 0.3.0
# Application version # Application version
appVersion: "1.0.0" appVersion: "1.0.0"

View File

@ -4,10 +4,11 @@ Helm chart for deploying Ora Map application in Kubernetes with microservices ar
## Architecture ## Architecture
This chart deploys two main components: This chart deploys three main components:
- **Backend**: Node.js Express API server (Port 3000) - **Backend**: Node.js Express API server with MongoDB integration (Port 3000)
- **Frontend**: Nginx serving static files (Port 80) - **Frontend**: Nginx serving static files with API proxy (Port 80)
- **MongoDB**: Database for storing family location data (Port 27017)
## Installation ## Installation
@ -62,6 +63,13 @@ The following table lists the configurable parameters and their default values:
| `ingress.enabled` | Enable ingress | `true` | | `ingress.enabled` | Enable ingress | `true` |
| `ingress.className` | Ingress class name | `traefik` | | `ingress.className` | Ingress class name | `traefik` |
| `ingress.hosts[0].host` | Ingress hostname | `oramap.dvirlabs.com` | | `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 ### Example Custom Values
@ -70,6 +78,12 @@ The following table lists the configurable parameters and their default values:
backend: backend:
image: image:
tag: "v1.0.0" tag: "v1.0.0"
mongodb:
persistence:
enabled: true
size: 10Gi
storageClass: "fast-ssd"
replicaCount: 3 replicaCount: 3
frontend: frontend:
@ -107,13 +121,55 @@ ingress:
### Services ### Services
- **Backend Service**: Internal ClusterIP service on port 3000 - **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 - Persistent database storage for family data
- TLS/SSL termination support - Health checks using mongosh
- Configurable hostname and paths - 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 ## CI/CD Integration

View File

@ -36,6 +36,10 @@ spec:
value: "production" value: "production"
- name: PORT - name: PORT
value: "{{ .Values.backend.containerPort }}" value: "{{ .Values.backend.containerPort }}"
{{- if .Values.mongodb.enabled }}
- name: MONGODB_URI
value: "mongodb://{{ include \"oramap.fullname\" . }}-mongodb:27017/{{ .Values.mongodb.database }}"
{{- end }}
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /api/health path: /api/health

View File

@ -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 }}

View File

@ -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 }}

View File

@ -41,6 +41,26 @@ service:
port: 80 port: 80
targetPort: 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: ingress:
enabled: true enabled: true
className: "traefik" className: "traefik"