chore: improve intake reminder observability

This commit is contained in:
Daniel Volz
2026-03-16 21:28:53 +01:00
committed by GitHub
parent 5b019f942d
commit 5138d784cd
8 changed files with 343 additions and 259 deletions
+3 -3
View File
@@ -247,7 +247,7 @@ export async function authRoutes(app: FastifyInstance) {
})
.returning();
app.log.info(`User registered: ${username}`);
app.log.info(`[Auth] Account registered: username=${newUser.username}, userId=${newUser.id}`);
return reply.status(201).send({
ok: true,
@@ -376,7 +376,7 @@ export async function authRoutes(app: FastifyInstance) {
{ expiresIn: `${refreshTtlDays}d`, key: app.config.refreshSecret }
);
app.log.info(`User logged in: ${username} (rememberMe: ${rememberMe})`);
app.log.info(`[Auth] Login succeeded: username=${user.username}, userId=${user.id}, rememberMe=${rememberMe}`);
// Cookie options: with maxAge for "remember me", without for session cookie
const accessCookieOptions = rememberMe
@@ -807,7 +807,7 @@ export async function authRoutes(app: FastifyInstance) {
// Delete user - cascade delete handles all related data
await db.delete(users).where(eq(users.id, authUser.id));
app.log.info(`User deleted account: ${authUser.username} (ID: ${authUser.id})`);
app.log.info(`[Auth] Account deleted: username=${authUser.username}, userId=${authUser.id}`);
// Clear auth cookies
return reply
+1 -1
View File
@@ -238,7 +238,7 @@ export async function oidcRoutes(app: FastifyInstance) {
// Set cookies (use app's centralized cookie options)
request.log.debug(
`[OIDC] Setting cookies for user ${user.username}, NODE_ENV=${env.NODE_ENV}, secure=${app.config.cookieOptions.secure}`
`[OIDC] Setting auth cookies for username=${user.username}, userId=${user.id}, NODE_ENV=${env.NODE_ENV}, secure=${app.config.cookieOptions.secure}`
);
setAuthCookies(app, reply, accessToken, refreshToken);
+49 -28
View File
@@ -40,13 +40,6 @@ function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, (char) => htmlEscapes[char] || char);
}
function maskEmail(email: string): string {
const [localPart, domain] = email.split("@");
if (!domain) return "invalid-email";
if (localPart.length <= 2) return `${localPart[0] ?? "*"}*@${domain}`;
return `${localPart.slice(0, 2)}***@${domain}`;
}
type MailDeliveryInfo = {
accepted?: unknown;
rejected?: unknown;
@@ -256,10 +249,7 @@ export async function plannerRoutes(app: FastifyInstance) {
},
async (request, reply) => {
const { email, from, until, rows, language: bodyLanguage } = request.body;
request.log.info(
{ hasEmail: Boolean(email), rowCount: rows?.length ?? 0 },
"[Planner] Demand notification request received"
);
request.log.info({ email, rowCount: rows?.length ?? 0 }, "[Planner] Demand notification request received");
if (!rows || rows.length === 0) {
return reply.status(400).send({ error: "Missing planner data" });
@@ -277,6 +267,7 @@ export async function plannerRoutes(app: FastifyInstance) {
request.log.warn("[Planner] Demand notification skipped: no active medications in request");
return reply.status(400).send({ error: "No active medications to notify" });
}
const activeMedicationNames = activeRows.map((row) => row.medicationName);
const userSettings = await loadUserSettings(userId);
const notificationSettings = {
@@ -291,6 +282,8 @@ export async function plannerRoutes(app: FastifyInstance) {
pushEnabled: notificationSettings.shoutrrrEnabled,
hasPushUrl: Boolean(notificationSettings.shoutrrrUrl),
activeRowCount: activeRows.length,
recipientEmail: email,
medications: activeMedicationNames,
},
"[Planner] Demand notification channel state"
);
@@ -377,13 +370,14 @@ ${getFooterPlain(language)}`;
request.log.info(
{
userId,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
hasSmtpPass: Boolean(smtpPass),
smtpPort,
smtpSecure,
hasSmtpFrom: Boolean(smtpFrom),
to: maskEmail(email),
recipientEmail: email,
},
"[Planner] Demand email path selected"
);
@@ -494,7 +488,7 @@ ${getFooterPlain(language)}`;
},
});
request.log.info({ to: maskEmail(email) }, "[Planner] Sending demand email");
request.log.info({ userId, recipientEmail: email }, "[Planner] Sending demand email");
const mailResult = await transporter.sendMail({
from: smtpFrom,
@@ -509,19 +503,23 @@ ${getFooterPlain(language)}`;
throw new Error(deliveryError);
}
request.log.info({ to: maskEmail(email), messageId: mailResult.messageId }, "[Planner] Demand email sent");
request.log.info(
{ userId, recipientEmail: email, messageId: mailResult.messageId },
"[Planner] Demand email sent"
);
results.email = true;
} catch (error) {
request.log.error({ error, to: maskEmail(email) }, "[Planner] Demand email failed");
request.log.error({ userId, recipientEmail: email, error }, "[Planner] Demand email failed");
const errorMessage = error instanceof Error ? error.message : "Unknown error";
results.errors.push(`Email: ${errorMessage}`);
}
} else {
request.log.warn(
{
userId,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
to: maskEmail(email),
recipientEmail: email,
},
"[Planner] Demand email skipped: SMTP not configured"
);
@@ -612,7 +610,7 @@ ${getFooterPlain(language)}`;
async (request, reply) => {
const { email, lowStock } = request.body;
request.log.info(
{ hasEmail: Boolean(email), lowStockCount: lowStock?.length ?? 0 },
{ email, lowStockCount: lowStock?.length ?? 0 },
"[ReminderManual] Stock reminder request received"
);
@@ -641,6 +639,7 @@ ${getFooterPlain(language)}`;
request.log.warn("[ReminderManual] Stock reminder skipped: no active medications after filtering");
return reply.status(400).send({ error: "No active medications to notify" });
}
const filteredMedicationNames = filteredLowStock.map((item) => item.name);
const userSettings = await loadUserSettings(userId);
const notificationSettings = {
@@ -655,6 +654,8 @@ ${getFooterPlain(language)}`;
pushEnabled: notificationSettings.shoutrrrEnabled,
hasPushUrl: Boolean(notificationSettings.shoutrrrUrl),
filteredLowStockCount: filteredLowStock.length,
recipientEmail: email,
medications: filteredMedicationNames,
},
"[ReminderManual] Stock reminder channel state"
);
@@ -731,13 +732,14 @@ ${getFooterPlain(language)}`;
request.log.info(
{
userId,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
hasSmtpPass: Boolean(smtpPass),
smtpPort,
smtpSecure,
hasSmtpFrom: Boolean(smtpFrom),
to: maskEmail(email),
recipientEmail: email,
},
"[ReminderManual] Stock email path selected"
);
@@ -855,7 +857,7 @@ ${getFooterPlain(language)}`;
},
});
request.log.info({ to: maskEmail(email) }, "[ReminderManual] Sending stock reminder email");
request.log.info({ userId, recipientEmail: email }, "[ReminderManual] Sending stock reminder email");
const mailResult = await transporter.sendMail({
from: smtpFrom,
@@ -871,21 +873,22 @@ ${getFooterPlain(language)}`;
}
request.log.info(
{ to: maskEmail(email), messageId: mailResult.messageId },
{ userId, recipientEmail: email, messageId: mailResult.messageId },
"[ReminderManual] Stock reminder email sent"
);
results.email = true;
} catch (error) {
request.log.error({ error, to: maskEmail(email) }, "[ReminderManual] Stock reminder email failed");
request.log.error({ userId, recipientEmail: email, error }, "[ReminderManual] Stock reminder email failed");
const errorMessage = error instanceof Error ? error.message : "Unknown error";
results.errors.push(`Email: ${errorMessage}`);
}
} else {
request.log.warn(
{
userId,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
to: maskEmail(email),
recipientEmail: email,
},
"[ReminderManual] Stock reminder email skipped: SMTP not configured"
);
@@ -983,7 +986,7 @@ ${getFooterPlain(language)}`;
async (request, reply) => {
const { email, prescriptionLow } = request.body;
request.log.info(
{ hasEmail: Boolean(email), prescriptionCount: prescriptionLow?.length ?? 0 },
{ email, prescriptionCount: prescriptionLow?.length ?? 0 },
"[ReminderManual] Prescription reminder request received"
);
@@ -1002,10 +1005,23 @@ ${getFooterPlain(language)}`;
request.log.warn("[ReminderManual] Prescription reminder skipped: no active medications after filtering");
return reply.status(400).send({ error: "No active medications to notify" });
}
const filteredMedicationNames = filteredPrescriptionLow.map((item) => item.name);
const userSettings = await loadUserSettings(userId);
const language = (userSettings.language as Language) || "en";
const tr = getTranslations(language);
request.log.info(
{
userId,
emailEnabled: userSettings.emailEnabled,
pushEnabled: userSettings.shoutrrrEnabled,
hasPushUrl: Boolean(userSettings.shoutrrrUrl),
prescriptionCount: filteredPrescriptionLow.length,
recipientEmail: email,
medications: filteredMedicationNames,
},
"[ReminderManual] Prescription reminder channel state"
);
const emptyRx = filteredPrescriptionLow.filter((item) => item.remainingRefills <= 0);
const lowRx = filteredPrescriptionLow.filter((item) => item.remainingRefills > 0);
@@ -1039,13 +1055,14 @@ ${getFooterPlain(language)}`;
request.log.info(
{
userId,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
hasSmtpPass: Boolean(smtpPass),
smtpPort,
smtpSecure,
hasSmtpFrom: Boolean(smtpFrom),
to: maskEmail(email),
recipientEmail: email,
},
"[ReminderManual] Prescription email path selected"
);
@@ -1133,7 +1150,7 @@ ${getFooterPlain(language)}`;
</div>
`;
request.log.info({ to: maskEmail(email) }, "[ReminderManual] Sending prescription reminder email");
request.log.info({ userId, recipientEmail: email }, "[ReminderManual] Sending prescription reminder email");
const mailResult = await transporter.sendMail({
from: smtpFrom,
@@ -1149,21 +1166,25 @@ ${getFooterPlain(language)}`;
}
request.log.info(
{ to: maskEmail(email), messageId: mailResult.messageId },
{ userId, recipientEmail: email, messageId: mailResult.messageId },
"[ReminderManual] Prescription reminder email sent"
);
results.email = true;
} catch (error) {
request.log.error({ error, to: maskEmail(email) }, "[ReminderManual] Prescription reminder email failed");
request.log.error(
{ userId, recipientEmail: email, error },
"[ReminderManual] Prescription reminder email failed"
);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
results.errors.push(`Email: ${errorMessage}`);
}
} else {
request.log.warn(
{
userId,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
to: maskEmail(email),
recipientEmail: email,
},
"[ReminderManual] Prescription reminder email skipped: SMTP not configured"
);
+5 -12
View File
@@ -97,13 +97,6 @@ const settingsErrorSchema = {
},
};
function maskEmail(email: string): string {
const [localPart, domain] = email.split("@");
if (!domain) return "invalid-email";
if (localPart.length <= 2) return `${localPart[0] ?? "*"}*@${domain}`;
return `${localPart.slice(0, 2)}***@${domain}`;
}
type MailDeliveryInfo = {
accepted?: unknown;
rejected?: unknown;
@@ -668,7 +661,7 @@ export async function settingsRoutes(app: FastifyInstance) {
request.log.info(
{
to: maskEmail(email),
to: email,
hasSmtpHost: Boolean(smtpHost),
hasSmtpUser: Boolean(smtpUser),
hasSmtpPass: Boolean(smtpPass),
@@ -681,7 +674,7 @@ export async function settingsRoutes(app: FastifyInstance) {
if (!smtpHost || !smtpUser) {
request.log.warn(
{ to: maskEmail(email), hasSmtpHost: Boolean(smtpHost), hasSmtpUser: Boolean(smtpUser) },
{ to: email, hasSmtpHost: Boolean(smtpHost), hasSmtpUser: Boolean(smtpUser) },
"[Settings] Test email skipped: SMTP not configured"
);
return reply.status(400).send({ error: "SMTP not configured" });
@@ -698,7 +691,7 @@ export async function settingsRoutes(app: FastifyInstance) {
},
});
request.log.info({ to: maskEmail(email) }, "[Settings] Sending test email");
request.log.info({ to: email }, "[Settings] Sending test email");
const mailResult = await transporter.sendMail({
from: smtpFrom,
@@ -721,11 +714,11 @@ export async function settingsRoutes(app: FastifyInstance) {
throw new Error(deliveryError);
}
request.log.info({ to: maskEmail(email), messageId: mailResult.messageId }, "[Settings] Test email sent");
request.log.info({ to: email, messageId: mailResult.messageId }, "[Settings] Test email sent");
return reply.send({ success: true, message: "Test email sent successfully" });
} catch (error) {
request.log.error({ error, to: maskEmail(email) }, "[Settings] Test email failed");
request.log.error({ to: email, error }, "[Settings] Test email failed");
const failure = classifyTestEmailFailure(error);
return reply.status(failure.status).send({ error: failure.message, code: failure.code });
}