This commit is contained in:
Ravi Gulhane 2026-10-29 09:31:10 +05:30
parent cb9a700e37
commit 4acc4fb84c
6 changed files with 274 additions and 14 deletions

View File

@ -58,9 +58,30 @@ npm run dev
Set `VITE_API_BASE=http://localhost:3050`. Set `VITE_API_BASE=http://localhost:3050`.
## Nginx (production sketch) ## Deploy (EC2)
Serve the React build on `tgics.cttr.in` and proxy `/api` (and `/health`) to the Hapi process, **or** set `VITE_API_BASE` to a separate API host. ```bash
cd /Applications/MAMP/htdocs/shabby/tgics
./deploy.sh
```
Defaults:
| Setting | Value |
|---------|--------|
| SSH | `ssh -i ~/Documents/Keys/VirtualEngage.pem ubuntu@15.207.15.52` |
| Path | `/var/www/tgics` |
| Site | `https://tgics.cttr.in` |
| PM2 | `tgics-api` on `127.0.0.1:3050` |
The script builds the frontend locally (same-origin `/api`), rsyncs to the server, installs nginx (`deploy/nginx-tgics.conf`), and reloads PM2. Remote `backend/.env` is never overwritten.
First-time after deploy:
1. DNS: `tgics.cttr.in` → `15.207.15.52`
2. Edit `/var/www/tgics/backend/.env` (`DB_*`, `RAPPORT_ACCESS_TOKEN`, `JWT_SECRET`)
3. `pm2 restart tgics-api --update-env`
4. `sudo certbot --nginx -d tgics.cttr.in`
## Rapport Laravel ## Rapport Laravel

View File

@ -0,0 +1,19 @@
const path = require('node:path');
module.exports = {
apps: [
{
name: 'tgics-api',
cwd: __dirname,
script: path.join(__dirname, 'src/server.js'),
interpreter: 'node',
instances: 1,
exec_mode: 'fork',
autorestart: true,
max_memory_restart: '512M',
env: {
NODE_ENV: 'production',
},
},
],
};

141
deploy.sh Executable file
View File

