fix: resolve all 32 ESLint errors across codebase

- Replace `any` types with proper interfaces (QuestDetailResponse, etc.)
- Remove unused imports (createUser, findUserByTelegramId, updatePointContent)
- Use `unknown` in catch blocks with instanceof narrowing
- Type Express handlers with Request/Response/NextFunction
- Add argsIgnorePattern for underscore-prefixed params in ESLint config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
laruevin 2026-02-11 12:27:42 +07:00
parent 9960f0cc78
commit c61e6ad9cd
11 changed files with 46 additions and 30 deletions

View File

@ -8,7 +8,7 @@ import { t, getLang } from './i18n/index.js'
// Helper: build reply_markup for opening the Mini App
// Telegram requires HTTPS for both web_app and url buttons.
// In dev (HTTP), we skip the button entirely and append the link to the message text.
function questMarkup(label: string): { inline_keyboard: any[][] } | undefined {
function questMarkup(label: string): { inline_keyboard: Record<string, unknown>[][] } | undefined {
const appUrl = process.env.APP_URL || 'https://guidly.example.com'
if (appUrl.startsWith('https://')) {
return { inline_keyboard: [[ { text: label, web_app: { url: appUrl } } ]] }

View File

@ -2,7 +2,7 @@ import type { Conversation } from '@grammyjs/conversations'
import type { BotContext } from '../types.js'
type BotConversation = Conversation<BotContext, BotContext>
import { getActiveCities, createUser, findUserByTelegramId } from '../../api/_lib/db.js'
import { getActiveCities, findUserByTelegramId } from '../../api/_lib/db.js'
import { t, getLang } from '../i18n/index.js'
import { createQuestFromOnboarding } from '../services/quest.service.js'
@ -25,7 +25,7 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
// Step 1: City selection
const cities = await conversation.external(() => getActiveCities())
const cityButtons = cities.map((city: any) => ([
const cityButtons = cities.map((city: { id: string; name: string }) => ([
{ text: city.name, callback_data: `city_${city.id}` }
]))
@ -35,7 +35,7 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
const cityResponse = await conversation.waitForCallbackQuery(/^city_/)
const cityId = cityResponse.callbackQuery!.data!.replace('city_', '')
const cityName = cities.find((c: any) => c.id === cityId)?.name || 'Unknown'
const cityName = cities.find((c: { id: string }) => c.id === cityId)?.name || 'Unknown'
await cityResponse.answerCallbackQuery()
// Step 2: Number of days

View File

@ -2,9 +2,8 @@ import type { Bot } from 'grammy'
import type { BotContext } from '../types.js'
import {
getUnprocessedEvents, markEventProcessed,
getPointById, findUserByTelegramId,
getPointById,
getActiveQuestForUser, getActiveDayForQuest,
updatePointContent,
} from '../../api/_lib/db.js'
import { aiService } from './ai.service.js'
import { generateNextPoint, generatePointContent } from './quest.service.js'
@ -47,7 +46,7 @@ async function processEvents(bot: Bot<BotContext>) {
}
}
async function processEvent(bot: Bot<BotContext>, event: any) {
async function processEvent(bot: Bot<BotContext>, event: Record<string, unknown>) {
// Look up the user to get their telegram_id for sending messages
const { pool } = await import('../../api/_lib/db.js')
const { rows: users } = await pool.query('SELECT * FROM users WHERE id = $1', [event.user_id])
@ -97,7 +96,7 @@ async function processEvent(bot: Bot<BotContext>, event: any) {
const day = await getActiveDayForQuest(quest.id)
if (!day) break
const result = await generateNextPoint(quest.id, {
await generateNextPoint(quest.id, {
city: quest.city_name,
dayNumber: day.day_number,
totalDays: quest.number_of_days,

View File

@ -104,8 +104,8 @@ export async function generateNextPoint(questId: string, questContext: {
if (!day) return null
const existingPoints = await getPointsForDay(day.id)
const previousPoints = existingPoints.map((p: any) => p.title)
const maxOrder = existingPoints.reduce((max: number, p: any) => Math.max(max, p.order_in_day || 0), 0)
const previousPoints = existingPoints.map((p: { title: string }) => p.title)
const maxOrder = existingPoints.reduce((max: number, p: { order_in_day?: number }) => Math.max(max, p.order_in_day || 0), 0)
const nextOrder = maxOrder + 1
const point = await aiService.generatePoint({

View File

@ -23,6 +23,10 @@ export default tseslint.config(
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
},
},
)

View File

@ -50,8 +50,8 @@ async function migrate() {
await pool.query(sql)
await pool.query('INSERT INTO _migrations (name) VALUES ($1)', [file])
console.log(` [ok] ${file}`)
} catch (error: any) {
console.error(` [fail] ${file}:`, error.message)
} catch (error: unknown) {
console.error(` [fail] ${file}:`, error instanceof Error ? error.message : error)
process.exit(1)
}
}

View File

@ -1,5 +1,5 @@
import 'dotenv/config'
import express from 'express'
import express, { type Request, type Response, type NextFunction } from 'express'
import cookieParser from 'cookie-parser'
import path from 'path'
import { fileURLToPath } from 'url'
@ -15,8 +15,8 @@ app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
// Wrap handler to catch errors and merge params
function wrapHandler(handler: Function) {
return async (req: any, res: any, next: any) => {
function wrapHandler(handler: (req: Request, res: Response) => Promise<void>) {
return async (req: Request, res: Response, next: NextFunction) => {
const mergedQuery = { ...req.query, ...req.params }
Object.defineProperty(req, 'query', {
value: mergedQuery,
@ -88,7 +88,7 @@ async function setupRoutes() {
app.use(express.static(distPath))
// SPA fallback
app.use((req: any, res: any, next: any) => {
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.path.startsWith('/api/')) {
return next()
}
@ -97,7 +97,7 @@ async function setupRoutes() {
}
// Error handler
app.use((err: any, _req: any, res: any, _next: any) => {
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
console.error('Server error:', err)
res.status(500).json({ error: 'Internal server error' })
})

View File

@ -4,13 +4,13 @@ import styles from './HistoryPage.module.css'
import { fetchQuestHistory, fetchQuestDetail } from '@/utils/api'
import { Loader, ErrorState } from '@/components/ui'
import { getLanguage } from '@/utils/telegram'
import type { QuestHistoryItem } from '@/types'
import type { QuestHistoryItem, QuestDetailResponse, QuestDetailDay, QuestPoint } from '@/types'
export function HistoryPage() {
const { questId } = useParams()
const lang = getLanguage()
const [quests, setQuests] = useState<QuestHistoryItem[]>([])
const [detail, setDetail] = useState<any>(null)
const [detail, setDetail] = useState<QuestDetailResponse | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -25,8 +25,8 @@ export function HistoryPage() {
const data = await fetchQuestHistory()
setQuests(data.quests)
}
} catch (err: any) {
setError(err.message)
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setIsLoading(false)
}
@ -50,13 +50,13 @@ export function HistoryPage() {
{detail.quest.city_name}, {detail.quest.city_country}
</p>
{detail.days.map((day: any) => (
{detail.days.map((day: QuestDetailDay) => (
<div key={day.id} className={styles.daySection}>
<h3 className={styles.dayTitle}>
{lang === 'ru' ? `День ${day.day_number}` : `Day ${day.day_number}`}
{day.theme && `${day.theme}`}
</h3>
{day.points.map((point: any) => (
{day.points.map((point: QuestPoint) => (
<div key={point.id} className={styles.pointCard}>
<div className={styles.pointStatus}>
{point.status === 'completed' ? 'V' : point.status === 'skipped' ? '-' : 'o'}

View File

@ -27,11 +27,12 @@ export const useQuestStore = create<QuestStore>((set, get) => ({
const state = await fetchCurrentQuest()
const showContent = state.point?.arrived_at !== null && state.point?.content_text !== null
set({ state, isLoading: false, showContent })
} catch (error: any) {
if (error.message?.includes('NO_ACTIVE_QUEST') || error.message?.includes('User not found')) {
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error'
if (message.includes('NO_ACTIVE_QUEST') || message.includes('User not found')) {
set({ state: null, isLoading: false, error: null })
} else {
set({ isLoading: false, error: error.message })
set({ isLoading: false, error: message })
}
}
},
@ -72,7 +73,7 @@ export const useQuestStore = create<QuestStore>((set, get) => ({
}
return response.nextAction
} catch (error: any) {
} catch (error: unknown) {
console.error('Failed to send event:', error)
return 'wait'
}

View File

@ -49,6 +49,18 @@ export interface EventResponse {
nextAction: 'wait' | 'show_content' | 'next_point' | 'day_complete' | 'quest_complete' | 'close_app'
}
export interface QuestDetailDay {
id: string
day_number: number
theme: string | null
points: QuestPoint[]
}
export interface QuestDetailResponse {
quest: QuestInfo & { city_country: string }
days: QuestDetailDay[]
}
export interface QuestHistoryItem {
id: string
title: string | null

View File

@ -1,5 +1,5 @@
import { getTelegramInitData } from './telegram'
import type { QuestState, EventResponse } from '../types'
import type { QuestState, EventResponse, QuestHistoryItem, QuestDetailResponse } from '../types'
const API_BASE = '/api'
@ -40,9 +40,9 @@ export async function sendQuestEvent(data: {
}
export async function fetchQuestHistory() {
return apiFetch<{ quests: any[] }>('/quest/history')
return apiFetch<{ quests: QuestHistoryItem[] }>('/quest/history')
}
export async function fetchQuestDetail(questId: string) {
return apiFetch<{ quest: any; days: any[] }>(`/quest/history/${questId}`)
return apiFetch<QuestDetailResponse>(`/quest/history/${questId}`)
}