Add MongoDB integration with CRUD UI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
parent
f3da9b66c3
commit
02074aa4a6
244
README.md
244
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:**
|
||||
|
||||
20
backend/config/database.js
Normal file
20
backend/config/database.js
Normal 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
33
backend/models/Family.js
Normal 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);
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
59
backend/scripts/seed.js
Normal file
59
backend/scripts/seed.js
Normal 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();
|
||||
@ -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`);
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -12,9 +12,41 @@
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Enter family name..." />
|
||||
<button onclick="searchFamily()">Search</button>
|
||||
<button onclick="toggleAddFamilyForm()" class="add-btn">➕ Add Family</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Add Family Form Modal -->
|
||||
<div id="addFamilyModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="toggleAddFamilyForm()">×</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>
|
||||
<div id="map"></div>
|
||||
</main>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
20
oramap/templates/mongodb-service.yaml
Normal file
20
oramap/templates/mongodb-service.yaml
Normal 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 }}
|
||||
75
oramap/templates/mongodb-statefulset.yaml
Normal file
75
oramap/templates/mongodb-statefulset.yaml
Normal 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 }}
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user