@ -0,0 +1,141 @@
#!/usr/bin/env bash
# Deploy TGICS (frontend + API) to the VirtualEngage EC2 host.
#
# Usage:
# ./deploy.sh
#
# Env overrides:
# DEPLOY_KEY default ~/Documents/Keys/VirtualEngage.pem
# DEPLOY_HOST default ubuntu@15.207.15.52
# DEPLOY_PATH default /var/www/tgics
# DEPLOY_SITE default https://tgics.cttr.in
# API_PORT default 3050
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
SSH_KEY="${DEPLOY_KEY:-$HOME/Documents/Keys/VirtualEngage.pem}"
SSH_HOST="${DEPLOY_HOST:-ubuntu@15.207.15.52}"
REMOTE_DIR="${DEPLOY_PATH:-/var/www/tgics}"
SITE="${DEPLOY_SITE:-https://tgics.cttr.in}"
API_PORT="${API_PORT:-3050}"
PM2_NAME="tgics-api"
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new)
ssh_cmd() {
ssh "${SSH_OPTS[@]}" "$SSH_HOST" "$@"
}
rsync_cmd() {
rsync -az --delete --progress \
-e "ssh ${SSH_OPTS[*]}" \
"$@"
}
if [[ ! -f "$SSH_KEY" ]]; then
echo "SSH key not found: $SSH_KEY" >&2
exit 1
fi
chmod 400 "$SSH_KEY" 2>/dev/null || true
echo "==> Building frontend (same-origin API via nginx)"
(
cd "$ROOT/frontend"
npm ci --cache "$ROOT/.npm-cache"
# Empty base → browser calls /api on tgics.cttr.in (proxied to Hapi)
VITE_API_BASE= npx vite build
)
echo "==> Ensuring remote directories"
ssh_cmd "sudo mkdir -p '$REMOTE_DIR/frontend/dist' '$REMOTE_DIR/backend' '$REMOTE_DIR/deploy' && sudo chown -R ubuntu:ubuntu '$REMOTE_DIR'"
echo "==> Syncing frontend build"
rsync_cmd \
"$ROOT/frontend/dist/" \
"$SSH_HOST:$REMOTE_DIR/frontend/dist/"
echo "==> Syncing backend (keeps remote .env)"
rsync_cmd \
--exclude node_modules \
--exclude .env \
"$ROOT/backend/" \
"$SSH_HOST:$REMOTE_DIR/backend/"
echo "==> Syncing deploy assets"
rsync_cmd \
"$ROOT/deploy/" \
"$SSH_HOST:$REMOTE_DIR/deploy/"
echo "==> Installing deps, configuring nginx, restarting PM2"
ssh_cmd \
"REMOTE_DIR=$(printf '%q' "$REMOTE_DIR") SITE=$(printf '%q' "$SITE") API_PORT=$(printf '%q' "$API_PORT") PM2_NAME=$(printf '%q' "$PM2_NAME") bash -s" <<'REMOTE'
set -euo pipefail
export PATH="/usr/bin:/usr/local/bin:$PATH"
cd "$REMOTE_DIR/backend"
if [[ ! -f .env ]]; then
if [[ ! -f .env.example ]]; then
echo "Missing $REMOTE_DIR/backend/.env on the server." >&2
exit 1
fi
cp .env.example .env
python3 - <<PY
from pathlib import Path
import re
path = Path(".env")
text = path.read_text()
site = ${SITE@Q}
port = ${API_PORT@Q}
def upsert(text, key, value):
line = f"{key}={value}"
pattern = re.compile("^" + re.escape(key) + r"=.*$", re.M)
if pattern.search(text):
return pattern.sub(line, text, count=1)
return text.rstrip() + "\n" + line + "\n"
text = upsert(text, "HOST", "127.0.0.1")
text = upsert(text, "PORT", str(port))
text = upsert(text, "FRONTEND_ORIGIN", site)
path.write_text(text)
print("Seeded backend/.env from example — edit DB_* / RAPPORT_ACCESS_TOKEN / JWT_SECRET")
PY
else
echo "Keeping existing backend/.env"
fi
if ! command -v pm2 >/dev/null 2>&1; then
sudo npm install -g pm2
fi
npm ci --omit=dev
pm2 startOrReload ecosystem.config.cjs --update-env
pm2 save
pm2 status "$PM2_NAME"
NGINX_AVAIL="/etc/nginx/sites-available/tgics"
NGINX_ENABLED="/etc/nginx/sites-enabled/tgics"
if [[ -f "$REMOTE_DIR/deploy/nginx-tgics.conf" ]]; then
sudo cp "$REMOTE_DIR/deploy/nginx-tgics.conf" "$NGINX_AVAIL"
sudo ln -sfn "$NGINX_AVAIL" "$NGINX_ENABLED"
sudo nginx -t
sudo systemctl reload nginx
echo "Nginx site installed for tgics.cttr.in"
fi
sleep 1
curl -fsS "http://127.0.0.1:${API_PORT}/health"
echo
REMOTE
echo "==> Deployed to $SSH_HOST:$REMOTE_DIR"
echo " Site: $SITE"
echo " API: pm2 $PM2_NAME → 127.0.0.1:$API_PORT"
echo " First time:"
echo " 1) Point DNS tgics.cttr.in → 15.207.15.52"
echo " 2) Edit $REMOTE_DIR/backend/.env (DB_*, RAPPORT_ACCESS_TOKEN, JWT_SECRET)"
echo " 3) ssh … 'cd $REMOTE_DIR/backend && pm2 restart $PM2_NAME --update-env'"
echo " 4) sudo certbot --nginx -d tgics.cttr.in"

36
deploy/nginx-tgics.conf Normal file
View File

