Skip to content

Commit 7ea2cca

Browse files
committed
feat: add logs to up
1 parent 91fc3ce commit 7ea2cca

5 files changed

Lines changed: 216 additions & 50 deletions

File tree

api/settings.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { getDBSize, vacuumDB } = require('@lib/sqlite/index');
1010
const { getSettings, toggleSetting, updateSetting } = require('@lib/sqlite/settings');
1111
const { countUsers } = require('@lib/sqlite/users');
1212
const { getSystemStats } = require('@lib/stats');
13+
const { getMemoryLogs } = require('@lib/logger');
1314
const { writefavicon, writeImage } = require('@lib/imageStore');
1415
const { verifyBufferIsJPG, verifyBufferIsJPGMaxDimensions, convertToWebp } = require('@lib/utils');
1516
const { getBackups, createBackup, restoreBackup } = require('@lib/backup');
@@ -87,6 +88,10 @@ router.get('/stats', verifyRequest('app.admin.stats.read'), limiter(1), async (r
8788
return res.json({ appversion, dbSize, systemStats });
8889
});
8990

91+
router.get('/logs', verifyRequest('app.admin.stats.read'), limiter(5), async (req, res) => {
92+
return res.json({ logs: getMemoryLogs() });
93+
});
94+
9095
router.post('/vacuumdb', verifyRequest('app.admin.db.write'), limiter(1), async (req, res) => {
9196
const dbSize_before = getDBSize();
9297
await vacuumDB();

config/locales/de/AdminSettings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111
"MemHeapExternalBuffer": "Speicher für C++ | Buffers",
1212
"Storage": "Speicher"
1313
},
14+
"Logs": {
15+
"Button": "Logs anzeigen",
16+
"ModalTitle": "Systemlogs",
17+
"Filters": "Level",
18+
"Loading": "Lade Logs...",
19+
"Empty": "Keine Logs fuer die aktuelle Auswahl.",
20+
"Error": "Fehler beim Laden der Logs.",
21+
"Levels": {
22+
"Error": "Error",
23+
"Warning": "Warning",
24+
"Info": "Info",
25+
"Debug": "Debug",
26+
"System": "System"
27+
}
28+
},
1429
"REGCode": {
1530
"Title": "Registrierungs Code",
1631
"Description": "Aktiviere diese Funktion wenn sich nur Benutzer mit diesem Code registrieren sollen.",

config/locals_map.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ module.exports = {
124124
"Error",
125125
"Page",
126126
"AdminSettings"
127+
],
128+
"/settings": [
129+
"Settings",
130+
"Error",
131+
"Page",
132+
"Setup",
133+
"Navbar",
134+
"Categories"
127135
],
128136
"/transaction_history": [
129137
"Items",

lib/logger.js

Lines changed: 43 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
const os = require('os');
2+
const util = require('node:util');
23

34
const loglevel = Number(process.env.LOG_LEVEL) || 3;
45
const Levels = ['error', 'warning', 'info', 'debug', 'system'];
6+
const maxMemoryLogs = 500;
7+
const memoryLogs = [];
8+
let logSequence = 0;
59

610
/**
711
* Returns the current timestamp
@@ -109,84 +113,77 @@ const logFormater = (levelnumber, text) => {
109113
return `[${process.env.APPLICATION || "Application"}] [${getTimestamp()}] ${logColorShort(levelnumber)} ${text}`;
110114
}
111115

112-
/**
113-
* @param {string} level Loglevel of the to log text
114-
* @param {string} text Text to log
115-
*/
116-
const logger = function (level, text) {
117-
const levelnumber = Levels.indexOf(level.toLowerCase()) + 1
116+
const rememberLog = (levelnumber, text) => {
117+
if (levelnumber < 1) return;
118118

119-
if (levelnumber <= loglevel && levelnumber === 1) {
120-
if (process.env.LOG_TYPE === 'console') {
121-
console.log(logFormater(levelnumber, text))
122-
} else {
123-
process.stdout.write(logFormater(levelnumber, text) + '\n');
124-
}
125-
}
119+
const level = Levels[levelnumber - 1] || 'unknown';
120+
memoryLogs.push({
121+
id: ++logSequence,
122+
timestamp: getTimestamp(),
123+
level,
124+
text: String(text),
125+
});
126126

127-
if (levelnumber <= loglevel && levelnumber === 2) {
128-
if (process.env.LOG_TYPE === 'console') {
129-
console.log(logFormater(levelnumber, text))
130-
} else {
131-
process.stdout.write(logFormater(levelnumber, text) + '\n');
132-
}
127+
if (memoryLogs.length > maxMemoryLogs) {
128+
memoryLogs.splice(0, memoryLogs.length - maxMemoryLogs);
133129
}
130+
}
134131

135-
if (levelnumber <= loglevel && levelnumber === 3) {
136-
if (process.env.LOG_TYPE === 'console') {
137-
console.log(logFormater(levelnumber, text))
138-
} else {
139-
process.stdout.write(logFormater(levelnumber, text) + '\n');
140-
}
141-
}
132+
const getMemoryLogs = () => memoryLogs.slice();
142133

143-
if (levelnumber <= loglevel && levelnumber === 4) {
144-
if (process.env.LOG_TYPE === 'console') {
145-
console.log(logFormater(levelnumber, text))
146-
} else {
147-
process.stdout.write(logFormater(levelnumber, text) + '\n');
148-
}
134+
const writeLog = (levelnumber, text) => {
135+
if (process.env.LOG_TYPE === 'console') {
136+
console.log(logFormater(levelnumber, text))
137+
} else {
138+
process.stdout.write(logFormater(levelnumber, text) + '\n');
149139
}
140+
}
150141

151-
if (levelnumber === 5) {
152-
if (process.env.LOG_TYPE === 'console') {
153-
console.log(logFormater(levelnumber, text))
154-
} else {
155-
process.stdout.write(logFormater(levelnumber, text) + '\n');
156-
}
142+
/**
143+
* @param {string} level Loglevel of the to log text
144+
* @param {...unknown} parts Text to log
145+
*/
146+
const logger = function (level, ...parts) {
147+
const levelnumber = Levels.indexOf(level.toLowerCase()) + 1
148+
const text = util.format(...parts);
149+
150+
rememberLog(levelnumber, text);
151+
152+
if ((levelnumber <= loglevel && levelnumber >= 1 && levelnumber <= 4) || levelnumber === 5) {
153+
writeLog(levelnumber, text);
157154
}
158155
}
159156

160157
/**
161158
* logs an error to the console
162159
* @param {String} text Text to log
163160
*/
164-
const logError = (text) => logger('error', text);
161+
const logError = (...parts) => logger('error', ...parts);
165162

166163
/**
167164
* logs a warning to the console
168165
* @param {String} text Text to log
169166
*/
170-
const logWarning = (text) => logger('warning', text);
167+
const logWarning = (...parts) => logger('warning', ...parts);
171168

172169
/**
173170
* logs a info to the console
174171
* @param {String} text Text to log
175172
*/
176-
const logInfo = (text) => logger('info', text);
173+
const logInfo = (...parts) => logger('info', ...parts);
177174

178175
/**
179176
* logs a debug message to the console
180177
* @param {String} text
181178
* @returns
182179
*/
183-
const logDebug = (text) => logger('debug', text);
180+
const logDebug = (...parts) => logger('debug', ...parts);
184181

185182
/**
186183
* logs an system message to the console
187184
* @param {String} text Text to log
188185
*/
189-
const logSystem = (text) => logger('system', text);
186+
const logSystem = (...parts) => logger('system', ...parts);
190187

191188
const log = {
192189
error: logError,
@@ -203,5 +200,6 @@ logger('system', `Logger initialized at level ${Levels[loglevel - 1]}`);
203200

204201
module.exports = {
205202
logger,
206-
log
207-
};
203+
log,
204+
getMemoryLogs
205+
};

views/admin/settings.ejs

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@
3131
#backup-list-modal-overlay.active #backup-list-modal-content {
3232
transform: scale(1);
3333
}
34+
35+
#system-logs-modal-overlay.active {
36+
visibility: visible;
37+
opacity: 1;
38+
}
39+
40+
#system-logs-modal-overlay.active #system-logs-modal-content {
41+
transform: scale(1);
42+
}
3443
</style>
3544
</head>
3645

@@ -48,11 +57,19 @@
4857
</header>
4958

5059
<div class="bg-white rounded-xl shadow-lg p-6 mb-6">
51-
<div class="flex items-center mb-4">
52-
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-500 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
53-
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
54-
</svg>
55-
<h2 class="font-bold text-xl text-gray-800" id="StatsTitle" data-translate="AdminSettings.Stats.Title"></h2>
60+
<div class="flex flex-col gap-3 mb-4 sm:flex-row sm:items-center sm:justify-between">
61+
<div class="flex items-center">
62+
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-500 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
63+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
64+
</svg>
65+
<h2 class="font-bold text-xl text-gray-800" id="StatsTitle" data-translate="AdminSettings.Stats.Title"></h2>
66+
</div>
67+
<button id="show-system-logs-btn" onclick="showSystemLogsModal()" class="inline-flex items-center justify-center gap-2 bg-gray-700 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-800 transition">
68+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
69+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6M9 8h6m2 13H7a2 2 0 01-2-2V5a2 2 0 012-2h7l5 5v11a2 2 0 01-2 2z" />
70+
</svg>
71+
<span data-translate="AdminSettings.Logs.Button"></span>
72+
</button>
5673
</div>
5774

5875
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4 text-sm">
@@ -257,6 +274,41 @@
257274
</div>
258275
</div>
259276

277+
<div id="system-logs-modal-overlay" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center p-4 z-50 invisible opacity-0 transition-opacity duration-300">
278+
<div id="system-logs-modal-content" class="bg-white rounded-xl shadow-2xl p-6 max-w-5xl w-full relative transform scale-95 transition-transform duration-300">
279+
<button onclick="closeSystemLogsModal()" class="absolute top-3 right-4 text-gray-500 hover:text-gray-800 text-3xl font-bold">&times;</button>
280+
<h2 class="text-2xl font-bold text-gray-800 mb-4 pr-8" data-translate="AdminSettings.Logs.ModalTitle"></h2>
281+
282+
<div class="flex flex-wrap items-center gap-3 mb-4" id="system-log-filter-container">
283+
<span class="text-sm font-semibold text-gray-700" data-translate="AdminSettings.Logs.Filters"></span>
284+
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
285+
<input type="checkbox" class="system-log-filter rounded border-gray-300" value="error" checked />
286+
<span data-translate="AdminSettings.Logs.Levels.Error"></span>
287+
</label>
288+
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
289+
<input type="checkbox" class="system-log-filter rounded border-gray-300" value="warning" checked />
290+
<span data-translate="AdminSettings.Logs.Levels.Warning"></span>
291+
</label>
292+
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
293+
<input type="checkbox" class="system-log-filter rounded border-gray-300" value="info" checked />
294+
<span data-translate="AdminSettings.Logs.Levels.Info"></span>
295+
</label>
296+
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
297+
<input type="checkbox" class="system-log-filter rounded border-gray-300" value="debug" checked />
298+
<span data-translate="AdminSettings.Logs.Levels.Debug"></span>
299+
</label>
300+
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
301+
<input type="checkbox" class="system-log-filter rounded border-gray-300" value="system" checked />
302+
<span data-translate="AdminSettings.Logs.Levels.System"></span>
303+
</label>
304+
</div>
305+
306+
<div id="system-logs-container" class="max-h-[65vh] overflow-y-auto rounded-lg bg-gray-950 p-3 font-mono text-xs text-gray-100">
307+
<p class="text-gray-400">...</p>
308+
</div>
309+
</div>
310+
</div>
311+
260312
<div id="favicon-cropper-modal" class="cropper-modal-container">
261313
<div class="cropper-modal-content">
262314
<h2 class="text-xl font-bold mb-4" data-translate="AdminSettings.Manifest.AdjustFavicon"></h2>
@@ -295,6 +347,16 @@
295347
let croppedFaviconBlob = null;
296348
let lowFundsCropper;
297349
let croppedLowFundsBlob = null;
350+
let systemLogs = [];
351+
352+
const logLevelClasses = {
353+
error: "text-red-300",
354+
warning: "text-yellow-300",
355+
info: "text-green-300",
356+
debug: "text-fuchsia-300",
357+
system: "text-cyan-300",
358+
};
359+
298360
function formatBytes(bytes, decimals = 2) {
299361
if (!+bytes) return "0 Bytes";
300362
const k = 1024;
@@ -321,6 +383,81 @@
321383
return result || "...";
322384
}
323385
386+
function getSelectedLogLevels() {
387+
return new Set(
388+
Array.from(document.querySelectorAll(".system-log-filter:checked"))
389+
.map((filter) => filter.value)
390+
);
391+
}
392+
393+
function renderSystemLogs() {
394+
const listContainer = document.getElementById("system-logs-container");
395+
const selectedLevels = getSelectedLogLevels();
396+
const visibleLogs = systemLogs.filter((log) => selectedLevels.has((log.level || "").toLowerCase()));
397+
398+
listContainer.innerHTML = "";
399+
400+
if (!visibleLogs.length) {
401+
const emptyMessage = document.createElement("p");
402+
emptyMessage.className = "text-gray-400";
403+
emptyMessage.textContent = t("AdminSettings.Logs.Empty");
404+
listContainer.appendChild(emptyMessage);
405+
return;
406+
}
407+
408+
visibleLogs.forEach((log) => {
409+
const level = (log.level || "info").toLowerCase();
410+
const entry = document.createElement("div");
411+
entry.className = "grid gap-2 border-b border-gray-800 py-1 last:border-b-0 md:grid-cols-[9rem_5rem_1fr]";
412+
413+
const timestamp = document.createElement("span");
414+
timestamp.className = "text-gray-400";
415+
timestamp.textContent = log.timestamp || "";
416+
417+
const levelElement = document.createElement("span");
418+
levelElement.className = `font-bold uppercase ${logLevelClasses[level] || "text-gray-300"}`;
419+
levelElement.textContent = level;
420+
421+
const text = document.createElement("span");
422+
text.className = "whitespace-pre-wrap break-words text-gray-100";
423+
text.textContent = log.text || "";
424+
425+
entry.appendChild(timestamp);
426+
entry.appendChild(levelElement);
427+
entry.appendChild(text);
428+
listContainer.appendChild(entry);
429+
});
430+
431+
listContainer.scrollTop = listContainer.scrollHeight;
432+
}
433+
434+
async function loadSystemLogs() {
435+
const listContainer = document.getElementById("system-logs-container");
436+
listContainer.innerHTML = `<p class="text-gray-400">${t("AdminSettings.Logs.Loading")}</p>`;
437+
438+
try {
439+
const response = await apiFetch("/api/v1/settings/logs");
440+
if (!response.ok) throw new Error("Network response was not ok");
441+
const data = await response.json();
442+
systemLogs = data.logs || [];
443+
renderSystemLogs();
444+
} catch (error) {
445+
console.error("Error fetching system logs:", error);
446+
listContainer.innerHTML = `<p class="text-red-300">${t("AdminSettings.Logs.Error")}</p>`;
447+
}
448+
}
449+
450+
function showSystemLogsModal() {
451+
const modalOverlay = document.getElementById("system-logs-modal-overlay");
452+
modalOverlay.classList.add("active");
453+
loadSystemLogs();
454+
}
455+
456+
function closeSystemLogsModal() {
457+
const modalOverlay = document.getElementById("system-logs-modal-overlay");
458+
modalOverlay.classList.remove("active");
459+
}
460+
324461
async function toggleSetting(settingKey) {
325462
try {
326463
const response = await apiFetch("/api/v1/settings/toggle", {
@@ -870,6 +1007,9 @@
8701007
document.getElementById("manifest-settings-form").addEventListener("submit", handleManifestSubmit);
8711008
document.getElementById("low-funds-image").addEventListener("change", handleLowFundsImageSelection);
8721009
document.getElementById("low-funds-settings-form").addEventListener("submit", handleLowFundsSubmit);
1010+
document.querySelectorAll(".system-log-filter").forEach((filter) => {
1011+
filter.addEventListener("change", renderSystemLogs);
1012+
});
8731013
document.getElementById("crop-favicon-button").addEventListener("click", () => {
8741014
if (!faviconCropper) return;
8751015

0 commit comments

Comments
 (0)