Enhance logging functionality in API and UI components

- Implemented detailed logging for USB serial communication in readdevice.js.
- Added log file management features in logfile.php, including deletion and selection of log files.
- Created a new communication log API endpoint in com_log.php to store USB communication data.
- Improved user interface for log file selection and added confirmation for log deletion.
This commit is contained in:
“VeLiTi”
2025-11-14 14:04:46 +01:00
parent 3c99272f5f
commit bd27bab30f
4 changed files with 412 additions and 62 deletions

View File

@@ -168,7 +168,10 @@ if($is_jwt_valid && str_contains($version, 'v')) {
// END check if endPoint is fileUpload
//------------------------------------------
if (isAllowed($collection,$profile,$permission,'R') === 1 && empty($input) && file_exists($api_file)){
if ($collection === 'com_log' && file_exists($api_file_post)) {
include_once $api_file_post;
}
elseif (isAllowed($collection,$profile,$permission,'R') === 1 && empty($input) && file_exists($api_file)){
include_once $api_file;
}

88
api/v2/post/com_log.php Normal file
View File

@@ -0,0 +1,88 @@
<?php
//defined($security_key) or exit;
//------------------------------------------
// Communication Log API endpoint
// Stores all USB serial communication data in date-organized files
//------------------------------------------
$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
//------------------------------------------
// Application related calls
//------------------------------------------
$action = $request[2];
$post_content = json_decode($input,true);
//SET PARAMETERS FOR QUERY
$date = date('Y-m-d H:i:s');
$current_date = date('Y-m-d');
// Connect to DB (if needed for future enhancements)
$pdo = dbConnect($dbname);
if ($action == 'log'){
// Validate input data
if (!empty($post_content['data']) && isset($post_content['direction'])) {
$communication_data = $post_content['data'];
$direction = $post_content['direction']; // 'sent' or 'received'
$timestamp = $post_content['timestamp'] ?? $date;
$serial_number = $post_content['serial_number'] ?? '';
$maintenance_run = $post_content['maintenance_run'] ?? 0;
// Create log directory if it doesn't exist
$log_dir = './log/';
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
// Create filename based on current date (YYYY-MM-DD)
$log_file = $log_dir .'serial'. $current_date . '.log';
// Format log entry
$log_entry = sprintf(
"[%s] [%s] [SN:%s] [MAINT:%d] %s: %s\n",
$timestamp,
$direction,
$serial_number,
$maintenance_run,
strtoupper($direction),
$communication_data
);
// Append to log file
if (file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX) !== false) {
// Success response
echo json_encode(array(
'status' => 'success',
'message' => 'Communication logged successfully',
'file' => $log_file
));
} else {
// Error response
http_response_code(500);
echo json_encode(array(
'status' => 'error',
'message' => 'Failed to write to log file'
));
}
} else {
// Invalid payload
http_response_code(400);
echo json_encode(array(
'status' => 'error',
'message' => 'Invalid payload: missing data or direction'
));
}
} else {
// Invalid action
http_response_code(400);
echo json_encode(array(
'status' => 'error',
'message' => 'Invalid action'
));
}
?>

View File

@@ -1,6 +1,52 @@
var port, textEncoder, writableStreamClosed, writer, historyIndex = -1;
const lineHistory = [];
maintenanceRun = 0;
handshakeComplete = false;
// Function to log communication to API
async function logCommunication(data, direction) {
// Log all communication including connection/disconnection events
try {
// Get service token for API authentication
const serviceToken = document.getElementById("servicetoken")?.innerHTML || '';
// Get serial number if available
let serialNumber = '';
if (typeof serial !== 'undefined' && serial) {
serialNumber = serial;
}
const logData = {
data: data,
direction: direction, // 'sent', 'received', 'connected', 'disconnected', 'handshake'
timestamp: new Date().toISOString(),
serial_number: serialNumber,
maintenance_run: maintenanceRun
};
// Get base URL for API calls (assuming 'link' variable is defined globally)
const url = link + '/v2/com_log/log';
const bearer = 'Bearer ' + serviceToken;
const response = await fetch(url, {
method: 'POST',
withCredentials: true,
credentials: 'include',
headers: {
'Authorization': bearer,
'Content-Type': 'application/json'
},
body: JSON.stringify(logData)
});
if (!response.ok) {
console.warn('Failed to log communication:', response.status);
}
} catch (error) {
console.warn('Error logging communication:', error);
}
}
function progressBar(percentage, message, color){
var readbar = document.getElementById("readBar");
@@ -14,6 +60,18 @@ async function connectSerial() {
// Prompt user to select any serial port.
const filters = [{ usbVendorId: 1027, usbProductId: 24597 }];
port = await navigator.serial.requestPort({ filters });
// Log selected port details
const portInfo = port.getInfo();
const portDetails = {
usbVendorId: portInfo.usbVendorId,
usbProductId: portInfo.usbProductId,
readable: !!port.readable,
writable: !!port.writable,
opened: port.readable !== null && port.writable !== null
};
await logCommunication(`Selected USB device - ${JSON.stringify(portDetails)}`, 'connected');
await port.open({ baudRate: 56700 });
listenToPort();
@@ -22,7 +80,20 @@ async function connectSerial() {
writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
writer = textEncoder.writable.getWriter();
} catch {
// Log successful connection with details
const portInfoSuccess = port.getInfo();
const portDetailsSuccess = {
usbVendorId: portInfoSuccess.usbVendorId,
usbProductId: portInfoSuccess.usbProductId,
readable: !!port.readable,
writable: !!port.writable,
opened: port.readable !== null && port.writable !== null
};
await logCommunication(`Serial port opened successfully (baudRate: 56700) - ${JSON.stringify(portDetailsSuccess)}`, 'connected');
} catch (error) {
// Log connection failure details
await logCommunication(`Serial connection failed: ${error.message || 'Unknown error'}`, 'disconnected');
alert("Serial Connection Failed");
}
}
@@ -36,6 +107,10 @@ async function sendSerialLine() {
dataToSend = dataToSend + "\n"; //new line
appendToTerminal("> " + dataToSend); //send echo
await writer.write(dataToSend);
// Log sent data
await logCommunication(dataToSend.trim(), 'sent');
document.getElementById("lineToSend").value = "";
//await writer.releaseLock();
}
@@ -194,6 +269,10 @@ function writeToStream(command, payload){
dataToSend = command + payload + limiter;
console.log(dataToSend);
writer.write(dataToSend);
// Log sent data
logCommunication(dataToSend.trim(), 'sent');
maintenanceRun++;
console.log(maintenanceRun);
maintenanceTest();
@@ -218,6 +297,8 @@ async function listenToPort() {
if (x.indexOf("STATE=MAINTENANCE") > 0 && maintenanceRun == 1)
{
progressBar("50", "Initializing program" , "#ff6666")
handshakeComplete = true; // Mark handshake as complete
await logCommunication('Handshake complete - entering maintenance mode', 'handshake');
setTimeout(maintenanceTest,3000);
maintenanceRun = 2;
}
@@ -231,6 +312,18 @@ const plug_data = document.getElementById("plug_data");
async function appendToTerminal(newStuff) {
serialResultsDiv.innerHTML += newStuff;
// Log received data - check if this is handshake data
let logDirection = 'received';
if (!handshakeComplete && maintenanceRun === 0) {
// Check if this contains device identification info (handshake)
if (newStuff.includes('PN=') || newStuff.includes('SN=') || newStuff.includes('HW=') ||
newStuff.includes('FW=') || newStuff.includes('STATE=')) {
logDirection = 'handshake';
}
}
await logCommunication(newStuff.trim(), logDirection);
const keyword = '$STATUS;STATE';
const keyword2 = 'PWM';
@@ -431,13 +524,34 @@ function getData() {
async function closePort(){
try {
// Log port closure start
const portInfo = port.getInfo();
const portDetails = {
usbVendorId: portInfo.usbVendorId,
usbProductId: portInfo.usbProductId,
readable: !!port.readable,
writable: !!port.writable,
opened: port.readable !== null && port.writable !== null
};
await logCommunication(`Starting port closure - ${JSON.stringify(portDetails)}`, 'disconnected');
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed
await writableStreamClosed;
console.log(maintenanceRun);
await port.close();
// Log successful port closure
await logCommunication(`Serial port closed successfully - VendorID: ${portInfo.usbVendorId}, ProductID: ${portInfo.usbProductId}`, 'disconnected');
} catch (error) {
// Log port closure failure
await logCommunication(`Serial port closure failed: ${error.message || 'Unknown error'}`, 'disconnected');
}
handshakeComplete = false; // Reset handshake for next connection
//Check for errors maintenanceRun = 999
if (maintenanceRun != 999){
serialResultsDiv.innerHTML = '';
@@ -463,13 +577,34 @@ async function closePort(){
async function closePortCarTest(){
try {
// Log port closure start
const portInfo = port.getInfo();
const portDetails = {
usbVendorId: portInfo.usbVendorId,
usbProductId: portInfo.usbProductId,
readable: !!port.readable,
writable: !!port.writable,
opened: port.readable !== null && port.writable !== null
};
await logCommunication(`Starting port closure (car test) - ${JSON.stringify(portDetails)}`, 'disconnected');
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed
await writableStreamClosed;
console.log(maintenanceRun);
await port.close();
// Log successful port closure
await logCommunication(`Serial port closed successfully (car test) - VendorID: ${portInfo.usbVendorId}, ProductID: ${portInfo.usbProductId}`, 'disconnected');
} catch (error) {
// Log port closure failure
await logCommunication(`Serial port closure failed (car test): ${error.message || 'Unknown error'}`, 'disconnected');
}
handshakeComplete = false; // Reset handshake for next connection
//Check for errors maintenanceRun = 999
if (maintenanceRun != 999){
serialResultsDiv.innerHTML = '';

View File

@@ -7,71 +7,195 @@ if (isAllowed('logfile',$_SESSION['profile'],$_SESSION['permission'],'R') === 0)
exit;
}
$filelocation = $_SERVER['DOCUMENT_ROOT'].'/log/log_'.date('d').'.txt';
$filelocation_webserver = '/var/www/vhosts/veliti.nl/logs/'.$_SERVER['HTTP_HOST'].'/access_ssl_log';
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// POST HANDLER - Delete all logs
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
$delete_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_all_logs'])) {
$logs_dir = __DIR__ . '/log/';
$deleted_count = 0;
// Capture post data
if (isset($_POST['logfile'])) {
// Save templates
file_put_contents($filelocation, $_POST['logfile']);
header('Location: index.php?page=logfile&success_msg=1');
if (is_dir($logs_dir)) {
$files = scandir($logs_dir);
foreach ($files as $file) {
if (preg_match('/\.(txt|log)$/', $file)) {
if (unlink($logs_dir . $file)) {
$deleted_count++;
}
}
}
}
$delete_message = "Successfully deleted $deleted_count log file(s).";
// Redirect to index page
header("Location: index.php?page=logfile&deleted=$deleted_count");
exit;
}
// Read language_US template PHP file
if (file_exists($filelocation)){
$contents = file_get_contents($filelocation);
} else {$contents = '';}
// Show delete confirmation message
if (isset($_GET['deleted'])) {
$delete_message = "Successfully deleted " . intval($_GET['deleted']) . " log file(s).";
}
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// GET HANDLER
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Get selected log file from query parameter
$selected_log = $_GET['log'] ?? '';
// Scan logs directory for all log files
$logs_dir = __DIR__ . '/log/';
$log_files = [];
if (is_dir($logs_dir)) {
$files = scandir($logs_dir);
foreach ($files as $file) {
if (preg_match('/\.(txt|log)$/', $file) && is_file($logs_dir . $file)) {
$log_files[] = [
'filename' => $file,
'path' => $logs_dir . $file,
'size' => filesize($logs_dir . $file),
'mtime' => filemtime($logs_dir . $file) // creation/modification time
];
}
}
}
// Sort by creation date descending (newest first)
usort($log_files, function($a, $b) {
return $b['mtime'] <=> $a['mtime'];
});
// Load selected log content
$contents = '';
if ($selected_log && file_exists($logs_dir . $selected_log)) {
$contents = file_get_contents($logs_dir . $selected_log);
} elseif (!empty($log_files)) {
// Default to most recent log
$contents = file_get_contents($log_files[0]['path']);
$selected_log = $log_files[0]['filename'];
} else {
$contents = 'No application log files found.';
}
$filelocation_webserver = '/var/www/vhosts/veliti.nl/logs/'.$_SERVER['HTTP_HOST'].'/access_ssl_log';
if (file_exists($filelocation_webserver)){
$contents_webserver = file_get_contents($filelocation_webserver);
} else {$contents_webserver = '';}
// Handle success messages
if (isset($_GET['success_msg'])) {
if ($_GET['success_msg'] == 1) {
$success_msg = 'Updated successfully!';
}
}
} else {$contents_webserver = 'No webserver log file found.';}
?>
<?=template_header('Log', 'log','view')?>
<form action="" method="post">
<div class="content-title responsive-flex-wrap responsive-pad-bot-3">
<h2 class="responsive-width-100">Debuglog</h2>
<input type="submit" name="submit" value="Save" class="btn">
</div>
<?php if (isset($success_msg)): ?>
<div class="msg success">
<?php if ($delete_message): ?>
<div class="msg success" id="delete-notification">
<i class="fas fa-check-circle"></i>
<p><?=$success_msg?></p>
<p><?=$delete_message?></p>
<i class="fas fa-times"></i>
</div>
<?php endif; ?>
<div class="tabs">
<a href="#" class="active">Application <?php echo date('d'); ?></a>
<a href="#" class="">Webserver</a>
<form action="" method="post">
<div class="content-title responsive-flex-wrap responsive-pad-bot-3">
<h2 class="responsive-width-100">Debuglog</h2>
<div style="display: flex; gap: 10px; align-items: center;">
<select id="log-file-selector" onchange="loadLogFile(this.value)" style="padding: 5px 10px; border: 1px solid #ddd; border-radius: 4px;">
<?php foreach ($log_files as $log): ?>
<?php $selected = ($log['filename'] === $selected_log) ? 'selected' : ''; ?>
<?php $size_kb = round($log['size'] / 1024, 2); ?>
<?php $date_str = date('Y-m-d H:i', $log['mtime']); ?>
<option value="<?=htmlspecialchars($log['filename'])?>" <?=$selected?>>
<?=htmlspecialchars($log['filename'])?> (<?=$size_kb?> KB) - <?=$date_str?>
</option>
<?php endforeach; ?>
</select>
<button type="button" onclick="refreshLog()" class="btn" >
<i class="fas fa-sync-alt"></i>
</button>
<button type="submit" name="delete_all_logs" onclick="return confirmDeleteAll()" class="btn" style="background: #e74c3c; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer;">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="tabs">
<a href="#" class="active">Application</a>
<a href="#" class="">Webserver</a>
</div>
<div class="content-block">
<div class="form responsive-width-100">
<div class="tab-content active">
<label for="Logfile"></label>
<textarea name="logfile" id="logfile" style="min-height: 100vh;"><?=$contents?></textarea>
<label for="Logfile">Application Log</label>
<textarea name="logfile" id="logfile" style="min-height: 70vh; font-family: 'Courier New', monospace; font-size: 12px; background: #1e1e1e; color: #f8f8f2; border: 1px solid #333; padding: 15px;"><?=$contents?></textarea>
</div>
<div class="tab-content">
<label for="Logfile"></label>
<textarea name="" id="" style="min-height: 100vh;"><?=$contents_webserver?></textarea>
<label for="WebserverLog">Webserver Log</label>
<textarea name="" id="webserver-log" style="min-height: 70vh; font-family: 'Courier New', monospace; font-size: 12px; background: #1e1e1e; color: #f8f8f2; border: 1px solid #333; padding: 15px;"><?=$contents_webserver?></textarea>
</div>
</div>
</div>
</form>
<script>
document.querySelectorAll("input[type='checkbox']").forEach(checkbox => {
checkbox.onclick = () => checkbox.value = checkbox.checked ? 'true' : 'false';
// Confirm delete all logs
function confirmDeleteAll() {
return confirm('⚠️ WARNING: This will permanently delete ALL log files in the logs directory.\n\nAre you absolutely sure you want to continue?');
}
// Auto-hide delete notification
document.addEventListener('DOMContentLoaded', function() {
const notification = document.getElementById('delete-notification');
if (notification) {
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.5s ease';
setTimeout(() => notification.remove(), 500);
}, 3000);
}
});
// Load selected log file
function loadLogFile(filename) {
if (filename) {
const url = new URL(window.location.href);
url.searchParams.set('log', filename);
window.location.href = url.toString();
}
}
// Refresh log functionality
function refreshLog() {
const button = event.target.closest('button');
const originalHTML = button.innerHTML;
// Show loading state
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
button.disabled = true;
// Reload the page to get fresh content
setTimeout(() => {
window.location.reload();
}, 500);
}
// Clear log content from view
function clearLogContent() {
const textarea = document.getElementById('logfile');
if (textarea) {
if (confirm('Clear the log content from view? (This won\'t delete the actual log file)')) {
textarea.value = '';
}
}
}
// Auto-scroll to bottom of logs when page loads
document.addEventListener('DOMContentLoaded', function() {
const activeTextarea = document.querySelector('.tab-content.active textarea');
if (activeTextarea && activeTextarea.value.trim()) {
activeTextarea.scrollTop = activeTextarea.scrollHeight;
}
});
</script>