Changes
This commit is contained in:
parent
cb9a700e37
commit
4acc4fb84c
25
README.md
25
README.md
|
|
@ -58,9 +58,30 @@ npm run dev
|
|||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -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"
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,30 @@ import DashboardLoading from './DashboardLoading';
|
|||
import exportCsv from './exportCsv';
|
||||
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 }) {
|
||||
const { data, loading, error, load } = useDashboardData(
|
||||
fetchDashboardDoctors,
|
||||
|
|
@ -136,21 +160,39 @@ export default function DoctorsSection({ onUnauthorized }) {
|
|||
<th>Employee code</th>
|
||||
<th>Position name</th>
|
||||
<th>Total surveys</th>
|
||||
<th>WhatsApp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filtered.map((row, index) => (
|
||||
<tr key={row.contact_id}>
|
||||
<td>{index + 1}</td>
|
||||
<td className="text-left">{row.name || '-'}</td>
|
||||
<td>{row.msl_code || '-'}</td>
|
||||
<td>{row.phone || '-'}</td>
|
||||
<td className="text-left">{row.mr_name || '-'}</td>
|
||||
<td>{row.employee_code || '-'}</td>
|
||||
<td>{row.position_name || '-'}</td>
|
||||
<td className="dash-num">{row.total_surveys}</td>
|
||||
</tr>
|
||||
))}
|
||||
{filtered.map((row, index) => {
|
||||
const whatsapp = doctorWhatsappLink(row);
|
||||
return (
|
||||
<tr key={row.contact_id}>
|
||||
<td>{index + 1}</td>
|
||||
<td className="text-left">{row.name || '-'}</td>
|
||||
<td>{row.msl_code || '-'}</td>
|
||||
<td>{row.phone || '-'}</td>
|
||||
<td className="text-left">{row.mr_name || '-'}</td>
|
||||
<td>{row.employee_code || '-'}</td>
|
||||
<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>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -256,6 +256,7 @@
|
|||
.dash-btn.info { background: var(--dash-info); }
|
||||
.dash-btn.dark { background: var(--dash-dark); }
|
||||
.dash-btn.success { background: var(--dash-success); }
|
||||
.dash-btn.whatsapp { background: #25d366; }
|
||||
.dash-btn.warning { background: var(--dash-warning); color: var(--dash-heading); }
|
||||
|
||||
.dash-table {
|
||||
|
|
|
|||
Loading…
Reference in New Issue