@ -0,0 +1,36 @@
# TGICS — https://tgics.cttr.in
# After DNS points to this host, run:
# sudo certbot --nginx -d tgics.cttr.in
server {
listen 80;
server_name tgics.cttr.in;
root /var/www/tgics/frontend/dist;
index index.html;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
client_max_body_size 6m;
location /api/ {
proxy_pass http://127.0.0.1:3050;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
proxy_pass http://127.0.0.1:3050/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@ -5,6 +5,30 @@ import DashboardLoading from './DashboardLoading';
import exportCsv from './exportCsv'; import exportCsv from './exportCsv';
import useDashboardData, { matchesQuery } from './useDashboardData'; import useDashboardData, { matchesQuery } from './useDashboardData';
const SURVEY_ORIGIN = 'https://tgics.cttr.in';
function doctorWhatsappLink(row) {
const userId = String(row.user_id || '').trim();
const contactId = String(row.contact_id || '').trim();
if (!userId || !contactId) {
return { href: '', label: 'Link unavailable' };
}
let digits = String(row.phone || '').replace(/\D+/g, '');
if (digits.length === 10) {
digits = `91${digits}`;
}
if (!digits) {
return { href: '', label: 'No WhatsApp number' };
}
const name = String(row.name || '').trim();
const link = `${SURVEY_ORIGIN}/${userId}/${contactId}`;
const text = `Dear Doctor${name ? ` ${name}` : ''},\n\nYou have been approved for TGICS (The Great Indian Cough Study).\nPlease open the link to add your surveys:\n${link}`;
return {
href: `https://api.whatsapp.com/send?phone=${digits}&text=${encodeURIComponent(text)}`,
label: '',
};
}
export default function DoctorsSection({ onUnauthorized }) { export default function DoctorsSection({ onUnauthorized }) {
const { data, loading, error, load } = useDashboardData( const { data, loading, error, load } = useDashboardData(
fetchDashboardDoctors, fetchDashboardDoctors,
@ -136,21 +160,39 @@ export default function DoctorsSection({ onUnauthorized }) {
<th>Employee code</th> <th>Employee code</th>
<th>Position name</th> <th>Position name</th>
<th>Total surveys</th> <th>Total surveys</th>
<th>WhatsApp</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filtered.map((row, index) => ( {filtered.map((row, index) => {
<tr key={row.contact_id}> const whatsapp = doctorWhatsappLink(row);
<td>{index + 1}</td> return (
<td className="text-left">{row.name || '-'}</td> <tr key={row.contact_id}>
<td>{row.msl_code || '-'}</td> <td>{index + 1}</td>
<td>{row.phone || '-'}</td> <td className="text-left">{row.name || '-'}</td>
<td className="text-left">{row.mr_name || '-'}</td> <td>{row.msl_code || '-'}</td>
<td>{row.employee_code || '-'}</td> <td>{row.phone || '-'}</td>
<td>{row.position_name || '-'}</td> <td className="text-left">{row.mr_name || '-'}</td>
<td className="dash-num">{row.total_surveys}</td> <td>{row.employee_code || '-'}</td>
</tr> <td>{row.position_name || '-'}</td>
))} <td className="dash-num">{row.total_surveys}</td>
<td>
{whatsapp.href ? (
<a
className="dash-btn whatsapp dash-btn-link"
href={whatsapp.href}
target="_blank"
rel="noopener noreferrer"
>
Send WhatsApp
</a>
) : (
<span className="dash-muted">{whatsapp.label}</span>
)}
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -256,6 +256,7 @@
.dash-btn.info { background: var(--dash-info); } .dash-btn.info { background: var(--dash-info); }
.dash-btn.dark { background: var(--dash-dark); } .dash-btn.dark { background: var(--dash-dark); }
.dash-btn.success { background: var(--dash-success); } .dash-btn.success { background: var(--dash-success); }
.dash-btn.whatsapp { background: #25d366; }
.dash-btn.warning { background: var(--dash-warning); color: var(--dash-heading); } .dash-btn.warning { background: var(--dash-warning); color: var(--dash-heading); }
.dash-table { .dash-table {