Skip to content

Commit 37cfe83

Browse files
committed
feat: add admin notification and reworked error handling
added LowStock warning and ErrorReport
1 parent 9826c43 commit 37cfe83

16 files changed

Lines changed: 513 additions & 35 deletions

File tree

api/settings.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ const uploadHandler = multer({
3535
});
3636

3737
const getSettingsToggleSchema = () => {
38-
const settingKeys = ['USER_SHOPPINGLIST_ACTIVE', 'DB_AUTOVACUUM', 'LOW_FUNDS_WARNING'];
38+
const settingKeys = [
39+
'USER_SHOPPINGLIST_ACTIVE',
40+
'DB_AUTOVACUUM',
41+
'LOW_FUNDS_WARNING',
42+
'ERROR_REPORTS_ACTIVE',
43+
'LOW_STOCK_WARNING',
44+
];
3945
if (!isEBGOAuthEnabled()) settingKeys.unshift('REG_CODE_ACTIVE');
4046

4147
return Joi.object({
@@ -88,6 +94,10 @@ router.get('/stats', verifyRequest('app.admin.stats.read'), limiter(1), async (r
8894
return res.json({ appversion, dbSize, systemStats });
8995
});
9096

97+
const settingsLowStockSchema = Joi.object({
98+
LOW_STOCK_PERCENT: Joi.number().min(0).max(100).required(),
99+
});
100+
91101
router.get('/logs', verifyRequest('app.admin.stats.read'), limiter(5), async (req, res) => {
92102
return res.json({ logs: getMemoryLogs() });
93103
});
@@ -157,6 +167,12 @@ router.put('/lowFunds', verifyRequest('app.admin.settings.write'), parseMultipar
157167
res.status(200).json({ success: true });
158168
});
159169

170+
router.put('/lowStock', verifyRequest('app.admin.settings.write'), limiter(10), async (req, res) => {
171+
const body = await settingsLowStockSchema.validateAsync(req.body);
172+
await updateSetting('LOW_STOCK_PERCENT', body.LOW_STOCK_PERCENT.toString());
173+
res.status(200).json({ success: true });
174+
});
175+
160176
router.get('/backup', verifyRequest('app.admin.backup.read'), limiter(5), async (req, res) => {
161177
const backups = getBackups();
162178
return res.json(backups);

api/store.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { getUserBalance, getUserFavorites, purchaseItem } = require('@lib/sqlite/
55
const { getItemsByCategory, toggleUserFavorite } = require('@lib/sqlite/items');
66
const { getSetting } = require('@lib/sqlite/settings');
77
const { getCategoryPurchaseLeaderboard } = require('@lib/sqlite/stats');
8+
const { notifyLowStockIfNeeded } = require('@lib/notifications');
89
const gategories_conf = require('@config/categories');
910
const Joi = require('@lib/sanitizer');
1011
const express = require('ultimate-express');
@@ -83,7 +84,10 @@ router.post('/item/:uuid/favorite', verifyRequest('web.user.favorite.write'), li
8384
router.post('/buy', verifyRequest('web.user.store.write'), limiter(4), async (req, res) => {
8485
const { uuid, quantity } = await buySchema.validateAsync(req.body);
8586

86-
await purchaseItem(req.user.user_data.uuid, uuid, quantity, req.user.user_data.id);
87+
const result = await purchaseItem(req.user.user_data.uuid, uuid, quantity, req.user.user_data.id);
88+
void notifyLowStockIfNeeded(result.previousStock, result.item)
89+
.catch((error) => process.log?.error?.(`Low stock notification failed: ${error?.message || error}`));
90+
8791
return res.json({ success: true });
8892
});
8993

config/errors.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ module.exports = {
66
"log_errors": {
77
"CustomError": true,
88
"RenderError": true,
9-
"TooManyRequests": false,
10-
"InvalidToken": false,
11-
"Invalid2FA": false,
12-
"InvalidLogin": true,
139
"InvalidRouteInput": true,
10+
"InvalidRouteJson": true,
11+
"InvalidLogin": true,
12+
"RequestBlocked": false,
13+
"BlockedRequest": false,
14+
"Invalid2FA": false,
15+
"OAuthError": true,
16+
"DBError": true,
17+
"SQLError": true,
18+
"SQLDuplicateError": true,
19+
"InvalidToken": false,
20+
"TooManyRequests": false,
21+
"PurchaseError": false,
1422
"PermissionsError": true,
1523
"FilesystemError": true,
16-
"OAuthError": true,
1724
"SqliteError": true,
18-
"DBError": true,
1925
"ValidationError": true,
2026
"TypeError": true,
2127
}

config/locales/de/AdminSettings.json

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"DefaultError": "Bitte generiere einen neuen Code.",
3838
"ModalTitle": "Registrierungs QR-Code",
3939
"ModalDescription": "Gib diesen Code allen Nutzern, scanne ihn mit der Kamera um zur Registrierung zu gelangen."
40-
},
40+
},
4141
"UserShoppinglist": {
4242
"Title": "Einkaufslisten für Benutzer",
4343
"Description": "Aktiviere diese Funktion um Benutzern zu erlauben, die Einkaufsliste nutzen zu können.",
@@ -65,6 +65,23 @@
6565
"DeleteError": "Fehler beim Löschen des Backups.",
6666
"DownloadError": "Fehler beim Herunterladen des Backups."
6767
},
68+
"Notification": {
69+
"Title": "Benachrichtigungen",
70+
"Description": "Aktiviere diese Funktion um Benachrichtigungen zu erhalten, wenn bestimmte Ereignisse auftreten.",
71+
"ErrorReport": {
72+
"ToggleLabel": "Bei Fehlern"
73+
},
74+
"LowStock": {
75+
"ToggleLabel": "Bei geringem Warenbestand",
76+
"Percent": "Trigger in Prozent vom Zielbestand"
77+
}
78+
},
79+
"Button": {
80+
"Save": "Speichern",
81+
"Saving": "Speichern...",
82+
"Success": "Gespeichert",
83+
"Error": "Fehler beim Speichern"
84+
},
6885
"Manifest": {
6986
"Title": "Manifest",
7087
"Description": "Passe die App-Informationen und das Favicon an.",
@@ -76,11 +93,7 @@
7693
"Favicon": "Favicon",
7794
"AdjustFavicon": "Favicon anpassen",
7895
"Cancel": "Abbrechen",
79-
"CropSave": "Zuschneiden und speichern",
80-
"Button": "Speichern",
81-
"Saving": "Speichern...",
82-
"Success": "Gespeichert",
83-
"Error": "Fehler beim Speichern"
96+
"CropSave": "Zuschneiden und speichern"
8497
},
8598
"LowFunds": {
8699
"Title": "Geringer Kontostand",
@@ -92,11 +105,7 @@
92105
"Image": "Bild",
93106
"AdjustImage": "Bild anpassen",
94107
"Cancel": "Abbrechen",
95-
"CropSave": "Zuschneiden und speichern",
96-
"Button": "Speichern",
97-
"Saving": "Speichern...",
98-
"Success": "Gespeichert",
99-
"Error": "Fehler beim Speichern"
108+
"CropSave": "Zuschneiden und speichern"
100109
},
101110
"Restore": {
102111
"Title": "Daten wiederherstellen",

config/notifications/core.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,60 @@ module.exports = [
5757
},
5858
},
5959
},
60+
{
61+
type: 'ErrorReport',
62+
constant: 'ERROR_REPORT',
63+
category: 'system',
64+
requiresMessage: true,
65+
channels: {
66+
email: {
67+
templatePath: path.join(emailTemplateDir, 'ErrorReport.ejs'),
68+
buildContext: (task) => ({
69+
errors: JSON.parse(task.custom_message).errors || [],
70+
}),
71+
buildText: (context) => [
72+
context.t('emails.greeting', { name: context.name }),
73+
'',
74+
context.t('emails.ErrorReport.body', { count: context.errors.length }),
75+
...context.errors.map((entry) => `[${entry.timestamp}] ${entry.message}`),
76+
'',
77+
context.domain,
78+
].join('\n'),
79+
},
80+
},
81+
},
82+
{
83+
type: 'LowStock',
84+
constant: 'LOW_STOCK',
85+
category: 'system',
86+
requiresMessage: true,
87+
channels: {
88+
email: {
89+
templatePath: path.join(emailTemplateDir, 'LowStock.ejs'),
90+
buildContext: (task) => {
91+
const context = JSON.parse(task.custom_message);
92+
const estimatedDays = context.trend?.estimatedDaysRemaining;
93+
return {
94+
...context,
95+
itemName: context.item?.name || '',
96+
estimatedDuration: estimatedDays ? `ca. ${estimatedDays} Tage` : 'Trend unbekannt',
97+
};
98+
},
99+
buildText: (context) => [
100+
context.t('emails.greeting', { name: context.name }),
101+
'',
102+
context.t('emails.LowStock.body'),
103+
`${context.item.name}: ${context.item.stock} / ${context.item.target_stock}`,
104+
context.trend?.averagePerDay > 0
105+
? context.t('emails.LowStock.estimateText', {
106+
averagePerDay: context.trend.averagePerDay,
107+
days: context.trend.estimatedDaysRemaining,
108+
})
109+
: context.t('emails.LowStock.noTrendText'),
110+
'',
111+
context.domain,
112+
].join('\n'),
113+
},
114+
},
115+
},
60116
];
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!doctype html>
2+
<html lang="<%= language %>">
3+
<head>
4+
<meta charset="utf-8">
5+
<title><%= t('emails.ErrorReport.subject', { application }) %></title>
6+
</head>
7+
<body>
8+
<p><%= t('emails.greeting', { name }) %></p>
9+
<p><%= t('emails.ErrorReport.body', { count: errors.length }) %></p>
10+
<ul>
11+
<% errors.forEach((entry) => { %>
12+
<li>
13+
<strong><%= entry.timestamp %></strong>:
14+
<pre style="white-space: pre-wrap;"><%= entry.message %></pre>
15+
</li>
16+
<% }) %>
17+
</ul>
18+
</body>
19+
</html>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!doctype html>
2+
<html lang="<%= language %>">
3+
<head>
4+
<meta charset="utf-8">
5+
<title><%= t('emails.LowStock.subject', { application, itemName }) %></title>
6+
</head>
7+
<body>
8+
<p><%= t('emails.greeting', { name }) %></p>
9+
<p><%= t('emails.LowStock.body') %></p>
10+
<ul>
11+
<li><strong><%= t('emails.LowStock.item') %>:</strong> <%= item.name %></li>
12+
<li><strong><%= t('emails.LowStock.stock') %>:</strong> <%= item.stock %> / <%= item.target_stock %></li>
13+
</ul>
14+
<% if (trend && trend.averagePerDay > 0) { %>
15+
<p><%= t('emails.LowStock.estimate', {
16+
averagePerDay: trend.averagePerDay,
17+
days: trend.estimatedDaysRemaining
18+
}) %></p>
19+
<% } else { %>
20+
<p><%= t('emails.LowStock.noTrend') %></p>
21+
<% } %>
22+
</body>
23+
</html>

config/templates/email/locales/de.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@
2323
"until": "bis {{date}}",
2424
"text": "Hallo {{name}},\n\nBei {{application}} gibt es aktuell neue Angebote. Öffne die Anwendung, um sie anzusehen."
2525
},
26+
"ErrorReport": {
27+
"subject": "Error Report von {{application}}",
28+
"heading": "Error Report",
29+
"body": "Heute wurden {{count}} Fehler geloggt."
30+
},
31+
"LowStock": {
32+
"subject": "Geringer Warenbestand: {{itemName}} ({{estimatedDuration}})",
33+
"heading": "Geringer Warenbestand",
34+
"body": "Ein Artikel ist unter den konfigurierten Warenbestand gefallen.",
35+
"item": "Artikel",
36+
"stock": "Bestand",
37+
"estimate": "Trend: Ø {{averagePerDay}} Stück pro Tag. Geschätzt reicht der Bestand noch {{days}} Tage.",
38+
"estimateText": "Trend: Ø {{averagePerDay}} Stück pro Tag. Geschätzt noch {{days}} Tage.",
39+
"noTrend": "Noch kein Verkaufs-Trend vorhanden.",
40+
"noTrendText": "Noch kein Verkaufs-Trend vorhanden."
41+
},
2642
"Custom": {
2743
"subject": "Benachrichtigung von {{application}}",
2844
"heading": "Benachrichtigung von {{application}}",

lib/logger.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ const rememberLog = (levelnumber, text, timestamp = getTimestamp()) => {
127127
if (memoryLogs.length > maxMemoryLogs) {
128128
memoryLogs.splice(0, memoryLogs.length - maxMemoryLogs);
129129
}
130+
131+
if (levelnumber === 1) {
132+
try {
133+
require('@lib/sqlite/adminNotifications').recordErrorReport(text);
134+
} catch {
135+
// DB may not exist yet during early startup/migration.
136+
}
137+
}
130138
}
131139

132140
const getMemoryLogs = () => memoryLogs.slice();

0 commit comments

Comments
 (0)