Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 118 additions & 13 deletions common/controllers/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,21 @@ module.exports = function (Sync) {
}
}

// outbreaks chosen by the user; when not sent, the ones configured for the upstream server are used
let requestedOutbreakIDs;
if (data.outbreakIDs !== undefined && data.outbreakIDs !== null) {
if (
!Array.isArray(data.outbreakIDs) ||
data.outbreakIDs.some((outbreakId) => typeof outbreakId !== 'string' || !outbreakId)
) {
return callback(app.utils.apiError.getError('REQUEST_VALIDATION_ERROR', {
errorMessages: 'Property "outbreakIDs" must be a list of outbreak ids'
}));
}

requestedOutbreakIDs = _.uniq(data.outbreakIDs);
}

// initialize flag to know if the callback function was already called
// need to do this as we will call it and then continue doing some actions
let callbackCalled = false;
Expand All @@ -836,6 +851,18 @@ module.exports = function (Sync) {
// initialize variable for caching the system settings
let systemSettings;

/**
* Save the step the sync reached, so it can be displayed while it is in progress
*/
const setSyncStep = (step) => {
syncLogEntry.syncStep = step;
return syncLogEntry
.save(options)
.catch((err) => {
app.logger.debug(`Sync ${syncLogEntry.id}: Error updating sync log entry step. ${err}`);
});
};

// check if the received upstream server URL matches one from the configured upstream servers
app.models.systemSettings
.findOne()
Expand Down Expand Up @@ -879,7 +906,8 @@ module.exports = function (Sync) {
return app.models.syncLog.create({
syncServerUrl: upstreamServerEntry.url,
actionStartDate: localizationHelper.now().toDate(),
status: 'LNG_SYNC_STATUS_IN_PROGRESS'
status: 'LNG_SYNC_STATUS_IN_PROGRESS',
syncStep: 'CONNECT'
}, options);
})
.then(function (syncLog) {
Expand All @@ -896,26 +924,102 @@ module.exports = function (Sync) {

app.logger.debug(`Started sync ${syncLogEntry.id}`);

// continue sync
// get available outbreakIDs for the client
return Sync.getAvailableOutbreaksIDs(upstreamServerEntry, syncLogEntry);
// check if the server can be reached & if it accepts the credentials, so the user knows which step failed
// Note: the check only explains a failure; it never stops a sync that would work
return upstreamServerCheck
.check({
url: upstreamServerEntry.url,
clientId: _.get(upstreamServerEntry, 'credentials.clientId'),
clientSecret: _.get(upstreamServerEntry, 'credentials.clientSecret')
})
.catch(function (err) {
app.logger.debug(`Sync ${syncLogEntry.id}: Couldn't check the upstream server. ${err}`);
return null;
})
.then(function (checkResult) {
// the server was reached
const serverOffline = checkResult && !checkResult.server.online;
return (serverOffline ? Promise.resolve() : setSyncStep('CREDENTIALS'))
.then(function () {
// get available outbreakIDs for the client
return Sync.getAvailableOutbreaksIDs(upstreamServerEntry, syncLogEntry);
})
.catch(function (err) {
// explain why
const failedCheck = serverOffline ?
checkResult.server : (
checkResult && checkResult.credentials && !checkResult.credentials.valid ?
checkResult.credentials :
null
);
if (failedCheck) {
syncLogEntry.syncStepErrorCode = failedCheck.errorCode;
syncLogEntry.syncStepErrorDetail = failedCheck.code;
}

throw err;
});
});
})
.then(function (outbreakIDs) {
// send only the chosen outbreaks, if the upstream server accepts them (no outbreaks means all of them)
const chosenOutbreakIDs = requestedOutbreakIDs !== undefined ?
requestedOutbreakIDs :
_.uniq(upstreamServerEntry.outbreakIDs || []);
if (chosenOutbreakIDs.length) {
const acceptedOutbreakIDs = outbreakIDs.length ?
chosenOutbreakIDs.filter((outbreakId) => outbreakIDs.includes(outbreakId)) :
chosenOutbreakIDs;
if (!acceptedOutbreakIDs.length) {
throw app.utils.apiError.getError('REQUEST_VALIDATION_ERROR', {
errorMessages: 'None of the chosen outbreaks is accepted by the upstream server'
});
}

outbreakIDs = acceptedOutbreakIDs;
}

app.logger.debug(`Sync ${syncLogEntry.id}: Sync will be done for ${outbreakIDs.length ? ('the following outbreaks: ' + outbreakIDs.join(', ')) : 'all the outbreaks in the system'}`);
// save retrieve outbreak IDs on the sync log entry
syncLogEntry.outbreakIDs = outbreakIDs;

// check if the outbreaks with the given IDs were ever successfully synced with the upstream server
// we will only sync daca updated from the last sync
// a previous sync can be used only if it sent all the outbreaks that are sent now, otherwise what was changed in the others would never be sent
// (e.g. an outbreak was added to the ones synced with the server, or the previous sync sent only some outbreaks)
const sentAllOutbreaksConditions = [{
outbreakIDs: {
$exists: false
}
}, {
outbreakIDs: null
}, {
outbreakIDs: {
$size: 0
}
}];
return app.models.syncLog
.findOne({
where: {
syncServerUrl: upstreamServerEntry.url,
status: 'LNG_SYNC_STATUS_SUCCESS'
.rawFind({
syncServerUrl: upstreamServerEntry.url,
status: 'LNG_SYNC_STATUS_SUCCESS',
$or: outbreakIDs.length ?
sentAllOutbreaksConditions.concat([{
outbreakIDs: {
$all: outbreakIDs
}
}]) :
sentAllOutbreaksConditions
}, {
projection: {
actionStartDate: 1
},
order: 'actionStartDate DESC'
sort: {
actionStartDate: -1
},
limit: 1
})
.then(function (lastSyncLogEntry) {
.then(function (lastSyncLogEntries) {
const lastSyncLogEntry = lastSyncLogEntries[0];
// determine the date from which we will sync the data
// by default it is the start of the last successful sync (or all data if there wasn't any), but the user can ask for more data
const fromDateResult = syncFromDate.determineFromDate({
Expand Down Expand Up @@ -965,7 +1069,7 @@ module.exports = function (Sync) {
const password = getSyncEncryptPassword(null, upstreamServerEntry.credentials, upstreamServerEntry.autoEncrypt);

app.logger.debug(`Sync ${syncLogEntry.id}: Exporting DB.`);
return new Promise(function (resolve, reject) {
return setSyncStep('EXPORT').then(() => new Promise(function (resolve, reject) {
Sync.exportDatabase(
filter,
collections,
Expand All @@ -980,6 +1084,7 @@ module.exports = function (Sync) {
// nothing changed since the last sync, so the upstream server is already up to date
if (err.code === 'NO-DATA') {
app.logger.debug(`Sync ${syncLogEntry.id}: No data to sync.`);
syncLogEntry.syncStep = 'NO_DATA';
return resolve();
}

Expand All @@ -988,7 +1093,7 @@ module.exports = function (Sync) {
app.logger.debug(`Sync ${syncLogEntry.id}: DB exported at ${fileName}.`);
return resolve(fileName);
});
});
}));
})
.then(function (exportedDBFileName) {
// nothing to send
Expand All @@ -997,7 +1102,7 @@ module.exports = function (Sync) {
}

// 2: send DB to be synced on the upstream server
return Sync.sendDBSnapshotForImport(upstreamServerEntry, exportedDBFileName, true, syncLogEntry);
return setSyncStep('SEND').then(() => Sync.sendDBSnapshotForImport(upstreamServerEntry, exportedDBFileName, true, syncLogEntry));
})
.then(function () {
// sync was successful
Expand Down
12 changes: 12 additions & 0 deletions common/models/sync-log.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
"syncClientId": {
"type": "string",
"description": "Client ID which is synced with the server. Present in the entries for Server instances"
},
"syncStep": {
"type": "string",
"description": "Step of a sync with an upstream server: CONNECT, CREDENTIALS, EXPORT, SEND, IMPORT or NO_DATA (nothing to send). When the sync fails, it is the step that failed"
},
"syncStepErrorCode": {
"type": "string",
"description": "Why the server couldn't be reached or didn't accept the credentials (e.g. CONNECTION_REFUSED, INVALID_CREDENTIALS)"
},
"syncStepErrorDetail": {
"type": "string",
"description": "Technical code of the connection error"
}
},
"validations": [],
Expand Down
9 changes: 9 additions & 0 deletions common/models/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,15 @@ module.exports = function (Sync) {
return client.sendDBSnapshotForImport(DBSnapshotFileName, asynchronous, upstreamServer.autoEncrypt ? 'true' : 'false')
.then(function (syncLogId) {
app.logger.debug(`Sync ${syncLogEntry.id}: Upstream server import: received upstream server sync log id: ${syncLogId}`);

// the package was received, now the upstream server is importing it
syncLogEntry.syncStep = 'IMPORT';
syncLogEntry
.save()
.catch((err) => {
app.logger.debug(`Sync ${syncLogEntry.id}: Error updating sync log entry step. ${err}`);
});

// initialize container for server sync log entry status check connection error
// will be updated only with connection errors
let statusCheckConnectionError = null;
Expand Down
2 changes: 1 addition & 1 deletion server/boot/scheduler.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"sync":{},"backup":{"startTime":"2026-06-17T14:01:55.370Z","lastExecutedTime":"2026-09-29T01:20:36.360Z","timeUnit":"h","interval":24,"backupTime":null},"backupCleanup":{"startTime":"2026-06-17T14:01:55.370Z","lastExecutedTime":"2026-09-29T01:20:36.359Z","timeUnit":"d","interval":90},"teamNotifications":{"startTime":"2026-06-17T14:01:55.370Z","lastExecutedTime":null,"timeUnit":"m","interval":5}}
{"sync":{"http://localhost:3001/api":{"startTime":"2026-09-30T01:57:43.683Z","lastExecutedTime":"2026-10-05T01:48:48.141Z","timeUnit":"h","interval":1}},"backup":{"startTime":"2026-06-17T14:01:55.370Z","lastExecutedTime":"2026-09-29T01:20:36.360Z","timeUnit":"h","interval":24,"backupTime":null},"backupCleanup":{"startTime":"2026-06-17T14:01:55.370Z","lastExecutedTime":"2026-09-29T01:20:36.359Z","timeUnit":"d","interval":90},"teamNotifications":{"startTime":"2026-06-17T14:01:55.370Z","lastExecutedTime":"2026-10-05T01:55:02.753Z","timeUnit":"m","interval":5}}
9 changes: 8 additions & 1 deletion server/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,14 @@
"signoutUsersOnRestart": false,
"cors": {
"enabled": false,
"whitelist": []
"whitelist": [
"http://54.232.189.113:3000",
"https://54.232.189.113:3000",
"http://10.0.8.2:3000",
"https://10.0.8.2:3000",
"http://192.168.1.22:3000",
"https://192.168.1.22:3000"
]
},
"enableConfigRewrite": true,
"authToken": {
Expand Down
4 changes: 2 additions & 2 deletions server/install/scripts/migrateModelData.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,13 +701,13 @@ const migrationVersions = [{
fileName: 'languageToken.js',
actions: [{
name: 'createUpdateLanguageTokens',
buildNo: 1
buildNo: 6
}, {
name: 'createUpdateSingleEnglishLanguageTokens',
buildNo: 1
}, {
name: 'createUpdatePortugueseLanguageTokens',
buildNo: 1
buildNo: 6
}]
}]
}];
Expand Down
114 changes: 114 additions & 0 deletions server/install/scripts/migrations/2.58.2/data/english/english_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,120 @@
},
"LNG_SYNC_DIRECTION_RECEIVED": {
"translation": "Received"
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_TITLE": {
"translation": "Synchronize with server"
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_HINT_SINCE_LAST": {
"translation": "Only the records created or updated since the last successful synchronization are sent. This is the usual option and the fastest one."
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_HINT_FROM_DATE": {
"translation": "The records created or updated since the chosen date are sent. Use it to send older data again, for example to fill a server that was recreated."
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_HINT_ALL": {
"translation": "All the data is sent. It may take a long time; the server only updates the records that are newer than the ones it already has."
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_HINT_FIRST_SYNC": {
"translation": "There was no synchronization with this server yet, so all the data will be sent."
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_OUTBREAKS": {
"translation": "Outbreaks"
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_OUTBREAKS_TOOLTIP": {
"translation": "Filled with the outbreaks configured for the server. Leave it empty to send all the outbreaks."
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_HINT_PARTIAL": {
"translation": "The chosen outbreaks don't include all the ones configured for this server. This synchronization isn't used to determine what the next ones send, so they still send what changed in the outbreaks that were left out."
},
"LNG_UPSTREAM_SERVER_FIELD_LABEL_OUTBREAKS_TO_SYNC": {
"translation": "Outbreaks to synchronize"
},
"LNG_UPSTREAM_SERVER_FIELD_LABEL_OUTBREAKS_TO_SYNC_ALL": {
"translation": "All outbreaks"
},
"LNG_UPSTREAM_SERVER_FIELD_LABEL_OUTBREAKS_TO_SYNC_DESCRIPTION": {
"translation": "Leave it empty to synchronize all the outbreaks. The scheduled synchronizations and the ones made on every change send only the chosen outbreaks. When an outbreak is added, the next synchronization sends all the data of the chosen outbreaks."
},
"LNG_SYNC_PROGRESS_DIALOG_TITLE": {
"translation": "Synchronizing with the server"
},
"LNG_SYNC_PROGRESS_DIALOG_STEP": {
"translation": "Step"
},
"LNG_SYNC_PROGRESS_DIALOG_STEP_CONNECT": {
"translation": "Connecting servers"
},
"LNG_SYNC_PROGRESS_DIALOG_STEP_CREDENTIALS": {
"translation": "Validating credentials"
},
"LNG_SYNC_PROGRESS_DIALOG_STEP_SYNC": {
"translation": "Synchronizing data"
},
"LNG_SYNC_PROGRESS_DIALOG_STEP_FINAL": {
"translation": "Final status"
},
"LNG_SYNC_PROGRESS_DIALOG_PHASE_CONNECT": {
"translation": "Establishing the connection with the server..."
},
"LNG_SYNC_PROGRESS_DIALOG_PHASE_CREDENTIALS": {
"translation": "Checking the client ID and secret..."
},
"LNG_SYNC_PROGRESS_DIALOG_PHASE_EXPORT": {
"translation": "Preparing the data to send..."
},
"LNG_SYNC_PROGRESS_DIALOG_PHASE_SEND": {
"translation": "Sending the data to the server..."
},
"LNG_SYNC_PROGRESS_DIALOG_PHASE_IMPORT": {
"translation": "The server is importing the data..."
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_SUCCESS": {
"translation": "Success!"
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_SUCCESS_MESSAGE": {
"translation": "The data was synchronized."
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_WARNING": {
"translation": "Synchronized with warnings"
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_WARNING_MESSAGE": {
"translation": "The data was synchronized, but some records had problems. See the details in Track synchronization."
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_UP_TO_DATE": {
"translation": "Up to date"
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_UP_TO_DATE_MESSAGE": {
"translation": "There was nothing new to send since the last synchronization."
},
"LNG_SYNC_PROGRESS_DIALOG_RESULT_ERROR": {
"translation": "Synchronization failed"
},
"LNG_SYNC_PROGRESS_DIALOG_ERROR_CONNECT": {
"translation": "It wasn't possible to connect to the server \"{{name}}\". Check if it is online and if the URL is correct."
},
"LNG_SYNC_PROGRESS_DIALOG_ERROR_CREDENTIALS": {
"translation": "The server \"{{name}}\" didn't accept the credentials. Check the client ID and secret in the server settings."
},
"LNG_SYNC_PROGRESS_DIALOG_ERROR_SYNC": {
"translation": "An error occurred while synchronizing the data."
},
"LNG_SYNC_PROGRESS_DIALOG_LOG_TITLE": {
"translation": "Error details"
},
"LNG_SYNC_PROGRESS_DIALOG_LOG_HELP": {
"translation": "If the problem continues, contact the system administrator or the person responsible for the destination server."
},
"LNG_SYNC_PROGRESS_DIALOG_VIEW_ERROR_BUTTON": {
"translation": "View full log"
},
"LNG_SYNC_PROGRESS_DIALOG_CLOSE_BUTTON": {
"translation": "Close"
},
"LNG_SYNC_PROGRESS_DIALOG_BACKGROUND_BUTTON": {
"translation": "Continue in background"
},
"LNG_PAGE_LIST_SYSTEM_UPSTREAM_SERVERS_SYNC_DIALOG_OUTBREAKS_ALL": {
"translation": "All outbreaks (none selected)"
}
}
}
Loading
Loading