Hello!
If anyone is interested, below are the steps to take to prevent silent automatic logout after the preset amount of time. This way, a small message pops up in the upper right hand side about 1 minute before logout and gives you an option to stay logged in. Very useful if you are typing a long progress note! Credit goes out to ChatGPT for this.
Cheers,
Alex.
_______________________________________________________________________
I added a 1-minute warning before OpenEMR’s automatic idle-session logout. This is useful because otherwise OpenEMR can log a user out while they are in the middle of entering a SOAP note or other unsaved data, with no warning.
The warning appears in the upper-right corner, shows a countdown, and has a “Stay Logged In” button. Clicking the button renews the actual OpenEMR session. If the warning is ignored, OpenEMR’s normal timeout/logout behavior remains unchanged.
This uses OpenEMR’s existing database-backed SessionTracker rather than creating a separate timeout mechanism.
Tested on OpenEMR 7.0 / PHP 8.3.
There are two files to modify.
============================================================
File:
/var/www/html/openemr/library/ajax/dated_reminders_counter.php
Find:
// ensure timeout has not happened
if (SessionTracker::isSessionExpired()) {
echo json_encode(['timeoutMessage' => 'timeout']);
exit;
}
// keep this below above time out check.
OpenEMR\Common\Session\SessionUtil::setSession('keepAliveTime', time());
Replace it with:
// Check the authoritative database-backed session tracker before
// performing the normal reminders/messages work.
//
// In addition to reporting an expired session, return the number of
// seconds remaining before OpenEMR's configured idle timeout.
//
// Use MariaDB NOW(), just as SessionTracker does, so the calculation
// remains correct even if PHP and database server clocks differ.
if (SessionTracker::isSessionExpired()) {
echo json_encode([
'timeoutMessage' => 'timeout',
'timeoutSecondsRemaining' => 0
]);
exit;
}
$sessionTracker = sqlQueryNoLog(
"SELECT
GREATEST(
0,
? - TIMESTAMPDIFF(SECOND, `last_updated`, NOW())
) AS seconds_remaining
FROM `session_tracker`
WHERE `uuid` = ?",
[
(int)$GLOBALS['timeout'],
$_SESSION['session_database_uuid']
]
);
$timeoutSecondsRemaining =
isset($sessionTracker['seconds_remaining'])
? (int)$sessionTracker['seconds_remaining']
: null;
// keep this below above time out check.
OpenEMR\Common\Session\SessionUtil::setSession('keepAliveTime', time());
Near the bottom of the same file, find:
$portal_count['reminderText'] = ($totalNumber > 0 ? text((int)$totalNumber) : '');
echo json_encode($portal_count);
Replace it with:
$portal_count['reminderText'] = ($totalNumber > 0 ? text((int)$totalNumber) : '');
if ($timeoutSecondsRemaining !== null) {
$portal_count['timeoutSecondsRemaining'] = $timeoutSecondsRemaining;
}
echo json_encode($portal_count);
============================================================
- MODIFY interface/main/tabs/main.php
File:
/var/www/html/openemr/interface/main/tabs/main.php
A. Add the warning state
Find:
var timed_out = false;
Immediately AFTER it, add:
// Session-expiration warning state.
//
// OpenEMR's database-backed session tracker remains authoritative.
// These variables only control the warning displayed during the final
// minute before the normal OpenEMR timeout.
var sessionWarningSeconds = 60;
var sessionSecondsRemaining = null;
var sessionWarningTimer = null;
var sessionWarningVisible = false;
B. Add the warning functions
Find:
function goRepeaterServices() {
Immediately BEFORE it, add:
function formatSessionWarningTime(seconds) {
seconds = Math.max(0, parseInt(seconds, 10) || 0);
var minutes = Math.floor(seconds / 60);
var remainingSeconds = seconds % 60;
return minutes + ":" + String(remainingSeconds).padStart(2, "0");
}
function updateSessionWarningCountdown() {
if (!sessionWarningVisible || sessionSecondsRemaining === null) {
return;
}
var countdown = document.getElementById("sessionTimeoutCountdown");
if (countdown) {
countdown.textContent =
formatSessionWarningTime(sessionSecondsRemaining);
}
if (sessionSecondsRemaining > 0) {
sessionSecondsRemaining--;
}
}
function showSessionTimeoutWarning(secondsRemaining) {
sessionSecondsRemaining =
Math.max(0, parseInt(secondsRemaining, 10) || 0);
var warning = document.getElementById("sessionTimeoutWarning");
if (!warning) {
return;
}
warning.style.display = "block";
sessionWarningVisible = true;
updateSessionWarningCountdown();
if (sessionWarningTimer === null) {
sessionWarningTimer =
window.setInterval(updateSessionWarningCountdown, 1000);
}
}
function hideSessionTimeoutWarning() {
var warning = document.getElementById("sessionTimeoutWarning");
if (warning) {
warning.style.display = "none";
}
sessionWarningVisible = false;
sessionSecondsRemaining = null;
if (sessionWarningTimer !== null) {
window.clearInterval(sessionWarningTimer);
sessionWarningTimer = null;
}
}
function renewOpenEmrSession() {
restoreSession();
var request = new FormData();
request.append("isPortal", isPortalEnabled);
request.append("csrf_token_form", csrf_token_js);
var button = document.getElementById("sessionTimeoutStayLoggedIn");
if (button) {
button.disabled = true;
button.textContent = <?php echo xlj('Renewing...'); ?>;
}
fetch(webroot_url + "/library/ajax/dated_reminders_counter.php", {
method: "POST",
credentials: "same-origin",
body: request
})
.then(function(response) {
if (!response.ok) {
throw new Error("Session renewal failed");
}
return response.json();
})
.then(function(data) {
if (data.timeoutMessage && data.timeoutMessage === "timeout") {
timeoutLogout();
return;
}
hideSessionTimeoutWarning();
})
.catch(function(error) {
console.error(error);
if (button) {
button.disabled = false;
button.textContent = <?php echo xlj('Stay Logged In'); ?>;
}
})
.finally(function() {
if (button && !sessionWarningVisible) {
button.disabled = false;
button.textContent = <?php echo xlj('Stay Logged In'); ?>;
}
});
}
C. Process the timeout information
Inside:
function goRepeaterServices() {
find:
if (data.timeoutMessage && (data.timeoutMessage == 'timeout')) {
// timeout has happened, so logout
timeoutLogout();
}
Immediately AFTER that block, add:
if (
typeof data.timeoutSecondsRemaining !== "undefined" &&
data.timeoutSecondsRemaining !== null
) {
var secondsRemaining =
parseInt(data.timeoutSecondsRemaining, 10);
if (
!isNaN(secondsRemaining) &&
secondsRemaining <= sessionWarningSeconds &&
secondsRemaining > 0
) {
showSessionTimeoutWarning(secondsRemaining);
} else if (secondsRemaining > sessionWarningSeconds) {
hideSessionTimeoutWarning();
}
}
D. Add the warning HTML
Find the opening:
<body>
Immediately AFTER it, add:
<div id="sessionTimeoutWarning"
role="alertdialog"
aria-live="assertive"
aria-labelledby="sessionTimeoutTitle"
style="display:none;">
<div class="session-timeout-title"
id="sessionTimeoutTitle">
<?php echo xlt('Session Expiring'); ?>
</div>
<div class="session-timeout-message">
<?php echo xlt('Your OpenEMR session will expire due to inactivity.'); ?>
</div>
<div class="session-timeout-row">
<div class="session-timeout-countdown">
<?php echo xlt('Time remaining'); ?>:
<strong id="sessionTimeoutCountdown">1:00</strong>
</div>
<button type="button"
id="sessionTimeoutStayLoggedIn"
class="btn btn-primary btn-sm"
onclick="renewOpenEmrSession();">
<?php echo xlt('Stay Logged In'); ?>
</button>
</div>
</div>
E. Add the CSS
Inside , immediately BEFORE the existing main block, add:
<style>
#sessionTimeoutWarning {
position: fixed;
top: 54px;
right: 18px;
z-index: 20000;
width: 330px;
padding: 12px 14px;
background: #fff;
border: 1px solid rgba(37, 67, 92, .18);
border-left: 4px solid #c58a23;
border-radius: 8px;
box-shadow:
0 3px 10px rgba(25, 42, 58, .10),
0 10px 28px rgba(25, 42, 58, .10);
color: #273746;
}
#sessionTimeoutWarning .session-timeout-title {
margin-bottom: 4px;
font-size: .93rem;
font-weight: 650;
color: #263d50;
}
#sessionTimeoutWarning .session-timeout-message {
margin-bottom: 10px;
font-size: .80rem;
line-height: 1.35;
color: #536675;
}
#sessionTimeoutWarning .session-timeout-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
#sessionTimeoutWarning .session-timeout-countdown {
font-size: .78rem;
color: #536675;
white-space: nowrap;
}
#sessionTimeoutWarning .session-timeout-countdown strong {
color: #9b6411;
font-size: .86rem;
}
#sessionTimeoutStayLoggedIn {
white-space: nowrap;
}
</style>
============================================================
HOW IT WORKS
OpenEMR already maintains the authoritative idle timeout in its
database-backed SessionTracker.
The existing dated_reminders_counter.php background request uses
skip_timeout_reset=1, so the background polling itself does not keep
an inactive user logged in.
The modification above also returns the number of seconds remaining
before the real OpenEMR session timeout.
During the final 60 seconds, main.php displays the warning and a local
countdown.
“Stay Logged In” makes an authenticated request WITHOUT
skip_timeout_reset. OpenEMR therefore treats it as real activity and
updates the normal SessionTracker expiration time.
If the user does nothing, the existing timeoutLogout() behavior remains
unchanged.
One limitation is that OpenEMR’s existing background check runs about
once per minute. Therefore the warning may initially appear with less
than exactly 60 seconds remaining (for example, 0:40). This could be
refined later without changing the basic approach.
For testing, temporarily setting the OpenEMR idle session timeout to
about 180 seconds makes it much easier to verify the behavior.