diff --git a/front/css/app.css b/front/css/app.css
index 114e5655..d96ffdc7 100755
--- a/front/css/app.css
+++ b/front/css/app.css
@@ -664,7 +664,9 @@ body
border-right: 5px solid #606060;
}
-
+.fc-ltr .fc-time-grid .fc-event-container {
+ margin: 0 2.5% 0 0px !important;
+}
/* -----------------------------------------------------------------------------
Notification float banner
diff --git a/front/deviceDetailsEdit.php b/front/deviceDetailsEdit.php
index 8efcb855..971396b0 100755
--- a/front/deviceDetailsEdit.php
+++ b/front/deviceDetailsEdit.php
@@ -210,14 +210,14 @@ function getDeviceData() {
groupSettings.forEach(setting => {
const column = $('
'); // Create a column for each setting (Bootstrap column)
- console.log(setting);
+ // console.log(setting);
// Get the field data (replace 'NEWDEV_' prefix from the key)
fieldData = deviceData[setting.setKey.replace('NEWDEV_', '')]
fieldData = fieldData == null ? "" : fieldData;
fieldOptionsOverride = null;
- console.log(setting.setKey);
+ // console.log(setting.setKey);
// console.log(fieldData);
// Additional form elements like the random MAC address button for devMac
diff --git a/front/deviceDetailsEvents.php b/front/deviceDetailsEvents.php
index a8d988d5..9c2351fa 100755
--- a/front/deviceDetailsEvents.php
+++ b/front/deviceDetailsEvents.php
@@ -30,51 +30,72 @@
\ No newline at end of file
diff --git a/front/js/common.js b/front/js/common.js
index 15849d52..2ea86506 100755
--- a/front/js/common.js
+++ b/front/js/common.js
@@ -373,7 +373,7 @@ function getLangCode() {
const LOCALE = getSetting('UI_LOCALE') || 'en-GB';
// -----------------------------------------------------------------------------
-// String utilities
+// DateTime utilities
// -----------------------------------------------------------------------------
function localizeTimestamp(input) {
@@ -462,6 +462,54 @@ function localizeTimestamp(input) {
}
+/**
+ * Returns start and end date for a given period.
+ * @param {string} period - "1 day", "7 days", "1 month", "1 year", "100 years"
+ * @returns {{start: string, end: string}} - Dates in "YYYY-MM-DD HH:MM:SS" format
+ */
+function getPeriodStartEnd(period) {
+ const now = new Date();
+ let start = new Date(now); // default start = now
+ let end = new Date(now); // default end = now
+
+ switch (period) {
+ case "1 day":
+ start.setDate(now.getDate() - 1);
+ break;
+ case "7 days":
+ start.setDate(now.getDate() - 7);
+ break;
+ case "1 month":
+ start.setMonth(now.getMonth() - 1);
+ break;
+ case "1 year":
+ start.setFullYear(now.getFullYear() - 1);
+ break;
+ case "100 years":
+ start = new Date(0); // very old date for "all"
+ break;
+ default:
+ console.warn("Unknown period, using 1 month as default");
+ start.setMonth(now.getMonth() - 1);
+ }
+
+ // Helper function to format date as "YYYY-MM-DD HH:MM:SS"
+ const formatDate = (date) => {
+ const pad = (n) => String(n).padStart(2, "0");
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
+ };
+
+ return {
+ start: formatDate(start),
+ end: formatDate(end)
+ };
+}
+
+// -----------------------------------------------------------------------------
+// String utilities
+// -----------------------------------------------------------------------------
+
// ----------------------------------------------------
/**
diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py
index 74edfa36..a374f8c4 100755
--- a/server/api_server/api_server_start.py
+++ b/server/api_server/api_server_start.py
@@ -938,8 +938,9 @@ def api_get_sessions_calendar():
# Query params: /sessions/calendar?start=2025-08-01&end=2025-08-21
start_date = request.args.get("start")
end_date = request.args.get("end")
+ mac = request.args.get("mac")
- return get_sessions_calendar(start_date, end_date)
+ return get_sessions_calendar(start_date, end_date, mac)
@app.route("/sessions/", methods=["GET"])
diff --git a/server/api_server/sessions_endpoint.py b/server/api_server/sessions_endpoint.py
index 225dbe39..8bf16e38 100755
--- a/server/api_server/sessions_endpoint.py
+++ b/server/api_server/sessions_endpoint.py
@@ -12,7 +12,7 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from database import get_temp_db_connection # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value, format_ip_long # noqa: E402 [flake8 lint suppression]
from db.db_helper import get_date_from_period # noqa: E402 [flake8 lint suppression]
-from utils.datetime_utils import format_date_iso, format_event_date, format_date_diff, format_date # noqa: E402 [flake8 lint suppression]
+from utils.datetime_utils import timeNowDB, format_date_iso, format_event_date, format_date_diff, format_date # noqa: E402 [flake8 lint suppression]
# --------------------------
@@ -88,36 +88,42 @@ def get_sessions(mac=None, start_date=None, end_date=None):
return jsonify({"success": True, "sessions": table_data})
-def get_sessions_calendar(start_date, end_date):
+def get_sessions_calendar(start_date, end_date, mac):
"""
Fetch sessions between a start and end date for calendar display.
- Returns JSON list of calendar sessions.
+ Returns FullCalendar-compatible JSON.
"""
if not start_date or not end_date:
return jsonify({"success": False, "error": "Missing start or end date"}), 400
+ # Normalize MAC (empty string → NULL)
+ mac = mac or None
+
conn = get_temp_db_connection()
+ conn.row_factory = sqlite3.Row
cur = conn.cursor()
sql = """
- -- Correct missing connection/disconnection sessions:
- -- If ses_EventTypeConnection is missing, backfill from last disconnection
- -- If ses_EventTypeDisconnection is missing, forward-fill from next connection
-
SELECT
- SES1.ses_MAC, SES1.ses_EventTypeConnection, SES1.ses_DateTimeConnection,
- SES1.ses_EventTypeDisconnection, SES1.ses_DateTimeDisconnection, SES1.ses_IP,
- SES1.ses_AdditionalInfo, SES1.ses_StillConnected,
+ SES1.ses_MAC,
+ SES1.ses_EventTypeConnection,
+ SES1.ses_DateTimeConnection,
+ SES1.ses_EventTypeDisconnection,
+ SES1.ses_DateTimeDisconnection,
+ SES1.ses_IP,
+ SES1.ses_AdditionalInfo,
+ SES1.ses_StillConnected,
CASE
WHEN SES1.ses_EventTypeConnection = '' THEN
IFNULL(
- (SELECT MAX(SES2.ses_DateTimeDisconnection)
- FROM Sessions AS SES2
- WHERE SES2.ses_MAC = SES1.ses_MAC
- AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection
- AND SES2.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?)
+ (
+ SELECT MAX(SES2.ses_DateTimeDisconnection)
+ FROM Sessions AS SES2
+ WHERE SES2.ses_MAC = SES1.ses_MAC
+ AND SES2.ses_DateTimeDisconnection < SES1.ses_DateTimeDisconnection
+ AND SES2.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?)
),
DATETIME(SES1.ses_DateTimeDisconnection, '-1 hour')
)
@@ -126,41 +132,46 @@ def get_sessions_calendar(start_date, end_date):
CASE
WHEN SES1.ses_EventTypeDisconnection = '' THEN
- (SELECT MIN(SES2.ses_DateTimeConnection)
- FROM Sessions AS SES2
- WHERE SES2.ses_MAC = SES1.ses_MAC
- AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection
- AND SES2.ses_DateTimeConnection BETWEEN Date(?) AND Date(?)
+ (
+ SELECT MIN(SES2.ses_DateTimeConnection)
+ FROM Sessions AS SES2
+ WHERE SES2.ses_MAC = SES1.ses_MAC
+ AND SES2.ses_DateTimeConnection > SES1.ses_DateTimeConnection
+ AND SES2.ses_DateTimeConnection BETWEEN Date(?) AND Date(?)
)
ELSE SES1.ses_DateTimeDisconnection
END AS ses_DateTimeDisconnectionCorrected
FROM Sessions AS SES1
- WHERE (SES1.ses_DateTimeConnection BETWEEN Date(?) AND Date(?))
+ WHERE (
+ (SES1.ses_DateTimeConnection BETWEEN Date(?) AND Date(?))
OR (SES1.ses_DateTimeDisconnection BETWEEN Date(?) AND Date(?))
OR SES1.ses_StillConnected = 1
+ )
+ AND (? IS NULL OR SES1.ses_MAC = ?)
"""
cur.execute(
sql,
(
- start_date,
- end_date,
- start_date,
- end_date,
- start_date,
- end_date,
- start_date,
- end_date,
+ start_date, end_date,
+ start_date, end_date,
+ start_date, end_date,
+ start_date, end_date,
+ mac, mac,
),
)
+
rows = cur.fetchall()
+ conn.close()
- table_data = []
- for r in rows:
- row = dict(r)
+ now_iso = timeNowDB()
- # Determine color
+ events = []
+ for row in rows:
+ row = dict(row)
+
+ # Color logic (unchanged from PHP)
if (
row["ses_EventTypeConnection"] == "" or row["ses_EventTypeDisconnection"] == ""
):
@@ -170,28 +181,31 @@ def get_sessions_calendar(start_date, end_date):
else:
color = "#0073b7"
- # Tooltip
+ # --- IMPORTANT FIX ---
+ # FullCalendar v3 CANNOT handle end = null
+ end_dt = row["ses_DateTimeDisconnectionCorrected"]
+ if not end_dt and row["ses_StillConnected"] == 1:
+ end_dt = now_iso
+
tooltip = (
f"Connection: {format_event_date(row['ses_DateTimeConnection'], row['ses_EventTypeConnection'])}\n"
f"Disconnection: {format_event_date(row['ses_DateTimeDisconnection'], row['ses_EventTypeDisconnection'])}\n"
f"IP: {row['ses_IP']}"
)
- # Append calendar entry
- table_data.append(
+ events.append(
{
"resourceId": row["ses_MAC"],
"title": "",
"start": format_date_iso(row["ses_DateTimeConnectionCorrected"]),
- "end": format_date_iso(row["ses_DateTimeDisconnectionCorrected"]),
+ "end": format_date_iso(end_dt),
"color": color,
"tooltip": tooltip,
"className": "no-border",
}
)
- conn.close()
- return jsonify({"success": True, "sessions": table_data})
+ return jsonify({"success": True, "sessions": events})
def get_device_sessions(mac, period):