Cache-busting version stamps + client-side encounter version tracking
1. Build-ID cache busting (server.js):
- Compute a BUILD_ID at boot: git HEAD short hash if available,
else /app/BUILD_ID file, else random-on-boot.
- On first request for /, rewrite every local /js/*.js and
/css/*.css reference in index.html to include ?v=BUILD_ID.
Cached once at startup so subsequent renders are free.
- X-Build-Id response header + GET /api/build expose it for
debugging.
- Eliminates the "works after hard-refresh" class of bugs: every
deploy gets a new build ID, so browsers fetch fresh JS/CSS on
the very next page load.
2. Optimistic encounter locking wired into the client
(public/js/encounters.js):
- On resumeEncounter(): stash enc.version into
window._encounterVersions[id]
- On saveEncounter(): send expected_version in the POST body
when we have one.
- Server returns 409 if another tab/device wrote first → user
sees "Someone else edited this encounter. Reload to see the
latest version." instead of silently clobbering the prior save.
- On success, remember the new server-assigned version for the
next save.
This commit is contained in:
parent
2f3e608c88
commit
e32e9977f5
2 changed files with 71 additions and 2 deletions
|
|
@ -117,16 +117,31 @@
|
|||
// Prevent duplicate saves from double-click
|
||||
if (_savingInProgress[type]) { showToast('Save in progress…', 'info'); return; }
|
||||
_savingInProgress[type] = true;
|
||||
// Optimistic locking: send the version we last saw. Server returns 409
|
||||
// if the encounter was updated elsewhere (another tab, another device).
|
||||
var body = Object.assign({}, opts);
|
||||
if (opts.id && window._encounterVersions && window._encounterVersions[opts.id] != null) {
|
||||
body.expected_version = window._encounterVersions[opts.id];
|
||||
}
|
||||
fetch('/api/encounters/saved', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(opts)
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(r) { return r.json().then(function(d) { d._status = r.status; return d; }); })
|
||||
.then(function(data) {
|
||||
_savingInProgress[type] = false;
|
||||
if (data._status === 409) {
|
||||
showToast('Someone else edited this encounter. Reload to see the latest version.', 'error');
|
||||
return;
|
||||
}
|
||||
if (data.success) {
|
||||
showToast('Encounter saved (' + (opts.label || '') + ')', 'success');
|
||||
// Track the new version the server assigned
|
||||
if (data.id != null && data.version != null) {
|
||||
window._encounterVersions = window._encounterVersions || {};
|
||||
window._encounterVersions[data.id] = data.version;
|
||||
}
|
||||
// Persist ID so refreshing the page won't create a duplicate
|
||||
if (data.id) {
|
||||
if (opts.onSaved) opts.onSaved(data.id);
|
||||
|
|
@ -275,6 +290,11 @@
|
|||
.then(function(data) {
|
||||
if (!data.success) { showToast(data.error || 'Failed', 'error'); return; }
|
||||
var enc = data.encounter;
|
||||
// Remember the loaded version so future saves can detect drift
|
||||
if (enc && enc.id != null && enc.version != null) {
|
||||
window._encounterVersions = window._encounterVersions || {};
|
||||
window._encounterVersions[enc.id] = enc.version;
|
||||
}
|
||||
// Close all popovers
|
||||
['enc', 'dict', 'wv', 'sick', 'hosp', 'chart', 'soap'].forEach(function(p) {
|
||||
var pop = document.getElementById(p + '-load-popover');
|
||||
|
|
|
|||
49
server.js
49
server.js
|
|
@ -130,6 +130,55 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
|
|||
res.sendFile(path.join(__dirname, 'public', '.well-known', 'assetlinks.json'));
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// CACHE-BUSTING VERSION STAMP
|
||||
// ============================================================
|
||||
// Compute a per-boot BUILD_ID (short hex). Inject it as ?v=BUILD_ID
|
||||
// on every local /js/*.js and /css/*.css reference in index.html so
|
||||
// browsers always fetch fresh JS/CSS after a deploy instead of
|
||||
// serving from the 1-hour cache.
|
||||
var fs = require('fs');
|
||||
var crypto = require('crypto');
|
||||
var BUILD_ID = crypto.randomBytes(4).toString('hex');
|
||||
try {
|
||||
var gitHead = fs.readFileSync(path.join(__dirname, '.git/HEAD'), 'utf8').trim();
|
||||
if (gitHead.indexOf('ref:') === 0) {
|
||||
var refPath = gitHead.split(' ')[1];
|
||||
BUILD_ID = fs.readFileSync(path.join(__dirname, '.git', refPath), 'utf8').trim().slice(0, 7);
|
||||
} else {
|
||||
BUILD_ID = gitHead.slice(0, 7);
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-git environment (Docker image) — use /app/BUILD_ID file if present,
|
||||
// otherwise stick with the random-on-boot value generated above.
|
||||
try {
|
||||
BUILD_ID = fs.readFileSync(path.join(__dirname, 'BUILD_ID'), 'utf8').trim() || BUILD_ID;
|
||||
} catch (_) {}
|
||||
}
|
||||
console.log('🔖 Build ID:', BUILD_ID);
|
||||
|
||||
// Template index.html once at boot with the version stamp. Serves fast
|
||||
// because the rewrite happens one time and the string is reused.
|
||||
var INDEX_HTML_TEMPLATED = null;
|
||||
try {
|
||||
var raw = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
|
||||
INDEX_HTML_TEMPLATED = raw.replace(
|
||||
/(<(?:script|link)[^>]+(?:src|href)=["'])(\/(?:js|css)\/[^"'?]+)(["'])/g,
|
||||
'$1$2?v=' + BUILD_ID + '$3'
|
||||
);
|
||||
} catch (e) { console.warn('[build-id] index.html template skipped:', e.message); }
|
||||
|
||||
app.get(['/', '/index.html'], function(req, res) {
|
||||
if (!INDEX_HTML_TEMPLATED) return res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('X-Build-Id', BUILD_ID);
|
||||
res.send(INDEX_HTML_TEMPLATED);
|
||||
});
|
||||
|
||||
// Public endpoint for cache-bust debugging + build-info
|
||||
app.get('/api/build', function(req, res) { res.json({ buildId: BUILD_ID }); });
|
||||
|
||||
app.use(loggingMiddleware);
|
||||
app.use(express.static(path.join(__dirname, 'public'), {
|
||||
setHeaders: (res, filePath) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue