import { NextRequest, NextResponse } from 'next/server'; import pool from '@/lib/db'; import { withAuth } from '@/lib/apiHelpers'; import { getServerLocaleMap } from '@/lib/localeServer'; import { matchKeys } from '@/lib/localization'; export const GET = withAuth(async (req: NextRequest) => { const p = req.nextUrl.searchParams; const combinators = p.getAll('combinator'); const itemsWhitelist = p.getAll('item'); const itemsBlacklist = p.getAll('exclude'); const signalType = p.get('signal') ?? 'both'; const from = p.get('from'); const to = p.get('to'); const useRegex = p.get('regex') === 'true'; const orderBy = p.get('order_by') ?? 'value_asc'; const limit = p.get('limit') ? parseInt(p.get('limit')!, 10) : null; const conditions: string[] = []; const values: unknown[] = []; let i = 1; if (combinators.length > 0) { conditions.push(`combinator = ANY($${i++})`); values.push(combinators); } if (itemsWhitelist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); // Each pattern is expanded to matching keys (tested against key AND localized name). // Union all patterns — if a pattern matches nothing, it contributes no keys. const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))]; if (keys.length === 0) return NextResponse.json([]); conditions.push(`item_key = ANY($${i++})`); values.push(keys); } else { conditions.push(`item_key = ANY($${i++})`); values.push(itemsWhitelist); } } if (itemsBlacklist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))]; // If blacklist pattern matches nothing, nothing to exclude — skip condition if (keys.length > 0) { conditions.push(`item_key != ALL($${i++})`); values.push(keys); } } else { conditions.push(`item_key != ALL($${i++})`); values.push(itemsBlacklist); } } if (from) { conditions.push(`real_time >= $${i++}`); values.push(new Date(from)); } if (to) { conditions.push(`real_time <= $${i++}`); values.push(new Date(to)); } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const valueCol = signalType === 'red' ? 'red' : 'green'; const selectCols = signalType === 'green' ? 'real_time, game_tick, combinator, item_key, green' : signalType === 'red' ? 'real_time, game_tick, combinator, item_key, red' : 'real_time, game_tick, combinator, item_key, green, red'; if ((orderBy === 'delta_asc' || orderBy === 'delta_desc') && limit !== null) { const baseConditions: string[] = []; const baseValues: unknown[] = []; let j = 1; if (combinators.length > 0) { baseConditions.push(`combinator = ANY($${j++})`); baseValues.push(combinators); } if (itemsWhitelist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))]; if (keys.length === 0) return NextResponse.json([]); baseConditions.push(`item_key = ANY($${j++})`); baseValues.push(keys); } else { baseConditions.push(`item_key = ANY($${j++})`); baseValues.push(itemsWhitelist); } } if (itemsBlacklist.length > 0) { if (useRegex) { const localeMap = getServerLocaleMap(); const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))]; if (keys.length > 0) { baseConditions.push(`item_key != ALL($${j++})`); baseValues.push(keys); } } else { baseConditions.push(`item_key != ALL($${j++})`); baseValues.push(itemsBlacklist); } } const baseWhere = baseConditions.length > 0 ? `WHERE ${baseConditions.join(' AND ')}` : ''; const baseWhereAnd = baseConditions.length > 0 ? `AND ${baseConditions.join(' AND ')}` : ''; const deltaQuery = ` WITH snap_now AS ( SELECT DISTINCT ON (combinator, item_key) combinator, item_key, ${valueCol} AS val, real_time AS ref_time FROM signals ${baseWhere} ORDER BY combinator, item_key, real_time DESC ), snap_then AS ( SELECT DISTINCT ON (s.combinator, s.item_key) s.combinator, s.item_key, s.${valueCol} AS val FROM signals s JOIN snap_now n USING (combinator, item_key) WHERE s.real_time <= n.ref_time - INTERVAL '10 minutes' ${baseWhereAnd} ORDER BY s.combinator, s.item_key, s.real_time DESC ), deltas AS ( SELECT snap_now.combinator, snap_now.item_key, (snap_now.val - COALESCE(snap_then.val, snap_now.val)) AS delta FROM snap_now LEFT JOIN snap_then USING (combinator, item_key) ) SELECT combinator, item_key, delta FROM deltas ORDER BY delta ${orderBy === 'delta_asc' ? 'ASC' : 'DESC'} LIMIT $${j} `; const deltaResult = await pool.query<{ combinator: string; item_key: string; delta: number }>( deltaQuery, [...baseValues, limit], ); const top = deltaResult.rows; if (top.length === 0) return NextResponse.json([]); const seriesConditions = top.map((_, idx) => `(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`, ); const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`; const orderCase = top.map((_, idx) => `WHEN combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1} THEN ${idx}`, ).join(' '); const result = await pool.query( `SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY CASE ${orderCase} ELSE ${top.length} END, real_time ASC`, [...values, ...top.flatMap(r => [r.combinator, r.item_key])], ); return NextResponse.json(result.rows); } if ((orderBy === 'value_asc' || orderBy === 'value_desc' || orderBy === 'abs_desc') && limit !== null) { const latestVals = await pool.query<{ combinator: string; item_key: string; val: number }>( `SELECT DISTINCT ON (combinator, item_key) combinator, item_key, ${valueCol} AS val FROM signals ${where} ORDER BY combinator, item_key, real_time DESC`, values, ); let sorted = latestVals.rows; if (orderBy === 'value_asc') sorted = [...sorted].sort((a, b) => a.val - b.val); if (orderBy === 'value_desc') sorted = [...sorted].sort((a, b) => b.val - a.val); if (orderBy === 'abs_desc') sorted = [...sorted].sort((a, b) => Math.abs(b.val) - Math.abs(a.val)); const top = sorted.slice(0, limit); if (top.length === 0) return NextResponse.json([]); const seriesConditions = top.map((_, idx) => `(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`, ); const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`; const orderCase = top.map((_, idx) => `WHEN combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1} THEN ${idx}`, ).join(' '); const result = await pool.query( `SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY CASE ${orderCase} ELSE ${top.length} END, real_time ASC`, [...values, ...top.flatMap(r => [r.combinator, r.item_key])], ); return NextResponse.json(result.rows); } const rowLimit = orderBy === 'time' && limit ? `LIMIT ${limit}` : ''; const result = await pool.query( `SELECT ${selectCols} FROM signals ${where} ORDER BY real_time ASC ${rowLimit}`, values, ); return NextResponse.json(result.rows); });