[OWL] Driver location tracking + bulk WhatsApp parser + deployment docs
Some checks are pending
FreightDesk CI/CD / Lint & Test (push) Waiting to run
FreightDesk CI/CD / Build Docker Image (push) Blocked by required conditions
FreightDesk CI/CD / Deploy to Coolify (push) Blocked by required conditions

Location Tracking:
- POST /api/location/update — driver GPS update
- GET /api/location/:load_id — get driver location for load
- Migration 007: vehicle_locations table with spatial indexes

Bulk WhatsApp Parser:
- UI for pasting multiple messages at once
- Batch parse via /api/parse-whatsapp
- Review parsed results with confidence scores
- Select and save all valid loads to database
- One-click import from WhatsApp to loads

Deployment:
- DEPLOYMENT.md: full deployment guide
- Environment configuration
- Docker + Docker Compose setup
- Coolify deployment steps
- Post-deployment checklist
- Troubleshooting guide
- Architecture diagram
This commit is contained in:
FreightDesk 2026-06-08 01:59:05 +00:00
parent 6be15fb059
commit 59d93d5281
5 changed files with 533 additions and 0 deletions

235
DEPLOYMENT.md Normal file
View file

@ -0,0 +1,235 @@
# FreightDesk — Deployment Guide
## Prerequisites
- Ubuntu 22.04+ VPS (minimum 2GB RAM, 2 vCPU)
- Domain pointed to VPS IP
- Coolify installed (or Docker + Docker Compose)
- Supabase project (self-hosted or cloud)
## Quick Start
### 1. Clone Repository
```bash
git clone http://forgejo-vil3xyowqk0qsh4hiqy77e3h.187.127.178.110.sslip.io/iamcoolvivek007/freightdesk.git
cd freightdesk/webapp
```
### 2. Environment Configuration
Create `.env` file:
```env
# Server
NODE_ENV=production
PORT=3000
# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your-service-role-key
SUPABASE_ANON_KEY=your-anon-key
# Session
SESSION_SECRET=generate-a-random-64-char-string-here
SESSION_MAX_AGE=86400000
# WhatsApp (optional — for receiving messages)
WHATSAPP_WEBHOOK_TOKEN=your-webhook-verify-token
# Payment Gateway (production)
RAZORPAY_KEY_ID=rzk_live_xxxxx
RAZORPAY_KEY_SECRET=xxxxx
# Email (optional — for notifications)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
```
### 3. Install Dependencies
```bash
npm ci --production
```
### 4. Run Database Migrations
Run migrations 001 through 007 in order:
```bash
# Using Supabase CLI
supabase db push
# Or manually via SQL editor:
# Copy contents of supabase/migrations/001_initial_schema.sql and run
# Copy contents of supabase/migrations/002_whatsapp_parser.sql and run
# ... through 007_location_tracking.sql
```
### 5. Create Admin User
Visit `/setup` in your browser and create the admin account.
### 6. Start the Server
```bash
# Development
npm run dev
# Production
NODE_ENV=production node src/server.js
```
### 7. Coolify Deployment (Recommended)
1. In Coolify, create new application
2. Connect to your Forgejo repository
3. Set buildpack: `Dockerfile`
4. Set Dockerfile path: `/webapp/Dockerfile`
5. Add environment variables from `.env`
6. Set domain and enable SSL
## Docker
```bash
cd webapp
docker build -t freightdesk .
docker run -d \
--name freightdesk \
-p 3000:3000 \
--env-file .env \
--restart unless-stopped \
freightdesk
```
## Docker Compose (Full Stack)
```yaml
version: '3.8'
services:
app:
build: ./webapp
ports:
- "3000:3000"
env_file: .env
restart: unless-stopped
depends_on:
- supabase
# If self-hosting Supabase
supabase:
image: supabase/supabase-local:latest
ports:
- "5432:5432" # PostgreSQL
- "8000:8000" # REST API
- "4000:4000" # Studio
volumes:
- supabase-data:/var/lib/supabase
restart: unless-stopped
volumes:
supabase-data:
```
## Post-Deployment Checklist
- [ ] Run all 7 migrations (001-007)
- [ ] Create admin account via /setup
- [ ] Configure SSL certificate
- [ ] Set up automated backups (Supabase: daily DB dump)
- [ ] Configure Coolify webhooks for auto-deploy on git push
- [ ] Set up monitoring (Prometheus /metrics endpoint at :3000/metrics)
- [ ] Configure Pino log aggregation
- [ ] Test WhatsApp parser with sample messages
- [ ] Test registration flow (shipper + driver)
- [ ] Test marketplace: post load → bid → accept
- [ ] Test payment escrow: deposit → hold → release → payout
## Migrations Summary
| # | File | What it adds |
|---|------|-------------|
| 001 | `001_initial_schema.sql` | Core tables: loads, shippers, vehicles, payments, users |
| 002 | `002_whatsapp_parser.sql` | Parser config, city list, known shippers |
| 003 | `003_soft_delete.sql` | Soft-delete columns on all tables |
| 004 | `004_audit_logging.sql` | Audit log table + triggers |
| 005 | `005_saas_marketplace.sql` | Bids, negotiations, ratings, notifications, marketplace fields |
| 006 | `006_payment_escrow.sql` | Escrow accounts, transactions, payouts, disputes |
| 007 | `007_location_tracking.sql` | Vehicle GPS location history |
## Troubleshooting
### App won't start
- Check `.env` has all required variables
- Verify Supabase connection: `curl $SUPABASE_URL/rest/v1/`
- Check logs: `docker logs freightdesk`
### Database errors
- Run migrations in order (001 → 007)
- Check Supabase service key has proper permissions
- Verify `pgcrypto` extension is enabled (for UUID generation)
### WhatsApp parser not working
- Ensure migration 002 ran (populates CITIES and parser config)
- Test via `/api/parser/test` endpoint
### Payment flow fails
- Ensure migration 006 ran
- Check escrow_accounts table exists
- Verify platform_config has default values
## Architecture
```
┌─────────────────────┐
│ Nginx / Coolify │
│ (SSL + Proxy) │
└──────────┬──────────┘
┌──────────▼──────────┐
│ Node.js + Express │
│ FreightDesk App │
│ Port 3000 │
└──┬──────┬──────┬────┘
│ │ │
┌──────────────┘ │ └──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ EJS Views │ │ REST API │ │ Supabase │
│ (templates) │ │ /api/* │ │ PostgreSQL │
│ + Recharts │ │ JSON │ │ + Realtime │
│ CDN widgets │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
Routes:
/ → Public landing page
/login → Admin login
/setup → Initial admin setup
/dashboard → Admin dashboard (EJS + Recharts)
/loads → Load management (admin)
/shippers → Shipper management
/vehicles → Vehicle management
/payments → Payment tracking
/reports → Reports
/audit-logs → Audit log viewer
/invoices → Invoice PDF generation
/admin/moderation → User verification, payouts, disputes
/register/shipper → Shipper self-registration
/register/driver → Driver self-registration
/portal/* → Shipper/driver portal (dashboard, loads, trips)
/marketplace → Browse/post loads, bidding
/escrow → Deposits, payouts, disputes
/api/* → REST API (JSON)
/metrics → Prometheus metrics
/health → Health check
```
## Support
- Forgejo: `http://forgejo-vil3xyowqk0qsh4hiqy77e3h.187.127.178.110.sslip.io/iamcoolvivek007/freightdesk`
- Issues: Create on Forgejo

