diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..27fff20 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,91 @@ +# Curio HMS — Production deploy checklist + +Target: [https://curio.applaude.net/dashboard.html](https://curio.applaude.net/dashboard.html) + +## Pre-deploy (local) + +- [ ] Confirm dashboard files are saved as **UTF-8** (no `₹` / `ðŸ` in source). +- [ ] Smoke-test locally: + ```powershell + cd c:\Users\sunde\proxmox\curaflow-hms + node backend/server.js + ``` + Open `http://localhost:3000/dashboard.html` — sidebar must not overlap the queue table; emojis and ₹ must render correctly. +- [ ] Bump cache-bust query strings if you changed static assets (`dashboard.css?v=`, `dashboard.js?v=` in `dashboard.html`). + +## Deploy (CI → registry → Kubernetes) + +This repo builds on **push** via `.gitea/workflows/build.yaml` (Kaniko → `192.168.8.250:5000/curio:latest`). + +1. **Commit and push** all changes (including `backend/server.js` UTF-8 headers and dashboard UI fixes). +2. **Wait for the Gitea Actions build** to finish successfully. +3. **Roll out Kubernetes** (pick one): + ```bash + # Restart pods to pull :latest + kubectl rollout restart deployment/curio-app -n curio + + # Or patch redeploy annotation (see k8s/app.yaml) + kubectl apply -f k8s/app.yaml -n curio + kubectl rollout status deployment/curio-app -n curio + ``` +4. **Confirm pods are new**: + ```bash + kubectl get pods -n curio -l app=curio-app + kubectl logs -n curio -l app=curio-app --tail=50 + ``` + +## Post-deploy verification + +Open production in a **private/incognito** window (avoids stale cache). + +| Check | Expected | +|-------|----------| +| Nav labels | 👥 Queue, 💊 Pharmacy (not `ðŸ` / `₹`) | +| Sidebar layout | Sidebar on the left; main content not covered | +| Overview stats | `↑ 4 since last hour` (not `â†'`) | +| Doctor workspace | Log in as Doctor → **Queue** → **Start Recording** visible | +| Response headers | DevTools → Network → `dashboard.html` → `Content-Type: text/html; charset=utf-8` | +| Recording | Chrome/Edge, allow microphone; button toggles to **Pause Scribe** | + +### Quick header check (PowerShell) + +```powershell +(Invoke-WebRequest -Uri "https://curio.applaude.net/dashboard.html" -Method Head).Headers["Content-Type"] +``` + +Should include `charset=utf-8`. + +### Doctor role (for Zen / recording UI) + +In browser console on production: + +```javascript +localStorage.setItem('userRole', 'DOCTOR'); +location.reload(); +``` + +Then open **Queue** — clinical workspace with SOAP notes and **Start Recording**. + +## If encoding is still broken after deploy + +1. **Files on disk** — Re-save `dashboard.html` (and other HTML) as UTF-8 in the editor; redeploy. +2. **Reverse proxy** — Ensure nginx/ingress does not force `charset=iso-8859-1` on static files. +3. **CDN/cache** — Purge edge cache for `*.html`, `*.css`, `*.js`. +4. **Hard refresh** — `Ctrl+Shift+R` on the dashboard. + +## Rollback + +```bash +# If you tag images per release, redeploy the previous tag instead of :latest +kubectl set image deployment/curio-app curio=192.168.8.250:5000/curio: -n curio +kubectl rollout status deployment/curio-app -n curio +``` + +## Files that must ship for the dashboard UX fix + +- `dashboard.html` +- `dashboard.css` +- `dashboard.js` +- `globals.css` +- `settings.css` +- `backend/server.js` (UTF-8 `Content-Type` headers) diff --git a/backend/server.js b/backend/server.js index 4d0a0c5..8361b64 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,38 +8,48 @@ const app = express(); const port = 3000; app.use(express.json()); + +const staticRoot = path.join(__dirname, '../'); + +/** UTF-8 Content-Type for text assets (fixes emoji/₹ mojibake when proxy omits charset). */ +function setUtf8ContentType(res, filePath) { + if (filePath.endsWith('.html')) { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + } else if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + } else if (filePath.endsWith('.json')) { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + } +} + +function setNoCacheHeaders(res) { + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); +} + +function sendHtmlPage(res, filename) { + setNoCacheHeaders(res); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.sendFile(path.join(staticRoot, filename)); +} + // Serve static files -app.use(express.static(path.join(__dirname, '../'), { - setHeaders: (res, path) => { - if (path.endsWith('.html')) { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); +app.use(express.static(staticRoot, { + setHeaders: (res, filePath) => { + setUtf8ContentType(res, filePath); + if (filePath.endsWith('.html')) { + setNoCacheHeaders(res); } } })); // Primary Routes -app.get('/', (req, res) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - res.sendFile(path.join(__dirname, '../login.html')); -}); - -app.get('/admin', (req, res) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - res.sendFile(path.join(__dirname, '../admin.html')); -}); - -app.get('/dashboard', (req, res) => { - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - res.sendFile(path.join(__dirname, '../dashboard.html')); -}); +app.get('/', (req, res) => sendHtmlPage(res, 'login.html')); +app.get('/admin', (req, res) => sendHtmlPage(res, 'admin.html')); +app.get('/dashboard', (req, res) => sendHtmlPage(res, 'dashboard.html')); // Multi-tenant Config API app.get('/api/config/:tenantId', (req, res) => {