chore: fix lint errors and reduce warnings across codebase (#234)
* chore: fix lint errors and reduce warnings across codebase - Fix noExplicitAny catches in backend routes and plugins - Fix noNestedTernary issues in backend services - Add keyboard event handlers for useKeyWithClickEvents in frontend - Disable noImportantStyles rule in biome.json - Fix formatting errors across all changed files - Fix test file lint issues Closes #233 * fix: restore any types in test files for TS compatibility * fix: revert Auth.tsx dependency array changes that caused infinite re-render * fix: null-safe user.username access in AppContext dependency array
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { type Client, createClient } from "@libsql/client";
|
||||
import dotenv from "dotenv";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
@@ -8,7 +7,6 @@ import { log } from "../utils/logger.js";
|
||||
import {
|
||||
ensureDataDirectory,
|
||||
ensureDefaultUser,
|
||||
getDataDir,
|
||||
getDbPaths,
|
||||
repairOrphanedDoseIds,
|
||||
repairTrailingHyphenDoseIds,
|
||||
@@ -65,8 +63,8 @@ let client: Client;
|
||||
try {
|
||||
client = createClient({ url });
|
||||
log.debug(`[DB] Database client created successfully`);
|
||||
} catch (err: any) {
|
||||
log.error(`[DB] ERROR: Failed to create database client: ${err.message}`);
|
||||
} catch (err: unknown) {
|
||||
log.error(`[DB] ERROR: Failed to create database client: ${(err as Error).message}`);
|
||||
log.error(`[DB] Database path: ${dbPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
+23
-23
@@ -71,8 +71,8 @@ export function ensureDataDirectory(dataDir: string): { success: boolean; error?
|
||||
writeFileSync(testFile, "test");
|
||||
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +87,14 @@ export async function runDrizzleMigrations(
|
||||
try {
|
||||
await migrate(database, { migrationsFolder });
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// If the error is about existing schema objects, the DB is already up-to-date
|
||||
// This happens when ALTER migrations in client.ts have already added the columns,
|
||||
// or when tables were created before drizzle migrations were introduced
|
||||
if (err.message?.includes("duplicate column") || err.message?.includes("already exists")) {
|
||||
return { success: true, warning: `Schema already up-to-date: ${err.message}` };
|
||||
if ((err as Error).message?.includes("duplicate column") || (err as Error).message?.includes("already exists")) {
|
||||
return { success: true, warning: `Schema already up-to-date: ${(err as Error).message}` };
|
||||
}
|
||||
return { success: false, error: err.message };
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +158,10 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
||||
for (const sql of alterMigrations) {
|
||||
try {
|
||||
await client.execute(sql);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
// Silently ignore "duplicate column" errors - column already exists
|
||||
if (!e.message?.includes("duplicate column")) {
|
||||
errors.push(e.message);
|
||||
if (!(e as Error).message?.includes("duplicate column")) {
|
||||
errors.push((e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,10 +182,10 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
||||
for (const sql of createTableMigrations) {
|
||||
try {
|
||||
await client.execute(sql);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
// Silently ignore "table already exists" errors
|
||||
if (!e.message?.includes("already exists")) {
|
||||
errors.push(e.message);
|
||||
if (!(e as Error).message?.includes("already exists")) {
|
||||
errors.push((e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,10 +199,10 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
||||
for (const sql of createIndexMigrations) {
|
||||
try {
|
||||
await client.execute(sql);
|
||||
} catch (e: any) {
|
||||
} catch (e: unknown) {
|
||||
// Silently ignore "already exists" errors
|
||||
if (!e.message?.includes("already exists")) {
|
||||
errors.push(e.message);
|
||||
if (!(e as Error).message?.includes("already exists")) {
|
||||
errors.push((e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,8 +227,8 @@ export async function ensureDefaultUser(client: Client, authEnabled: boolean): P
|
||||
return true; // Created
|
||||
}
|
||||
return false; // Already exists
|
||||
} catch (e: any) {
|
||||
console.error(`[DB] Error creating default user:`, e.message);
|
||||
} catch (e: unknown) {
|
||||
console.error(`[DB] Error creating default user:`, (e as Error).message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -255,8 +255,8 @@ export async function repairTrailingHyphenDoseIds(client: Client): Promise<{ rep
|
||||
"UPDATE dose_tracking SET dose_id = RTRIM(dose_id, '-') WHERE dose_id LIKE '%-'"
|
||||
);
|
||||
repaired = result.rowsAffected;
|
||||
} catch (e: any) {
|
||||
errors.push(`Trailing-hyphen repair failed: ${e.message}`);
|
||||
} catch (e: unknown) {
|
||||
errors.push(`Trailing-hyphen repair failed: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
return { repaired, errors };
|
||||
@@ -379,14 +379,14 @@ export async function repairOrphanedDoseIds(client: Client): Promise<{ repaired:
|
||||
args: [newDoseId, dose.id],
|
||||
});
|
||||
repaired++;
|
||||
} catch (e: any) {
|
||||
errors.push(`Failed to repair dose ${dose.id}: ${e.message}`);
|
||||
} catch (e: unknown) {
|
||||
errors.push(`Failed to repair dose ${dose.id}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
errors.push(`Repair failed: ${e.message}`);
|
||||
} catch (e: unknown) {
|
||||
errors.push(`Repair failed: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
return { repaired, errors };
|
||||
|
||||
@@ -41,8 +41,8 @@ export async function executeMigration(
|
||||
const executed = Number(tables.rows[0].count) || 0;
|
||||
|
||||
return { success: true, executed, errors };
|
||||
} catch (err: any) {
|
||||
errors.push(err.message);
|
||||
} catch (err: unknown) {
|
||||
errors.push((err as Error).message);
|
||||
return { success: false, executed: 0, errors };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +142,12 @@ export async function requireAuth(request: FastifyRequest, reply: FastifyReply)
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// Re-throw our own errors
|
||||
if (err?.message === "AUTH_REQUIRED" || err?.message === "USER_NOT_FOUND" || err?.message === "ACCOUNT_DISABLED") {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
(err.message === "AUTH_REQUIRED" || err.message === "USER_NOT_FOUND" || err.message === "ACCOUNT_DISABLED")
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
// JWT verification failed
|
||||
|
||||
@@ -864,11 +864,8 @@ export async function medicationRoutes(app: FastifyInstance) {
|
||||
const takenByJson = row.takenByJson ? JSON.parse(row.takenByJson) : [];
|
||||
const intake = intakes[blisterIdx];
|
||||
const intakePerson = intake?.takenBy;
|
||||
const peopleForThisIntake: (string | null)[] = intakePerson
|
||||
? [intakePerson]
|
||||
: takenByJson.length > 0
|
||||
? takenByJson
|
||||
: [null];
|
||||
const takenByFallback: (string | null)[] = takenByJson.length > 0 ? takenByJson : [null];
|
||||
const peopleForThisIntake: (string | null)[] = intakePerson ? [intakePerson] : takenByFallback;
|
||||
|
||||
// Generate expected dose IDs and check if they're taken
|
||||
for (let i = 0; i < occurrences; i++) {
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function oidcRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
return reply.redirect(authUrl.href);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
console.error("[OIDC] Login error:", err);
|
||||
return reply.redirect(`${getFrontendUrl()}/?error=oidc_init_failed`);
|
||||
}
|
||||
@@ -167,7 +167,10 @@ export async function oidcRoutes(app: FastifyInstance) {
|
||||
// Extract username from configured claim
|
||||
const usernameClaim = env.OIDC_USERNAME_CLAIM;
|
||||
const username =
|
||||
(userInfo as any)[usernameClaim] || userInfo.preferred_username || userInfo.email || userInfo.sub;
|
||||
(userInfo as Record<string, string>)[usernameClaim] ||
|
||||
userInfo.preferred_username ||
|
||||
userInfo.email ||
|
||||
userInfo.sub;
|
||||
const oidcSubject = userInfo.sub;
|
||||
|
||||
if (!username || !oidcSubject) {
|
||||
@@ -210,7 +213,7 @@ export async function oidcRoutes(app: FastifyInstance) {
|
||||
// In dev: CORS_ORIGINS contains the frontend URL
|
||||
const frontendUrl = env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173";
|
||||
return reply.redirect(`${frontendUrl}/dashboard`);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
console.error("[OIDC] Callback error:", err);
|
||||
return reply.redirect(`${getFrontendUrl()}/?error=oidc_callback_failed`);
|
||||
}
|
||||
|
||||
@@ -509,8 +509,10 @@ ${getFooterPlain(language)}`;
|
||||
const buildTableRow = (row: LowStockItem) => {
|
||||
const isEmpty = row.medsLeft <= 0;
|
||||
const isCritical = row.isCritical !== false;
|
||||
const statusIcon = isEmpty ? "🚨" : isCritical ? "🚨" : "⚠️";
|
||||
const rowBg = isEmpty ? "#fef2f2" : isCritical ? "#fff7ed" : "white";
|
||||
const nonEmptyIcon = isCritical ? "🚨" : "⚠️";
|
||||
const statusIcon = isEmpty ? "🚨" : nonEmptyIcon;
|
||||
const nonEmptyBg = isCritical ? "#fff7ed" : "white";
|
||||
const rowBg = isEmpty ? "#fef2f2" : nonEmptyBg;
|
||||
const safeName = escapeHtml(row.name);
|
||||
const safeMedsLeft = Number(row.medsLeft) || 0;
|
||||
const safeDaysLeft = Number(row.daysLeft) || 0;
|
||||
@@ -586,7 +588,7 @@ ${getFooterPlain(language)}`;
|
||||
|
||||
// Send push notification if enabled
|
||||
if (notificationSettings.shoutrrrEnabled && notificationSettings.shoutrrrUrl) {
|
||||
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`;
|
||||
const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
|
||||
|
||||
try {
|
||||
const pushResult = await sendShoutrrrNotification(notificationSettings.shoutrrrUrl, notificationTitle, message);
|
||||
@@ -603,7 +605,8 @@ ${getFooterPlain(language)}`;
|
||||
|
||||
// Update the reminder state to record this notification was sent
|
||||
if (results.email || results.push) {
|
||||
const channel = results.email && results.push ? "both" : results.email ? "email" : "push";
|
||||
const singleChannel = results.email ? "email" : "push";
|
||||
const channel = results.email && results.push ? "both" : singleChannel;
|
||||
updateReminderSentTime("stock", channel);
|
||||
|
||||
// Also update user settings in database so frontend can display the info
|
||||
@@ -700,14 +703,15 @@ ${getFooterPlain(language)}`;
|
||||
|
||||
const bodyText =
|
||||
emptyRx.length > 0 ? tr.prescriptionReminder.descriptionEmpty : tr.prescriptionReminder.descriptionLow;
|
||||
const alertText =
|
||||
emptyRx.length > 0
|
||||
? emptyRx.length === 1
|
||||
? tr.prescriptionReminder.alertEmptySingle
|
||||
: t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length })
|
||||
: lowRx.length === 1
|
||||
? tr.prescriptionReminder.alertLowSingle
|
||||
: t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length });
|
||||
const emptyAlert =
|
||||
emptyRx.length === 1
|
||||
? tr.prescriptionReminder.alertEmptySingle
|
||||
: t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length });
|
||||
const lowAlert =
|
||||
lowRx.length === 1
|
||||
? tr.prescriptionReminder.alertLowSingle
|
||||
: t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length });
|
||||
const alertText = emptyRx.length > 0 ? emptyAlert : lowAlert;
|
||||
|
||||
const tableRows = filteredPrescriptionLow
|
||||
.map((item) => {
|
||||
@@ -807,7 +811,7 @@ ${getFooterPlain(language)}`;
|
||||
);
|
||||
}
|
||||
}
|
||||
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`;
|
||||
const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
|
||||
|
||||
try {
|
||||
const pushResult = await sendShoutrrrNotification(userSettings.shoutrrrUrl, title, message);
|
||||
@@ -823,7 +827,8 @@ ${getFooterPlain(language)}`;
|
||||
}
|
||||
|
||||
if (results.email || results.push) {
|
||||
const channel = results.email && results.push ? "both" : results.email ? "email" : "push";
|
||||
const singleChannel = results.email ? "email" : "push";
|
||||
const channel = results.email && results.push ? "both" : singleChannel;
|
||||
updateReminderSentTime("prescription", channel);
|
||||
await updateUserReminderSentTime(userId, "prescription", channel, medNames);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import nodemailer from "nodemailer";
|
||||
import { db } from "../db/client.js";
|
||||
import { userSettings } from "../db/schema.js";
|
||||
@@ -239,7 +239,7 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
|
||||
// Helper to get user ID from request
|
||||
// Returns anonymous user ID when auth is disabled
|
||||
async function getUserId(request: any, reply: any): Promise<number> {
|
||||
async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
|
||||
// If auth is disabled, use the anonymous user
|
||||
if (!env.AUTH_ENABLED) {
|
||||
return getAnonymousUserId();
|
||||
@@ -544,7 +544,7 @@ export async function sendShoutrrrNotification(
|
||||
}
|
||||
|
||||
// Use ONLY the reconstructed URL from validation - never the original urlStr
|
||||
const { url: sanitizedUrl, isNtfy, auth } = validation;
|
||||
const { url: sanitizedUrl, isNtfy: _isNtfy, auth } = validation;
|
||||
|
||||
let targetUrl: string;
|
||||
const method = "POST";
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
getTimezone,
|
||||
getTodaysIntakes,
|
||||
getUpcomingIntakes,
|
||||
type Intake,
|
||||
type IntakeReminderState,
|
||||
parseIntakeReminderState,
|
||||
parseIntakesJson,
|
||||
@@ -321,7 +320,7 @@ async function checkAndSendIntakeRemindersForUser(
|
||||
});
|
||||
|
||||
// Process each intake separately to track blisterIndex
|
||||
intakesWithReminders.forEach((intake, blisterIndex) => {
|
||||
intakesWithReminders.forEach((intake, _blisterIndex) => {
|
||||
const actualIndex = intakes.indexOf(intake); // Get the actual index in original array
|
||||
logger.debug(
|
||||
`[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} - start: ${intake.start}, every: ${intake.every} days, usage: ${intake.usage}, takenBy: ${intake.takenBy || "(none)"}`
|
||||
@@ -684,7 +683,8 @@ async function checkAndSendIntakeRemindersForUser(
|
||||
saveIntakeReminderState(state);
|
||||
|
||||
// Update global reminder state for UI display
|
||||
const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push";
|
||||
const singleChannel = emailSuccess ? "email" : "push";
|
||||
const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
|
||||
updateReminderSentTime("intake", channel);
|
||||
|
||||
// Also update user settings in database so frontend can display the info
|
||||
|
||||
@@ -275,8 +275,10 @@ async function sendReminderEmail(
|
||||
.map((row) => {
|
||||
const isEmpty = row.medsLeft <= 0;
|
||||
const isCritical = row.isCritical;
|
||||
const statusIcon = isEmpty ? "🚨" : isCritical ? "🚨" : "⚠️";
|
||||
const rowBg = isEmpty ? "#fef2f2" : isCritical ? "#fff7ed" : "white";
|
||||
const nonEmptyIcon = isCritical ? "🚨" : "⚠️";
|
||||
const statusIcon = isEmpty ? "🚨" : nonEmptyIcon;
|
||||
const nonEmptyBg = isCritical ? "#fff7ed" : "white";
|
||||
const rowBg = isEmpty ? "#fef2f2" : nonEmptyBg;
|
||||
return `
|
||||
<tr style="background: ${rowBg};">
|
||||
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; white-space: nowrap;">${statusIcon} ${row.name}</td>
|
||||
@@ -329,7 +331,8 @@ ${lowStock.map((r) => `${r.name}: ${r.medsLeft} ${tr.common.pills}, ${r.daysLeft
|
||||
---
|
||||
${getFooterPlain(language)}${isRepeatDaily ? `\n\n${tr.stockReminder.repeatDailyNote}` : ""}`;
|
||||
|
||||
const subjectPlural = lowStock.length === 1 ? "" : language === "de" ? "e" : "s";
|
||||
const pluralSuffix = language === "de" ? "e" : "s";
|
||||
const subjectPlural = lowStock.length === 1 ? "" : pluralSuffix;
|
||||
const subject = t(tr.stockReminder.subject, { count: lowStock.length, s: subjectPlural, e: subjectPlural });
|
||||
|
||||
try {
|
||||
@@ -460,7 +463,7 @@ async function checkAndSendReminderForUser(
|
||||
)
|
||||
);
|
||||
}
|
||||
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`;
|
||||
const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
|
||||
const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message);
|
||||
shoutrrrSuccess = result.success;
|
||||
if (!result.success) {
|
||||
@@ -470,7 +473,8 @@ async function checkAndSendReminderForUser(
|
||||
|
||||
if (emailSuccess || shoutrrrSuccess) {
|
||||
const currentState = loadReminderState();
|
||||
const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push";
|
||||
const singleChannel = emailSuccess ? "email" : "push";
|
||||
const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
|
||||
saveReminderState({
|
||||
lastAutoEmailSent: new Date().toISOString(),
|
||||
lastAutoEmailDate: today,
|
||||
@@ -480,7 +484,6 @@ async function checkAndSendReminderForUser(
|
||||
lastNotificationChannel: channel,
|
||||
});
|
||||
|
||||
const firstMed = allLowStock[0];
|
||||
const medNames = allLowStock.map((m) => m.name).join(", ");
|
||||
await updateUserReminderSentTime(settings.userId, "stock", channel, medNames);
|
||||
}
|
||||
@@ -537,14 +540,15 @@ async function checkAndSendReminderForUser(
|
||||
|
||||
const bodyText =
|
||||
emptyRx.length > 0 ? tr.prescriptionReminder.descriptionEmpty : tr.prescriptionReminder.descriptionLow;
|
||||
const alertText =
|
||||
emptyRx.length > 0
|
||||
? emptyRx.length === 1
|
||||
? tr.prescriptionReminder.alertEmptySingle
|
||||
: t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length })
|
||||
: lowRx.length === 1
|
||||
? tr.prescriptionReminder.alertLowSingle
|
||||
: t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length });
|
||||
const emptyAlert =
|
||||
emptyRx.length === 1
|
||||
? tr.prescriptionReminder.alertEmptySingle
|
||||
: t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length });
|
||||
const lowAlert =
|
||||
lowRx.length === 1
|
||||
? tr.prescriptionReminder.alertLowSingle
|
||||
: t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length });
|
||||
const alertText = emptyRx.length > 0 ? emptyAlert : lowAlert;
|
||||
|
||||
const tableRows = allPrescriptionLow
|
||||
.map((item) => {
|
||||
@@ -649,7 +653,7 @@ async function checkAndSendReminderForUser(
|
||||
);
|
||||
}
|
||||
}
|
||||
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`;
|
||||
const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
|
||||
const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message);
|
||||
shoutrrrSuccess = result.success;
|
||||
if (!result.success) {
|
||||
@@ -659,7 +663,8 @@ async function checkAndSendReminderForUser(
|
||||
|
||||
if (emailSuccess || shoutrrrSuccess) {
|
||||
const currentState = loadReminderState();
|
||||
const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push";
|
||||
const singleChannel = emailSuccess ? "email" : "push";
|
||||
const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
|
||||
saveReminderState({
|
||||
lastAutoEmailSent: new Date().toISOString(),
|
||||
lastAutoEmailDate: today,
|
||||
@@ -669,7 +674,6 @@ async function checkAndSendReminderForUser(
|
||||
lastNotificationChannel: channel,
|
||||
});
|
||||
|
||||
const firstMed = allPrescriptionLow[0];
|
||||
const medNames = allPrescriptionLow.map((m) => m.name).join(", ");
|
||||
await updateUserReminderSentTime(settings.userId, "prescription", channel, medNames);
|
||||
}
|
||||
|
||||
@@ -294,8 +294,8 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
|
||||
// Should set cookies
|
||||
const cookies = response.cookies;
|
||||
expect(cookies.find((c: any) => c.name === "access_token")).toBeDefined();
|
||||
expect(cookies.find((c: any) => c.name === "refresh_token")).toBeDefined();
|
||||
expect(cookies.find((c: { name: string }) => c.name === "access_token")).toBeDefined();
|
||||
expect(cookies.find((c: { name: string }) => c.name === "refresh_token")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should login case-insensitively with different username casing", async () => {
|
||||
@@ -393,7 +393,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const refreshToken = login.cookies.find((c: any) => c.name === "refresh_token");
|
||||
const refreshToken = login.cookies.find((c: { name: string }) => c.name === "refresh_token");
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
@@ -456,7 +456,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const refreshToken = login.cookies.find((c: any) => c.name === "refresh_token");
|
||||
const refreshToken = login.cookies.find((c: { name: string }) => c.name === "refresh_token");
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
@@ -506,7 +506,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = login.cookies.find((c: any) => c.name === "access_token");
|
||||
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
||||
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
@@ -604,7 +604,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = login.cookies.find((c: any) => c.name === "access_token");
|
||||
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PUT",
|
||||
@@ -653,7 +653,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = login.cookies.find((c: any) => c.name === "access_token");
|
||||
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PUT",
|
||||
@@ -689,7 +689,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = login.cookies.find((c: any) => c.name === "access_token");
|
||||
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PUT",
|
||||
@@ -742,7 +742,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = login.cookies.find((c: any) => c.name === "access_token");
|
||||
const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
|
||||
|
||||
// Delete account
|
||||
const response = await app.inject({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { createClient } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { migrate } from "drizzle-orm/libsql/migrator";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
// Import utility functions from db-utils (no side effects, unlike client.ts which initializes the DB)
|
||||
import {
|
||||
|
||||
@@ -271,7 +271,7 @@ describe("Dose Tracking API", () => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
const data = response.json();
|
||||
expect(data.doses).toHaveLength(2);
|
||||
expect(data.doses.map((d: any) => d.doseId).sort()).toEqual([doseId1, doseId2].sort());
|
||||
expect(data.doses.map((d: { doseId: string }) => d.doseId).sort()).toEqual([doseId1, doseId2].sort());
|
||||
// Each dose should have a takenAt timestamp
|
||||
for (const dose of data.doses) {
|
||||
expect(dose.takenAt).toBeTypeOf("number");
|
||||
|
||||
@@ -1671,7 +1671,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
url: "/medications",
|
||||
});
|
||||
expect(medsResponse.statusCode).toBe(200);
|
||||
const med = medsResponse.json().find((m: any) => m.id === medId);
|
||||
const med = medsResponse.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.prescriptionRemainingRefills).toBe(1);
|
||||
|
||||
const historyResponse = await app.inject({
|
||||
@@ -1809,8 +1809,10 @@ describe("E2E Tests with Real Routes", () => {
|
||||
const refills = response.json();
|
||||
expect(refills).toHaveLength(2);
|
||||
// Check both refills exist (order may vary)
|
||||
const hasPackRefill = refills.some((r: any) => r.packsAdded === 1 && r.loosePillsAdded === 0);
|
||||
const hasLooseRefill = refills.some((r: any) => r.packsAdded === 0 && r.loosePillsAdded === 5);
|
||||
const hasPackRefill = refills.some((r: Record<string, unknown>) => r.packsAdded === 1 && r.loosePillsAdded === 0);
|
||||
const hasLooseRefill = refills.some(
|
||||
(r: Record<string, unknown>) => r.packsAdded === 0 && r.loosePillsAdded === 5
|
||||
);
|
||||
expect(hasPackRefill).toBe(true);
|
||||
expect(hasLooseRefill).toBe(true);
|
||||
});
|
||||
@@ -1888,7 +1890,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
|
||||
expect(getResponse.statusCode).toBe(200);
|
||||
const meds = getResponse.json();
|
||||
const med = meds.find((m: any) => m.id === medId);
|
||||
const med = meds.find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med).toBeDefined();
|
||||
expect(med.stockAdjustment).toBe(-7);
|
||||
expect(med.lastStockCorrectionAt).toBeTruthy();
|
||||
@@ -1934,7 +1936,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
method: "GET",
|
||||
url: "/medications",
|
||||
});
|
||||
const med = getResponse.json().find((m: any) => m.id === medId);
|
||||
const med = getResponse.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.name).toBe("Renamed Med");
|
||||
expect(med.stockAdjustment).toBe(-5);
|
||||
});
|
||||
@@ -2003,7 +2005,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
|
||||
// Verify adjustment is set
|
||||
let getMeds = await app.inject({ method: "GET", url: "/medications" });
|
||||
let med = getMeds.json().find((m: any) => m.id === medId);
|
||||
let med = getMeds.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.stockAdjustment).toBe(-10);
|
||||
|
||||
// Edit medication with CHANGED stock fields (packCount 1 → 2)
|
||||
@@ -2022,7 +2024,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
|
||||
// stockAdjustment should be reset to 0
|
||||
getMeds = await app.inject({ method: "GET", url: "/medications" });
|
||||
med = getMeds.json().find((m: any) => m.id === medId);
|
||||
med = getMeds.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.stockAdjustment).toBe(0);
|
||||
expect(med.lastStockCorrectionAt).toBeTruthy();
|
||||
});
|
||||
@@ -2066,7 +2068,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
|
||||
// stockAdjustment should be preserved
|
||||
const getMeds = await app.inject({ method: "GET", url: "/medications" });
|
||||
const med = getMeds.json().find((m: any) => m.id === medId);
|
||||
const med = getMeds.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.name).toBe("Renamed Preserve Med");
|
||||
expect(med.stockAdjustment).toBe(-5);
|
||||
});
|
||||
@@ -2114,7 +2116,7 @@ describe("E2E Tests with Real Routes", () => {
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const data = response.json();
|
||||
const med = data.find((m: any) => m.medicationId === medId);
|
||||
const med = data.find((m: Record<string, unknown>) => m.medicationId === medId);
|
||||
expect(med).toBeDefined();
|
||||
// Total should be very close to 113 (not 112 or lower from phantom consumption)
|
||||
// Allow up to 1 pill of natural consumption (test runs fast, but at most 1 day could pass)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
|
||||
// Mock process.exit to prevent tests from exiting
|
||||
const mockExit = vi.fn();
|
||||
vi.spyOn(process, "exit").mockImplementation(mockExit as any);
|
||||
vi.spyOn(process, "exit").mockImplementation(mockExit as unknown as (...args: unknown[]) => never);
|
||||
|
||||
// Re-create the schema from env.ts for testing
|
||||
const EnvSchema = z.object({
|
||||
|
||||
@@ -23,10 +23,12 @@ async function registerExportRoutes(ctx: TestContext) {
|
||||
const userId = 1; // Test user ID
|
||||
|
||||
// Helper to parse blisters from DB
|
||||
function parseBlisters(row: any): Array<{ usage: number; every: number; start: string; remind: boolean }> {
|
||||
const usage = JSON.parse(row.usage_json || "[]") as number[];
|
||||
const every = JSON.parse(row.every_json || "[]") as number[];
|
||||
const start = JSON.parse(row.start_json || "[]") as string[];
|
||||
function parseBlisters(
|
||||
row: Record<string, unknown>
|
||||
): Array<{ usage: number; every: number; start: string; remind: boolean }> {
|
||||
const usage = JSON.parse((row.usage_json as string) || "[]") as number[];
|
||||
const every = JSON.parse((row.every_json as string) || "[]") as number[];
|
||||
const start = JSON.parse((row.start_json as string) || "[]") as string[];
|
||||
const len = Math.min(usage.length, every.length, start.length);
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
usage: usage[i],
|
||||
@@ -99,7 +101,7 @@ async function registerExportRoutes(ctx: TestContext) {
|
||||
args: [userId],
|
||||
});
|
||||
|
||||
let settings;
|
||||
let settings: Record<string, unknown> | undefined;
|
||||
if (settingsResult.rows.length > 0) {
|
||||
const s = settingsResult.rows[0];
|
||||
settings = {
|
||||
@@ -150,7 +152,8 @@ async function registerExportRoutes(ctx: TestContext) {
|
||||
});
|
||||
|
||||
// POST /import
|
||||
app.post<{ Body: any }>("/import", async (request, reply) => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: test helper with dynamic import data shape
|
||||
app.post("/import", async (request, reply) => {
|
||||
const importData = request.body as any;
|
||||
|
||||
// Basic validation
|
||||
@@ -167,9 +170,15 @@ async function registerExportRoutes(ctx: TestContext) {
|
||||
// Import medications
|
||||
const exportIdToNewId = new Map<string, number>();
|
||||
for (const med of importData.medications || []) {
|
||||
const usageJson = JSON.stringify((med.schedules || []).map((s: any) => s.usage));
|
||||
const everyJson = JSON.stringify((med.schedules || []).map((s: any) => s.every));
|
||||
const startJson = JSON.stringify((med.schedules || []).map((s: any) => s.start));
|
||||
const usageJson = JSON.stringify(
|
||||
((med.schedules as Array<Record<string, unknown>>) || []).map((s: Record<string, unknown>) => s.usage)
|
||||
);
|
||||
const everyJson = JSON.stringify(
|
||||
((med.schedules as Array<Record<string, unknown>>) || []).map((s: Record<string, unknown>) => s.every)
|
||||
);
|
||||
const startJson = JSON.stringify(
|
||||
((med.schedules as Array<Record<string, unknown>>) || []).map((s: Record<string, unknown>) => s.start)
|
||||
);
|
||||
const takenByJson = JSON.stringify(med.takenBy || []);
|
||||
|
||||
const result = await client.execute({
|
||||
|
||||
@@ -1333,8 +1333,8 @@ describe("Integration Tests", () => {
|
||||
url: "/medications",
|
||||
});
|
||||
const meds = medsRes.json();
|
||||
const med1 = meds.find((m: any) => m.id === med1Id);
|
||||
const med2 = meds.find((m: any) => m.id === med2Id);
|
||||
const med1 = meds.find((m: Record<string, unknown>) => m.id === med1Id);
|
||||
const med2 = meds.find((m: Record<string, unknown>) => m.id === med2Id);
|
||||
|
||||
expect(med1.dismissedUntil).toBe("2025-01-15");
|
||||
expect(med2.dismissedUntil).toBe("2025-01-15");
|
||||
@@ -1376,7 +1376,7 @@ describe("Integration Tests", () => {
|
||||
method: "GET",
|
||||
url: "/medications",
|
||||
});
|
||||
const med = medsRes.json().find((m: any) => m.id === medId);
|
||||
const med = medsRes.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.dismissedUntil).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1446,7 +1446,7 @@ describe("Integration Tests", () => {
|
||||
method: "GET",
|
||||
url: "/medications",
|
||||
});
|
||||
const med = medsRes.json().find((m: any) => m.id === medId);
|
||||
const med = medsRes.json().find((m: Record<string, unknown>) => m.id === medId);
|
||||
expect(med.dismissedUntil).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ vi.mock("../services/reminder-scheduler.js", () => ({
|
||||
|
||||
// Mock sendShoutrrrNotification from settings
|
||||
vi.mock("../routes/settings.js", async (importOriginal) => {
|
||||
const original = (await importOriginal()) as any;
|
||||
const original = (await importOriginal()) as Record<string, unknown>;
|
||||
return {
|
||||
...original,
|
||||
sendShoutrrrNotification: mockSendShoutrrr,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
|
||||
import cookie from "@fastify/cookie";
|
||||
import cors from "@fastify/cors";
|
||||
import sensible from "@fastify/sensible";
|
||||
import Fastify from "fastify";
|
||||
import Fastify, { type FastifyInstance } from "fastify";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
// Import from utils to avoid index.ts import side effects (server start)
|
||||
@@ -294,10 +294,18 @@ describe("Server Bootstrap", () => {
|
||||
refreshCookieOptions,
|
||||
});
|
||||
|
||||
expect((app as any).config.accessTtl).toBe(15);
|
||||
expect((app as any).config.refreshTtl).toBe(7);
|
||||
expect((app as any).config.cookieOptions.httpOnly).toBe(true);
|
||||
expect((app as any).config.refreshCookieOptions.maxAge).toBe(7 * 24 * 60 * 60);
|
||||
const appWithConfig = app as unknown as {
|
||||
config: {
|
||||
accessTtl: number;
|
||||
refreshTtl: number;
|
||||
cookieOptions: { httpOnly: boolean };
|
||||
refreshCookieOptions: { maxAge: number };
|
||||
};
|
||||
};
|
||||
expect(appWithConfig.config.accessTtl).toBe(15);
|
||||
expect(appWithConfig.config.refreshTtl).toBe(7);
|
||||
expect(appWithConfig.config.cookieOptions.httpOnly).toBe(true);
|
||||
expect(appWithConfig.config.refreshCookieOptions.maxAge).toBe(7 * 24 * 60 * 60);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
@@ -364,15 +372,15 @@ describe("Server Bootstrap", () => {
|
||||
const app = Fastify({ logger: false });
|
||||
|
||||
// Mock route plugins
|
||||
const healthRoutes = async (app: any) => {
|
||||
const healthRoutes = async (app: FastifyInstance) => {
|
||||
app.get("/health", async () => ({ status: "ok" }));
|
||||
};
|
||||
|
||||
const authRoutes = async (app: any) => {
|
||||
const authRoutes = async (app: FastifyInstance) => {
|
||||
app.post("/auth/login", async () => ({ token: "mock" }));
|
||||
};
|
||||
|
||||
const medicationRoutes = async (app: any) => {
|
||||
const medicationRoutes = async (app: FastifyInstance) => {
|
||||
app.get("/medications", async () => []);
|
||||
};
|
||||
|
||||
|
||||
@@ -612,8 +612,8 @@ describe("Stock Calculation API", () => {
|
||||
const data = response.json();
|
||||
expect(data).toHaveLength(2);
|
||||
|
||||
const medA = data.find((d: any) => d.medicationName === "Med A");
|
||||
const medB = data.find((d: any) => d.medicationName === "Med B");
|
||||
const medA = data.find((d: Record<string, unknown>) => d.medicationName === "Med A");
|
||||
const medB = data.find((d: Record<string, unknown>) => d.medicationName === "Med B");
|
||||
|
||||
expect(medA.plannerUsage).toBe(10); // 10 days × 1 pill
|
||||
expect(medB.plannerUsage).toBe(10); // 5 doses × 2 pills
|
||||
|
||||
@@ -191,7 +191,7 @@ export function parseIntakesJson(
|
||||
try {
|
||||
const parsed = JSON.parse(intakesJson);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
return parsed.map((intake: any) => ({
|
||||
return parsed.map((intake: Record<string, unknown>) => ({
|
||||
usage: typeof intake.usage === "number" ? intake.usage : 0,
|
||||
every: typeof intake.every === "number" ? intake.every : 1,
|
||||
start: typeof intake.start === "string" ? intake.start : new Date().toISOString(),
|
||||
@@ -312,7 +312,7 @@ export type UpcomingIntake = {
|
||||
export function getTodaysIntakes(
|
||||
medName: string,
|
||||
intakes: Intake[],
|
||||
medicationTakenBy: string[], // Medication-level takenBy as fallback
|
||||
_medicationTakenBy: string[], // Medication-level takenBy as fallback
|
||||
pillWeightMg: number | null,
|
||||
locale: string,
|
||||
tz?: string,
|
||||
@@ -388,7 +388,7 @@ export function getUpcomingIntakes(
|
||||
medName: string,
|
||||
intakes: Intake[],
|
||||
minutesBefore: number,
|
||||
medicationTakenBy: string[], // Medication-level takenBy as fallback
|
||||
_medicationTakenBy: string[], // Medication-level takenBy as fallback
|
||||
pillWeightMg: number | null,
|
||||
locale: string,
|
||||
tz?: string,
|
||||
|
||||
Reference in New Issue
Block a user