monitor/web/main/serverLog.ejs
2025-04-16 22:30:27 +07:00

510 lines
22 KiB
Plaintext

<%- await include('parts/header.ejs', locals) %>
<style>
.pre-log-content {
height: calc(100vh - 270px); /* -251px */
margin-bottom: 0;
font-size: 100%;
/* word-break: break-all; */
}
.nui-height {
height: calc(100vh - 170px);
}
.card {
height: 100%;
margin-bottom: 0 !important;
}
.e-src{
color: var(--dark);
font-weight: 900 !important;
padding: .15rem;
margin-left: .25rem;
margin-right: .25rem;
}
.e-src-player{
color: var(--primary);
font-weight: 900 !important;
cursor: pointer;
}
.e-src-player:hover{
/* color: var(--warning); */
color: var(--light);
background-color: var(--dark);
}
.filter-controls-container {
margin-top: 10px;
}
.filter-show-toggle {
display: none;
}
@media only screen and (max-width: 1200px) {
.card-containers {
margin-bottom: 20px;
}
.filter-show-toggle {
display: flex;
}
.section-title {
display: flex;
flex-basis: 0 ;
justify-content: center;
align-items: center;
}
}
@media (min-width: 1250px) and (max-width: 1450px) {
.override-bootstrap {
flex: 0 0 auto !important;
}
}
</style>
<div class="row">
<div class="col-lg-3 col-md-12 card-containers" style="min-width: 235px;">
<div class="card" >
<div class="card-body text-center">
<div id="cardstatus">
<h4>Mode: <span class="text-success" id="modeLabel">LIVE</span></h4>
<p>
<strong>From:</strong> <span id="histLogStart">--</span> <br>
<strong>To:</strong> <span id="histLogEnd">--</span> <br>
</p>
<button type="button" class="btn btn-outline-dark btn-sm mb-2" id="viewOlderBtn">
&lt; View Older
</button>
<button type="button" class="btn btn-outline-dark btn-sm mb-2" id="viewNewerBtn">
View Newer &gt;
</button>
<br>
<button type="button" id="clearConsole" class="btn btn-outline-dark btn-sm mb-2">
Clear Console
</button>
<!-- TODO: add modal -->
<button type="button" id="showLogsModalBtn" class="btn btn-outline-dark btn-sm mb-2 d-none">
Download Log
</button>
</div>
<hr class="border-primary">
<div class='section-title'>
<button
style='margin-left: 10px;'
class='btn btn-outline-info filter-show-toggle rounded'
data-toggle='collapse'
href="#filter-control-container"
role='button'
aria-expanded='false'
aria-controls='collapseControls'>
Show Log Filters
</button>
</div>
<div class='filter-controls-container' id='filter-control-container'>
<h4>Logger Filters</h4>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Player Joins</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="PlayerJoin">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Player Leaves</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="playerDropped">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Chat Messages</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="ChatMessage">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Player Death</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="DeathNotice">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Menu Actions</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="MenuEvent">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Explosions</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="explosionEvent">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Commands</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="CommandExecuted">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">System Events</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="System">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div>
</div>
<!-- <div class="form-group row mb-1">
<label class="col-sm-8 col-form-label">Other Events</label>
<div class="col-sm-4">
<label class="c-switch c-switch-sm c-switch-label c-switch-pill c-switch-success fix-pill-form">
<input class="c-switch-input e-filter-sw" type="checkbox" checked data-e-type="other">
<span class="c-switch-slider" data-checked="On" data-unchecked="Off"></span>
</label>
</div>
</div> -->
</div>
</div>
</div>
<div class="col-lg-9 col-md-12 card-containers override-bootstrap">
<div class="card">
<div class="card-body p-3" style="position: relative">
<pre id="logContainer" class="thin-scroll pre-log-content <%= isWebInterface ? '' : 'nui-height' %>"></pre>
<div id="autoScrollDiv">
<a id="autoScrollBtn" class="d-none" href="#"><span></span><span></span><span></span></a>
</div>
</div>
</div>
</div>
</div>
<%- await include('parts/footer.ejs', locals) %>
<script>
(function () {
//============================================== Moved from main.js
function showPlayerByMutexNetid(mutexNetid) {
const [mutex, netid] = mutexNetid.split(/[_#]/, 2);
return window.parent.postMessage({ type: 'openPlayerModal', ref: { mutex, netid } });
}
const getSocket = (rooms) => {
const socketOpts = {
transports: ['polling'],
upgrade: false,
query: { rooms }
};
const socket = isWebInterface
? io({ ...socketOpts, path: '/socket.io' })
: io('monitor', { ...socketOpts, path: '/WebPipe/socket.io' });
socket.on('logout', () => {
console.log('Received logout command from websocket.');
window.parent.postMessage({ type: 'logoutNotice' });
});
return socket;
}
//============================================== Preparing variables
const autoScrollBtn = document.getElementById("autoScrollBtn");
const logContainer = document.getElementById("logContainer");
const modeLabel = document.getElementById("modeLabel");
const histLogStart = document.getElementById("histLogStart");
const histLogEnd = document.getElementById("histLogEnd");
const viewOlderBtn = document.getElementById("viewOlderBtn");
const viewNewerBtn = document.getElementById("viewNewerBtn");
const MAX_HISTORY_SIZE = 500;
const frameTimeOptions = {weekday: 'long', hour: '2-digit', minute: '2-digit'};
const eventTimeOptions = {hour: '2-digit', minute: '2-digit', second: '2-digit'};
let logStartTs, logEndTs;
let eventFilters = {};
let autoScroll = true;
const pageSocket = getSocket('serverlog');
const logContainerSpinner = `<div style="height: 90%;" class="d-flex"><div style="margin: auto;">${SPINNER_HTML}</div></div>`;
//============================================== Responsivity stuff
const mql = window.matchMedia('(max-width: 992px)');
const targetEl = document.getElementById('filter-control-container');
if (mql.matches) {
targetEl.classList.add('collapse');
}
mql.addEventListener("change", (e) => {
if (e.matches && !targetEl.classList.contains('collapse')) {
return targetEl.classList.add('collapse');
}
if (targetEl.classList.contains('collapse')) targetEl.classList.remove('collapse');
})
//============================================== Socket Stuff
pageSocket.on('error', (error) => {
console.log('Page Socket.IO', error)
});
pageSocket.on('connect', () => {
console.log("Page Socket.IO Connected.");
logContainer.innerHTML = '';
});
pageSocket.on('disconnect', (message) => {
console.log("Page Socket.IO Disonnected:", message);
});
pageSocket.on('logData', processLog);
//============================================== Mode selection
function goLive() {
logContainer.innerHTML = logContainerSpinner;
modeLabel.classList.remove('text-warning');
modeLabel.classList.add('text-success');
modeLabel.textContent = 'LIVE';
viewOlderBtn.disabled = false;
viewNewerBtn.disabled = true;
if (!pageSocket.connected) pageSocket.connect();
}
function goHistory(direction) {
let ref;
if(direction === 'older'){
ref = logStartTs;
}else if(direction === 'newer'){
ref = logEndTs;
}else{
throw new Error(`unknown direction`);
}
pageSocket.disconnect();
logContainer.innerHTML = logContainerSpinner;
modeLabel.classList.add('text-warning');
modeLabel.classList.remove('text-success');
modeLabel.textContent = 'LOG';
viewOlderBtn.disabled = false;
viewNewerBtn.disabled = false;
//Get log from API
txAdminAPI({
type: 'GET',
url: `/serverLog/partial?dir=${direction}&ref=${ref}`,
timeout: REQ_TIMEOUT_MEDIUM,
success: function (data) {
logContainer.innerHTML = '';
if(!Array.isArray(data.log)){
logContainer.innerHTML = 'Failed to load log. Please refresh the page and try again.';
return;
}
//FIXME: reorganize this mess
if(data.boundry){
if(direction === 'older'){
if(!data.log.length){
logContainer.innerHTML = 'No more log entries to show.';
}else{
processLog(data.log);
}
viewOlderBtn.disabled = true;
}else{
goLive();
}
return;
}
processLog(data.log);
// TODO: da até pra depois do processLog() adicionar um <a> pra ir older ou newer
},
error: function (xmlhttprequest, textstatus, message) {
logContainer.innerHTML = 'Failed to load log. Please refresh the page and try again.';
},
});
}
// TODO: check if search string then do goLive() or goHistory()
goLive();
//============================================== Handling filters
if(typeof window.localStorage.eventFilters !== 'undefined'){
try {
const fromStorage = JSON.parse(window.localStorage.eventFilters);
console.log('Filters from storage:', fromStorage);
if(typeof fromStorage === 'object' && fromStorage !== null) {
eventFilters = fromStorage;
}
} catch (error) {
console.error('Failed to process window.localStorage.eventFilters');
}
}
const allSwitches = document.querySelectorAll('.e-filter-sw');
allSwitches.forEach((sw) => {
sw.addEventListener('change', eventFiltersChanged);
if(typeof eventFilters[sw.dataset.eType] === 'undefined'){
eventFilters[sw.dataset.eType] = true;
}
sw.checked = eventFilters[sw.dataset.eType];
});
window.localStorage.eventFilters = JSON.stringify(eventFilters);
const getShowTypes = () => Object.keys(eventFilters)
.filter(fn => eventFilters[fn])
.flatMap(fn => {
if (fn === 'PlayerJoin') return ['playerJoining', 'playerJoinDenied'];
if(fn === 'System') return ['LoggerStarted', 'DebugMessage'];
return fn;
});
function eventFiltersChanged(caller){
eventFilters[caller.target.dataset.eType] = caller.target.checked;
window.localStorage.eventFilters = JSON.stringify(eventFilters);
const showTypes = getShowTypes();
console.log('showTypes:', showTypes);
logContainer.childNodes.forEach(line => {
if(showTypes.includes(line.dataset.eType)){
line.classList.remove('d-none');
}else{
line.classList.add('d-none');
}
});
}
//============================================== AutoScroll Things
const scrollBottom = () => {
if (autoScroll) logContainer.scrollTop = logContainer.scrollHeight;
}
const autoscrollToggle = (status) => {
autoScroll = status
if(autoScroll){
autoScrollBtn.classList.add('d-none');
}else{
autoScrollBtn.classList.remove('d-none');
}
scrollBottom();
}
logContainer.addEventListener('scroll',function(){
const scrollTop = logContainer.scrollTop;
const scrollHeight = logContainer.scrollHeight;
const offsetHeight = logContainer.offsetHeight;
const contentHeight = scrollHeight - offsetHeight;
if (scrollTop < contentHeight) {
autoscrollToggle(false);
} else if(scrollTop === contentHeight){
autoscrollToggle(true);
}
}
)
autoScrollBtn.addEventListener("click", (event) => {
event.preventDefault();
autoscrollToggle(true);
});
//============================================== Buttons
document.getElementById("viewOlderBtn").addEventListener("click", function () {
goHistory('older');
});
document.getElementById("viewNewerBtn").addEventListener("click", function () {
goHistory('newer');
});
document.getElementById("clearConsole").addEventListener("click", function () {
logContainer.innerHTML = "";
});
document.getElementById("showLogsModalBtn").addEventListener("click", function () {
//TODO: add modal stuff
});
//============================================== Log processor
function processLog(events) {
console.log(`Events: ${events.length}`);
const showTypes = getShowTypes();
//For every new entry
for (let i = 0; i < events.length; i++) {
const event = events[i];
logEndTs = event.ts;
//Line
const lineElement = document.createElement('div');
lineElement.dataset.eTs = event.ts;
lineElement.dataset.eType = event.type;
if(!showTypes.includes(event.type)){
lineElement.classList.add('d-none');
}
//Time
const localeTime = new Date(event.ts).toLocaleTimeString(window.navigator.language, eventTimeOptions);
const timeNode = document.createElement('span');
timeNode.textContent = `[${localeTime}]`;
timeNode.classList.add('text-muted');
lineElement.appendChild(timeNode);
//Source
const sourceNode = document.createElement('strong');
sourceNode.classList.add('e-src');
if(event.src.id){
const [mutex, netid] = event.src.id.split('#', 2);
sourceNode.classList.add('e-src-player');
sourceNode.addEventListener('click', ()=>{ showPlayerByMutexNetid(`${mutex}_${netid}`) });
sourceNode.textContent = `[${netid}] ${event.src.name}`;
}else{
sourceNode.textContent = event.src.name;
}
lineElement.appendChild(sourceNode);
//Message
const msgNode = document.createTextNode(event.msg);
lineElement.appendChild(msgNode);
//Appending & capping log
logContainer.appendChild(lineElement)
if(logContainer.childNodes.length > MAX_HISTORY_SIZE){
logContainer.removeChild(logContainer.childNodes[0])
}
}
//Process times
logStartTs = parseInt(logContainer.childNodes[0].dataset.eTs);
const logStartDate = new Date(logStartTs);
const logEndDate = new Date(logEndTs);
histLogStart.textContent = logStartDate.toLocaleString(window.navigator.language, frameTimeOptions);
histLogEnd.textContent = logEndDate.toLocaleString(window.navigator.language, frameTimeOptions);
//AutoScroll
scrollBottom();
}
})();
</script>