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';
const express = require('express');
const router = express.Router();
let _appVersion;
let _appVersion: string | undefined;
function getAppVersion() {
if (!_appVersion) {
try {
@ -196,7 +195,7 @@ router.get('/login-history', requireAuth, (req: Req, res: Res) => {
)
.all(req.user.id);
const safeDecrypt = (v) => {
const safeDecrypt = (v: any) => {
if (!v) return null;
try {
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)
: null;
const history = rows.map((r) => ({
const history = rows.map((r: any) => ({
id: r.id,
logged_in_at: r.logged_in_at,
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';
const express = require('express');
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
// 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);
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
`);
const shaped = categories.map((category) => {
const bills = billsByCategory.all(req.user.id, category.id).map((bill) => ({
const shaped = categories.map((category: any) => {
const bills = billsByCategory.all(req.user.id, category.id).map((bill: any) => ({
...bill,
active: !!bill.active,
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,
}));
const activeBillCount = bills.filter((bill) => bill.active).length;
const activeBillCount = bills.filter((bill: any) => bill.active).length;
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 {
...category,
@ -77,7 +76,7 @@ router.get('/', (req: Req, res: Res) => {
active_bill_count: activeBillCount,
inactive_bill_count: inactiveBillCount,
payment_count: paymentCount,
bill_names: bills.map((bill) => bill.name),
bill_names: bills.map((bill: any) => bill.name),
bills,
};
});
@ -129,7 +128,7 @@ router.put('/reorder', (req: Req, res: Res) => {
const update = db.prepare(
"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);
})(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';
('use strict');
@ -22,14 +21,14 @@ const { syncLimiter } = require('../middleware/rateLimiter.cts');
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
const VALID_STATUSES = new Set(['active', 'inactive', 'error']);
function cleanFilter(value) {
function cleanFilter(value: any) {
return typeof value === 'string' ? value.trim() : '';
}
// SimpleFIN/service errors are deliberately surfaced to the user — but only
// after sanitizeErrorMessage strips tokens/URLs. Wrapping in ApiError keeps
// 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 status = typeof err?.status === 'number' ? err.status : 500;
return new ApiError(err?.code || code, msg, status);
@ -63,7 +62,7 @@ router.get('/accounts/all', (req: Req, res: Res) => {
.all(req.user.id);
res.json(
accounts.map((a) => ({
accounts.map((a: any) => ({
...a,
monitored: a.monitored === 1,
balance_dollars: a.balance !== null ? a.balance / 100 : null,
@ -211,7 +210,7 @@ router.get('/:sourceId/accounts', (req: Req, res: Res) => {
LIMIT 50
`);
const result = accounts.map((acc) => ({
const result = accounts.map((acc: any) => ({
...acc,
monitored: acc.monitored === 1,
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;
for (const name of result.matched_bills ?? []) matchedBillSet.add(name);
for (const attr of result.late_attributions ?? []) lateAttrAll.push(attr);
} catch (err) {
} catch (err: any) {
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';
('use strict');
@ -25,7 +24,7 @@ function dataImportEnabled() {
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()) {
return res
.status(403)
@ -38,7 +37,7 @@ function makeErrorId() {
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
// an explicit 5xx status must fall through to the masked error-id branch.
if (err.status && err.status < 500) {
@ -423,7 +422,7 @@ router.get('/history', requireDataImportEnabled, (req: Req, res: Res) => {
try {
const history = getImportHistory(req.user.id);
res.json({ history });
} catch (err) {
} catch (err: any) {
log.error('[import] history error:', err.message);
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';
const express = require('express');
const { log } = require('../utils/logger.cts');
@ -29,7 +28,7 @@ router.get('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => {
'notify_global_recipient',
'reminder_hour',
];
const settings = {};
const settings: Record<string, string> = {};
for (const k of keys) settings[k] = getSetting(k) || '';
if (!settings.reminder_hour) settings.reminder_hour = '6';
// Mask password in response
@ -65,7 +64,7 @@ router.put('/admin', requireAuth, requireAdmin, (req: Req, res: Res) => {
setSetting('reminder_hour', String(hour));
try {
require('../workers/dailyWorker.cts').rescheduleDailyWorker();
} catch (err) {
} catch (err: any) {
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');
try {
await sendTestEmail(to);
} catch (err) {
} catch (err: any) {
// Deliberate pass-through: the SMTP error text is the admin's own config
// feedback (bad host/auth/TLS). ApiError messages survive formatError;
// 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
const safeDecrypt = (v) => {
const safeDecrypt = (v: any) => {
try {
return v ? decryptSecret(v) : '';
} catch {
@ -189,7 +188,7 @@ router.post('/test-push', requireAuth, requireUser, async (req: Req, res: Res) =
try {
if (!sendTestPush) throw new Error('Push service not initialised');
await sendTestPush(userForPush);
} catch (err) {
} catch (err: any) {
// Deliberate pass-through: the push-provider error is the user's own
// channel-config feedback (bad URL/token) — same rationale as /test above.
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';
('use strict');
@ -35,14 +34,14 @@ function dataImportEnabled() {
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()) {
throw ForbiddenError('Data import is disabled by DATA_IMPORT_ENABLED=false');
}
next();
}
function profileResponse(user) {
function profileResponse(user: any) {
const displayName = user.display_name || null;
return {
id: user.id,
@ -287,10 +286,11 @@ router.patch('/settings', (req: Req, res: Res) => {
: null
: 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
const encryptOrNull = (v, fallback) => {
const encryptOrNull = (v: any, fallback: any) => {
if (v === undefined) return fallback;
if (!v) return null;
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';
('use strict');
@ -21,7 +20,7 @@ const {
// Throws ValidationError on bad input; service errors propagate to the
// terminal handler (4xx with .status pass their message through, 5xx are
// masked + logged there — no per-route try/catch, per CODE_STANDARDS.md).
function parseYM(source) {
function parseYM(source: any) {
const now = new Date();
const year = parseInt(source.year || now.getFullYear(), 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';
const express = require('express');
const router = express.Router();
@ -22,7 +21,7 @@ try {
pkg = { name: 'bill-tracker', version: '1.0.0' };
}
function errorMessage(err) {
function errorMessage(err: any) {
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
// inconsistencies when mixed with ISO strings that do have Z.
// This ensures all timestamps sent to clients are unambiguous UTC ISO strings.
function toIso(sqliteOrIso) {
function toIso(sqliteOrIso: any) {
if (!sqliteOrIso) return null;
const s = String(sqliteOrIso).trim();
// Already ISO with Z or offset — return as-is
@ -40,7 +39,7 @@ function toIso(sqliteOrIso) {
return s;
}
function monthRange(now) {
function monthRange(now: any) {
const year = now.getFullYear();
const month = now.getMonth() + 1;
const daysInMonth = new Date(year, month, 0).getDate();
@ -84,7 +83,7 @@ router.get('/', async (req: Req, res: Res) => {
last_sent_at: null,
last_error: runtimeState.notifications.last_error,
};
let backups = {
let backups: any = {
ok: true,
enabled: false,
configured: false,
@ -271,7 +270,7 @@ router.get('/', async (req: Req, res: Res) => {
}
// Bank sync (SimpleFIN) status
let bankSync = {
let bankSync: any = {
enabled: false,
running: false,
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
let cleanup = { ok: true, last_run_at: null, last_result: null };
let cleanup: any = { ok: true, last_run_at: null, last_result: null };
try {
const raw = getSetting('cleanup_last_result');
cleanup = {