refactor(types): type-check batch 1 — 8 tail routes (spending/auth/dataSources/status/profile/import/notifications/categories)

Dropped @ts-nocheck and annotated the fallout (~54 errors, all mechanical:
implicit-any helper/callback params + dynamic response-assembly objects
in status.cts typed `any` per the shared db.d.ts philosophy). No behavior
change — pure annotation. Zero 'possibly undefined' cases in this batch.
@ts-nocheck server count: 29 -> 21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-11 06:53:27 -05:00
parent 25404ec4ed
commit 345a159288
8 changed files with 35 additions and 42 deletions

View File

@ -1,9 +1,8 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
let _appVersion; let _appVersion: string | undefined;
function getAppVersion() { function getAppVersion() {
if (!_appVersion) { if (!_appVersion) {
try { try {
@ -196,7 +195,7 @@ router.get('/login-history', requireAuth, (req: Req, res: Res) => {
) )
.all(req.user.id); .all(req.user.id);
const safeDecrypt = (v) => { const safeDecrypt = (v: any) => {
if (!v) return null; if (!v) return null;
try { try {
return decryptSecret(v); return decryptSecret(v);
@ -214,7 +213,7 @@ router.get('/login-history', requireAuth, (req: Req, res: Res) => {
? require('crypto').createHash('sha256').update(currentCookie).digest('hex').slice(0, 32) ? require('crypto').createHash('sha256').update(currentCookie).digest('hex').slice(0, 32)
: null; : null;
const history = rows.map((r) => ({ const history = rows.map((r: any) => ({
id: r.id, id: r.id,
logged_in_at: r.logged_in_at, logged_in_at: r.logged_in_at,
ip_address: safeDecrypt(r.ip_address), ip_address: safeDecrypt(r.ip_address),

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { ValidationError, NotFoundError, ConflictError } = require('../utils/apiError.cts'); const { ValidationError, NotFoundError, ConflictError } = require('../utils/apiError.cts');
@ -10,7 +9,7 @@ const { sql } = require('kysely');
// Maps a SQLite UNIQUE-constraint violation to a friendly 409; re-throws anything // Maps a SQLite UNIQUE-constraint violation to a friendly 409; re-throws anything
// else so the terminal handler returns a safe 500. // else so the terminal handler returns a safe 500.
function asConflict(e, message, field) { function asConflict(e: any, message: string, field?: string) {
if (e.message?.includes('UNIQUE')) throw ConflictError(message, field); if (e.message?.includes('UNIQUE')) throw ConflictError(message, field);
throw e; throw e;
} }
@ -57,8 +56,8 @@ router.get('/', (req: Req, res: Res) => {
ORDER BY b.active DESC, b.due_day ASC, b.name COLLATE NOCASE ASC ORDER BY b.active DESC, b.due_day ASC, b.name COLLATE NOCASE ASC
`); `);
const shaped = categories.map((category) => { const shaped = categories.map((category: any) => {
const bills = billsByCategory.all(req.user.id, category.id).map((bill) => ({ const bills = billsByCategory.all(req.user.id, category.id).map((bill: any) => ({
...bill, ...bill,
active: !!bill.active, active: !!bill.active,
payment_count: Number(bill.payment_count || 0), payment_count: Number(bill.payment_count || 0),
@ -66,9 +65,9 @@ router.get('/', (req: Req, res: Res) => {
last_paid_date: bill.last_paid_date || null, last_paid_date: bill.last_paid_date || null,
})); }));
const activeBillCount = bills.filter((bill) => bill.active).length; const activeBillCount = bills.filter((bill: any) => bill.active).length;
const inactiveBillCount = bills.length - activeBillCount; const inactiveBillCount = bills.length - activeBillCount;
const paymentCount = bills.reduce((sum, bill) => sum + bill.payment_count, 0); const paymentCount = bills.reduce((sum: number, bill: any) => sum + bill.payment_count, 0);
return { return {
...category, ...category,
@ -77,7 +76,7 @@ router.get('/', (req: Req, res: Res) => {
active_bill_count: activeBillCount, active_bill_count: activeBillCount,
inactive_bill_count: inactiveBillCount, inactive_bill_count: inactiveBillCount,
payment_count: paymentCount, payment_count: paymentCount,
bill_names: bills.map((bill) => bill.name), bill_names: bills.map((bill: any) => bill.name),
bills, bills,
}; };
}); });
@ -129,7 +128,7 @@ router.put('/reorder', (req: Req, res: Res) => {
const update = db.prepare( const update = db.prepare(
"UPDATE categories SET sort_order = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?", "UPDATE categories SET sort_order = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
); );
db.transaction((items) => { db.transaction((items: any) => {
for (const item of items) update.run(item.sortOrder, item.categoryId, req.user.id); for (const item of items) update.run(item.sortOrder, item.categoryId, req.user.id);
})(entries); })(entries);

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
('use strict'); ('use strict');
@ -22,14 +21,14 @@ const { syncLimiter } = require('../middleware/rateLimiter.cts');
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']); const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
const VALID_STATUSES = new Set(['active', 'inactive', 'error']); const VALID_STATUSES = new Set(['active', 'inactive', 'error']);
function cleanFilter(value) { function cleanFilter(value: any) {
return typeof value === 'string' ? value.trim() : ''; return typeof value === 'string' ? value.trim() : '';
} }
// SimpleFIN/service errors are deliberately surfaced to the user — but only // SimpleFIN/service errors are deliberately surfaced to the user — but only
// after sanitizeErrorMessage strips tokens/URLs. Wrapping in ApiError keeps // after sanitizeErrorMessage strips tokens/URLs. Wrapping in ApiError keeps
// the sanitized message through the terminal handler (raw 5xx would be masked). // the sanitized message through the terminal handler (raw 5xx would be masked).
function toSourceError(err, fallback, code = 'SIMPLEFIN_ERROR') { function toSourceError(err: any, fallback: string, code = 'SIMPLEFIN_ERROR') {
const msg = sanitizeErrorMessage(err?.message || fallback); const msg = sanitizeErrorMessage(err?.message || fallback);
const status = typeof err?.status === 'number' ? err.status : 500; const status = typeof err?.status === 'number' ? err.status : 500;
return new ApiError(err?.code || code, msg, status); return new ApiError(err?.code || code, msg, status);
@ -63,7 +62,7 @@ router.get('/accounts/all', (req: Req, res: Res) => {
.all(req.user.id); .all(req.user.id);
res.json( res.json(
accounts.map((a) => ({ accounts.map((a: any) => ({
...a, ...a,
monitored: a.monitored === 1, monitored: a.monitored === 1,
balance_dollars: a.balance !== null ? a.balance / 100 : null, balance_dollars: a.balance !== null ? a.balance / 100 : null,
@ -211,7 +210,7 @@ router.get('/:sourceId/accounts', (req: Req, res: Res) => {
LIMIT 50 LIMIT 50
`); `);
const result = accounts.map((acc) => ({ const result = accounts.map((acc: any) => ({
...acc, ...acc,
monitored: acc.monitored === 1, monitored: acc.monitored === 1,
transactions: acc.monitored === 1 ? txStmt.all(acc.id, req.user.id) : [], transactions: acc.monitored === 1 ? txStmt.all(acc.id, req.user.id) : [],
@ -310,7 +309,7 @@ router.post('/sync-all', syncLimiter, async (req: Req, res: Res) => {
autoMatched += result.autoMatched ?? 0; autoMatched += result.autoMatched ?? 0;
for (const name of result.matched_bills ?? []) matchedBillSet.add(name); for (const name of result.matched_bills ?? []) matchedBillSet.add(name);
for (const attr of result.late_attributions ?? []) lateAttrAll.push(attr); for (const attr of result.late_attributions ?? []) lateAttrAll.push(attr);
} catch (err) { } catch (err: any) {
errors.push(sanitizeErrorMessage(err?.message || 'Sync failed')); errors.push(sanitizeErrorMessage(err?.message || 'Sync failed'));
} }
} }

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
('use strict'); ('use strict');
@ -25,7 +24,7 @@ function dataImportEnabled() {
return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false'; return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false';
} }
function requireDataImportEnabled(req, res, next) { function requireDataImportEnabled(req: Req, res: Res, next: Next) {
if (!dataImportEnabled()) { if (!dataImportEnabled()) {
return res return res
.status(403) .status(403)
@ -38,7 +37,7 @@ function makeErrorId() {
return `imp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; return `imp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
} }
function sendImportError(res, err, fallback, defaultCode) { function sendImportError(res: Res, err: any, fallback: string, defaultCode: string) {
// Only 4xx service errors carry a user-facing message; a thrown error with // Only 4xx service errors carry a user-facing message; a thrown error with
// an explicit 5xx status must fall through to the masked error-id branch. // an explicit 5xx status must fall through to the masked error-id branch.
if (err.status && err.status < 500) { if (err.status && err.status < 500) {
@ -423,7 +422,7 @@ router.get('/history', requireDataImportEnabled, (req: Req, res: Res) => {
try { try {
const history = getImportHistory(req.user.id); const history = getImportHistory(req.user.id);
res.json({ history }); res.json({ history });
} catch (err) { } catch (err: any) {
log.error('[import] history error:', err.message); log.error('[import] history error:', err.message);
res.status(500).json({ error: 'Failed to load import history' }); res.status(500).json({ error: 'Failed to load import history' });
} }

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const { log } = require('../utils/logger.cts'); const { log } = require('../utils/logger.cts');
@ -29,7 +28,7 @@ router.get('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => {
'notify_global_recipient', 'notify_global_recipient',
'reminder_hour', 'reminder_hour',
]; ];
const settings = {}; const settings: Record<string, string> = {};
for (const k of keys) settings[k] = getSetting(k) || ''; for (const k of keys) settings[k] = getSetting(k) || '';
if (!settings.reminder_hour) settings.reminder_hour = '6'; if (!settings.reminder_hour) settings.reminder_hour = '6';
// Mask password in response // Mask password in response
@ -65,7 +64,7 @@ router.put('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => {
setSetting('reminder_hour', String(hour)); setSetting('reminder_hour', String(hour));
try { try {
require('../workers/dailyWorker.cts').rescheduleDailyWorker(); require('../workers/dailyWorker.cts').rescheduleDailyWorker();
} catch (err) { } catch (err: any) {
log.error('[notifications] Failed to reschedule daily worker:', err.message); log.error('[notifications] Failed to reschedule daily worker:', err.message);
} }
} }
@ -78,7 +77,7 @@ router.post('/test', requireAuth, requireAdmin, async (req: Req, res: Res) => {
if (!to) throw ValidationError('Recipient address required', 'to'); if (!to) throw ValidationError('Recipient address required', 'to');
try { try {
await sendTestEmail(to); await sendTestEmail(to);
} catch (err) { } catch (err: any) {
// Deliberate pass-through: the SMTP error text is the admin's own config // Deliberate pass-through: the SMTP error text is the admin's own config
// feedback (bad host/auth/TLS). ApiError messages survive formatError; // feedback (bad host/auth/TLS). ApiError messages survive formatError;
// anything not wrapped would be masked as a generic 5xx. // anything not wrapped would be masked as a generic 5xx.
@ -172,7 +171,7 @@ router.post('/test-push', requireAuth, requireUser, async (req: Req, res: Res) =
} }
// Decrypt for sending // Decrypt for sending
const safeDecrypt = (v) => { const safeDecrypt = (v: any) => {
try { try {
return v ? decryptSecret(v) : ''; return v ? decryptSecret(v) : '';
} catch { } catch {
@ -189,7 +188,7 @@ router.post('/test-push', requireAuth, requireUser, async (req: Req, res: Res) =
try { try {
if (!sendTestPush) throw new Error('Push service not initialised'); if (!sendTestPush) throw new Error('Push service not initialised');
await sendTestPush(userForPush); await sendTestPush(userForPush);
} catch (err) { } catch (err: any) {
// Deliberate pass-through: the push-provider error is the user's own // Deliberate pass-through: the push-provider error is the user's own
// channel-config feedback (bad URL/token) — same rationale as /test above. // channel-config feedback (bad URL/token) — same rationale as /test above.
throw new ApiError('NOTIFICATION_TEST_FAILED', err.message || 'Test push failed', 502); throw new ApiError('NOTIFICATION_TEST_FAILED', err.message || 'Test push failed', 502);

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
('use strict'); ('use strict');
@ -35,14 +34,14 @@ function dataImportEnabled() {
return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false'; return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false';
} }
function requireDataImportEnabled(req, res, next) { function requireDataImportEnabled(req: Req, res: Res, next: Next) {
if (!dataImportEnabled()) { if (!dataImportEnabled()) {
throw ForbiddenError('Data import is disabled by DATA_IMPORT_ENABLED=false'); throw ForbiddenError('Data import is disabled by DATA_IMPORT_ENABLED=false');
} }
next(); next();
} }
function profileResponse(user) { function profileResponse(user: any) {
const displayName = user.display_name || null; const displayName = user.display_name || null;
return { return {
id: user.id, id: user.id,
@ -287,10 +286,11 @@ router.patch('/settings', (req: Req, res: Res) => {
: null : null
: current.notification_email; : current.notification_email;
const boolVal = (incoming, fallback) => (incoming !== undefined ? (incoming ? 1 : 0) : fallback); const boolVal = (incoming: any, fallback: any) =>
incoming !== undefined ? (incoming ? 1 : 0) : fallback;
// Encrypt push_url and push_token before storing // Encrypt push_url and push_token before storing
const encryptOrNull = (v, fallback) => { const encryptOrNull = (v: any, fallback: any) => {
if (v === undefined) return fallback; if (v === undefined) return fallback;
if (!v) return null; if (!v) return null;
try { try {

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
('use strict'); ('use strict');
@ -21,7 +20,7 @@ const {
// Throws ValidationError on bad input; service errors propagate to the // Throws ValidationError on bad input; service errors propagate to the
// terminal handler (4xx with .status pass their message through, 5xx are // terminal handler (4xx with .status pass their message through, 5xx are
// masked + logged there — no per-route try/catch, per CODE_STANDARDS.md). // masked + logged there — no per-route try/catch, per CODE_STANDARDS.md).
function parseYM(source) { function parseYM(source: any) {
const now = new Date(); const now = new Date();
const year = parseInt(source.year || now.getFullYear(), 10); const year = parseInt(source.year || now.getFullYear(), 10);
const month = parseInt(source.month || now.getMonth() + 1, 10); const month = parseInt(source.month || now.getMonth() + 1, 10);

View File

@ -1,4 +1,3 @@
// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http'; import type { Req, Res, Next } from '../types/http';
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
@ -22,7 +21,7 @@ try {
pkg = { name: 'bill-tracker', version: '1.0.0' }; pkg = { name: 'bill-tracker', version: '1.0.0' };
} }
function errorMessage(err) { function errorMessage(err: any) {
return err?.message || String(err); return err?.message || String(err);
} }
@ -30,7 +29,7 @@ function errorMessage(err) {
// Without the Z suffix, JS Date parses it as LOCAL time, creating timezone // Without the Z suffix, JS Date parses it as LOCAL time, creating timezone
// inconsistencies when mixed with ISO strings that do have Z. // inconsistencies when mixed with ISO strings that do have Z.
// This ensures all timestamps sent to clients are unambiguous UTC ISO strings. // This ensures all timestamps sent to clients are unambiguous UTC ISO strings.
function toIso(sqliteOrIso) { function toIso(sqliteOrIso: any) {
if (!sqliteOrIso) return null; if (!sqliteOrIso) return null;
const s = String(sqliteOrIso).trim(); const s = String(sqliteOrIso).trim();
// Already ISO with Z or offset — return as-is // Already ISO with Z or offset — return as-is
@ -40,7 +39,7 @@ function toIso(sqliteOrIso) {
return s; return s;
} }
function monthRange(now) { function monthRange(now: any) {
const year = now.getFullYear(); const year = now.getFullYear();
const month = now.getMonth() + 1; const month = now.getMonth() + 1;
const daysInMonth = new Date(year, month, 0).getDate(); const daysInMonth = new Date(year, month, 0).getDate();
@ -84,7 +83,7 @@ router.get('/', async (req: Req, res: Res) => {
last_sent_at: null, last_sent_at: null,
last_error: runtimeState.notifications.last_error, last_error: runtimeState.notifications.last_error,
}; };
let backups = { let backups: any = {
ok: true, ok: true,
enabled: false, enabled: false,
configured: false, configured: false,
@ -271,7 +270,7 @@ router.get('/', async (req: Req, res: Res) => {
} }
// Bank sync (SimpleFIN) status // Bank sync (SimpleFIN) status
let bankSync = { let bankSync: any = {
enabled: false, enabled: false,
running: false, running: false,
source_count: 0, source_count: 0,
@ -351,7 +350,7 @@ router.get('/', async (req: Req, res: Res) => {
} }
// Cleanup status — safe read-only summary from settings, no paths or secrets // Cleanup status — safe read-only summary from settings, no paths or secrets
let cleanup = { ok: true, last_run_at: null, last_result: null }; let cleanup: any = { ok: true, last_run_at: null, last_result: null };
try { try {
const raw = getSetting('cleanup_last_result'); const raw = getSetting('cleanup_last_result');
cleanup = { cleanup = {