View file

@ -0,0 +1,24 @@
-- ============================================================
-- FreightDesk — Migration 007: Driver Location Tracking
-- GPS location history for real-time tracking
-- ============================================================
CREATE TABLE IF NOT EXISTS vehicle_locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
vehicle_id UUID NOT NULL REFERENCES vehicles(id) ON DELETE CASCADE,
lat DECIMAL(10,8) NOT NULL,
lng DECIMAL(11,8) NOT NULL,
accuracy DECIMAL(8,2),
heading DECIMAL(6,2),
speed DECIMAL(6,2),
recorded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle ON vehicle_locations(vehicle_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_time ON vehicle_locations(recorded_at);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_time ON vehicle_locations(vehicle_id, recorded_at DESC);
-- Enable PostGIS-like functionality with btree_gist for spatial queries
-- (In production, use PostGIS extension)
CREATE INDEX IF NOT EXISTS idx_vehicles_location ON vehicles(current_lat, current_lng)
WHERE current_lat IS NOT NULL AND current_lng IS NOT NULL;

View file

@ -0,0 +1,68 @@
// POST /api/location/update — driver updates their GPS location
// GET /api/location/:load_id — get driver location for a load (shipper views this)
const express = require('express');
const router = express.Router();
const supabase = require('../services/supabase');
const { asyncHandler } = require('../middleware/security');
function requirePortalAuth(req, res, next) {
if (!req.session.portalUser) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
}
// POST /api/location/update
router.post('/update', requirePortalAuth, asyncHandler(async (req, res) => {
const { lat, lng, accuracy, heading, speed } = req.body;
const driverId = req.session.portalUser?.driver_id;
if (!lat || !lng) {
return res.status(400).json({ error: 'lat and lng are required' });
}
if (!driverId) {
return res.status(400).json({ error: 'Driver profile not found' });
}
await supabase.from('vehicles').update({
current_lat: parseFloat(lat),
current_lng: parseFloat(lng),
updated_at: new Date().toISOString(),
}).eq('id', driverId);
// Also store in location history
await supabase.from('vehicle_locations').insert({
vehicle_id: driverId,
lat: parseFloat(lat),
lng: parseFloat(lng),
accuracy: accuracy || null,
heading: heading || null,
speed: speed || null,
});
res.json({ success: true });
}));
// GET /api/location/:load_id — get assigned driver's location
router.get('/:load_id', requirePortalAuth, asyncHandler(async (req, res) => {
const { data: load } = await supabase
.from('loads')
.select('accepted_bid_id, vehicles(current_lat, current_lng, driver_name, updated_at)')
.eq('id', req.params.load_id)
.single();
if (!load?.vehicles) {
return res.json({ error: 'No driver assigned or location not available' });
}
res.json({
driver_name: load.vehicles.driver_name,
lat: load.vehicles.current_lat,
lng: load.vehicles.current_lng,
last_updated: load.vehicles.updated_at,
});
}));
module.exports = router;

View file

@ -210,6 +210,7 @@ app.use('/portal', require('./routes/portal'));
app.use('/invoices', require('./routes/invoices')); app.use('/invoices', require('./routes/invoices'));
app.use('/portal-users', require('./routes/portal-users')); app.use('/portal-users', require('./routes/portal-users'));
app.use('/api', require('./routes/api')); app.use('/api', require('./routes/api'));
app.use('/api/location', require('./routes/location'));
app.use('/marketplace', require('./routes/marketplace')); app.use('/marketplace', require('./routes/marketplace'));
app.use('/escrow', require('./routes/payments')); app.use('/escrow', require('./routes/payments'));
app.use('/admin/moderation', require('./routes/admin-moderation')); app.use('/admin/moderation', require('./routes/admin-moderation'));

View file

@ -0,0 +1,205 @@
<%- include('../partials/portal-header', { activeMenu: 'parser' }) %>
<div class="page-header">
<div>
<h1 class="page-title">&#128241; Bulk WhatsApp Parser</h1>
<p class="page-subtitle">Paste multiple WhatsApp messages at once to create loads in bulk</p>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-header"><h3 class="card-title">Paste Messages</h3></div>
<div class="card-body">
<p class="text-muted" style="font-size:13px;margin-bottom:12px;">
Paste multiple WhatsApp messages (one per line or separated by blank lines).
Each message will be parsed and you can review before saving.
</p>
<div class="form-group">
<textarea id="bulkInput" class="form-input" rows="12" placeholder="Paste WhatsApp messages here...
Example:
Kahn Transport KL01AB1234 Bangalore to Chennai freight 50000 loaded
Agarwal MH12CD5678 Delhi to Mumbai 75000 advance 30000 in transit
TN09EF9012 Coimbatore to Hyderabad 45000 delivered"></textarea>
</div>
<div style="display:flex;gap:8px;">
<button type="button" class="btn btn-primary" onclick="parseBulk()">&#128241; Parse All Messages</button>
<button type="button" class="btn btn-outline" onclick="clearAll()">&#10060; Clear</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Parsed Results <span id="parseCount" class="badge badge-primary" style="display:none;"></span></h3>
</div>
<div class="card-body" style="padding:0;">
<div id="bulkResults">
<div class="empty-state" style="padding:48px;">
<div class="empty-icon">&#128241;</div>
<h3>No messages parsed yet</h3>
<p>Paste WhatsApp messages and click Parse</p>
</div>
</div>
</div>
</div>
</div>
<!-- Review & Save Section -->
<div id="reviewSection" class="card mt-3" style="display:none;">
<div class="card-header">
<h3 class="card-title">Review &amp; Save Loads</h3>
</div>
<div class="card-body">
<div id="reviewList"></div>
<div style="display:flex;gap:8px;margin-top:16px;">
<button type="button" class="btn btn-success" onclick="saveAll()">&#128190; Save All Valid Loads</button>
<button type="button" class="btn btn-outline" onclick="selectAllToggle()">&#9745; Select All</button>
</div>
</div>
</div>
<script>
let parsedMessages = [];
async function parseBulk() {
const input = document.getElementById('bulkInput').value.trim();
if (!input) return alert('Paste some messages first');
// Split by double newlines or single newlines (each line = one message)
const messages = input.split(/\n\s*\n|\n/).filter(m => m.trim().length > 0);
if (messages.length === 0) return alert('No messages found');
parsedMessages = [];
let html = '<div style="padding:16px;">';
for (let i = 0; i < messages.length; i++) {
const msg = messages[i].trim();
try {
const res = await fetch('/api/parse-whatsapp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: msg })
});
const parsed = await res.json();
parsedMessages.push({ original: msg, parsed, index: i });
const confidenceColor = parsed.confidence === 'high' ? '#2e7d32' : parsed.confidence === 'medium' ? '#f59e0b' : '#666';
const fields = parsed.parsed_fields?.join(', ') || 'none';
html += `<div style="padding:12px;border-bottom:1px solid #f0ede5;">
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
<strong>Message ${i + 1}</strong>
<span class="badge" style="background:${confidenceColor}20;color:${confidenceColor};">${parsed.confidence}</span>
</div>
<div style="font-size:12px;color:#666;margin-bottom:8px;white-space:pre-wrap;">${msg.substring(0, 100)}${msg.length > 100 ? '...' : ''}</div>
<div style="font-size:13px;">
<strong>${parsed.shipper || 'Unknown'}</strong>
${parsed.from_city ? parsed.from_city + ' → ' + (parsed.to_city || '?') : ''}
${parsed.freight_charged ? ' · ₹' + parsed.freight_charged.toLocaleString('en-IN') : ''}
${parsed.vehicle ? ' · ' + parsed.vehicle : ''}
<br><small style="color:#999;">Fields: ${fields}</small>
</div>
</div>`;
} catch (e) {
parsedMessages.push({ original: msg, parsed: null, index: i, error: e.message });
html += `<div style="padding:12px;border-bottom:1px solid #f0ede5;">
<strong>Message ${i + 1}</strong>
<span class="badge badge-danger">Error</span>
<div style="font-size:12px;color:#666;">${msg.substring(0, 80)}</div>
</div>`;
}
}
html += '</div>';
document.getElementById('bulkResults').innerHTML = html;
document.getElementById('parseCount').textContent = `${messages.length} parsed`;
document.getElementById('parseCount').style.display = 'inline';
// Show review section for valid ones
const validMessages = parsedMessages.filter(m => m.parsed && m.parsed.confidence !== 'low');
if (validMessages.length > 0) {
showReview(validMessages);
}
}
function showReview(messages) {
const section = document.getElementById('reviewSection');
const list = document.getElementById('reviewList');
let html = '<table class="table"><thead><tr><th>Select</th><th>Shipper</th><th>Route</th><th>Freight</th><th>Vehicle</th><th>Status</th></tr></thead><tbody>';
messages.forEach((m, i) => {
const p = m.parsed;
html += `<tr>
<td><input type="checkbox" class="load-checkbox" data-index="${m.index}" checked></td>
<td>${p.shipper || '<span style="color:#999;">Unknown</span>'}</td>
<td>${p.from_city || '?'} → ${p.to_city || '?'}</td>
<td>₹${(p.freight_charged || 0).toLocaleString('en-IN')}</td>
<td>${p.vehicle || '-'}</td>
<td><span class="badge badge-gray">${p.status || 'pending'}</span></td>
</tr>`;
});
html += '</tbody></table>';
list.innerHTML = html;
section.style.display = 'block';
}
async function saveAll() {
const checkboxes = document.querySelectorAll('.load-checkbox:checked');
if (checkboxes.length === 0) return alert('Select at least one load');
let saved = 0;
let failed = 0;
for (const cb of checkboxes) {
const index = parseInt(cb.dataset.index);
const { parsed } = parsedMessages[index];
if (!parsed) continue;
try {
const res = await fetch('/api/loads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
shipper: parsed.shipper,
from_city: parsed.from_city,
to_city: parsed.to_city,
vehicle: parsed.vehicle,
freight_charged: parsed.freight_charged,
advance_received: parsed.advance_received,
status: parsed.status || 'pending lead',
notes: parsed.notes || '',
source: 'whatsapp_bulk',
})
});
if (res.ok) saved++;
else failed++;
} catch (e) {
failed++;
}
}
alert(`Saved ${saved} loads. ${failed} failed.`);
if (saved > 0) window.location.href = '/loads';
}
function selectAllToggle() {
const boxes = document.querySelectorAll('.load-checkbox');
const allChecked = Array.from(boxes).every(b => b.checked);
boxes.forEach(b => b.checked = !allChecked);
}
function clearAll() {
document.getElementById('bulkInput').value = '';
document.getElementById('bulkResults').innerHTML = '<div class="empty-state" style="padding:48px;"><div class="empty-icon">&#128241;</div><h3>No messages parsed yet</h3><p>Paste WhatsApp messages and click Parse</p></div>';
document.getElementById('parseCount').style.display = 'none';
document.getElementById('reviewSection').style.display = 'none';
parsedMessages = [];
}
</script>
<%- include('../partials/portal-footer') %>