diff --git a/web/app/api/alerts/[id]/route.ts b/web/app/api/alerts/[id]/route.ts index df342f9..88c8dd5 100644 --- a/web/app/api/alerts/[id]/route.ts +++ b/web/app/api/alerts/[id]/route.ts @@ -1,6 +1,7 @@ -import { NextRequest, NextResponse } from 'next/server'; -import pool from '@/lib/db'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; export const PUT = withAuth(async (req: NextRequest, { params }) => { const { id } = await params; diff --git a/web/app/api/alerts/check/route.ts b/web/app/api/alerts/check/route.ts index e7832a0..1d73643 100644 --- a/web/app/api/alerts/check/route.ts +++ b/web/app/api/alerts/check/route.ts @@ -1,9 +1,11 @@ import { NextResponse } from 'next/server'; -import pool from '@/lib/db'; + +import type { AlertConfig, TriggeredAlert } from '@/lib/types'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; import { getServerLocaleMap } from '@/lib/localeServer'; import { resolveName } from '@/lib/localization'; -import type { AlertConfig, TriggeredAlert } from '@/lib/types'; export const GET = withAuth(async () => { const alertsResult = await pool.query( diff --git a/web/app/api/alerts/route.ts b/web/app/api/alerts/route.ts index 30c0baf..06d6c4f 100644 --- a/web/app/api/alerts/route.ts +++ b/web/app/api/alerts/route.ts @@ -1,6 +1,7 @@ -import { NextRequest, NextResponse } from 'next/server'; -import pool from '@/lib/db'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; export const GET = withAuth(async () => { const result = await pool.query('SELECT * FROM alerts ORDER BY created_at DESC'); diff --git a/web/app/api/charts/[id]/route.ts b/web/app/api/charts/[id]/route.ts index 7b7bd85..b7e2afa 100644 --- a/web/app/api/charts/[id]/route.ts +++ b/web/app/api/charts/[id]/route.ts @@ -1,6 +1,7 @@ -import { NextRequest, NextResponse } from 'next/server'; -import pool from '@/lib/db'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; export const PUT = withAuth(async (req: NextRequest, { params }) => { const { id } = await params; diff --git a/web/app/api/charts/route.ts b/web/app/api/charts/route.ts index 5dbfa2d..36c6a7e 100644 --- a/web/app/api/charts/route.ts +++ b/web/app/api/charts/route.ts @@ -1,6 +1,7 @@ -import { NextRequest, NextResponse } from 'next/server'; -import pool from '@/lib/db'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; export const GET = withAuth(async () => { const result = await pool.query('SELECT * FROM charts ORDER BY pos_y ASC, pos_x ASC'); diff --git a/web/app/api/ingest/[combinator]/route.ts b/web/app/api/ingest/[combinator]/route.ts index 189174a..f5255bb 100644 --- a/web/app/api/ingest/[combinator]/route.ts +++ b/web/app/api/ingest/[combinator]/route.ts @@ -1,6 +1,7 @@ -import { NextRequest, NextResponse } from 'next/server'; -import pool from '@/lib/db'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; interface CircuitNetwork { green: Record; diff --git a/web/app/api/sessions/route.ts b/web/app/api/sessions/route.ts index 75c6f92..fa5f945 100644 --- a/web/app/api/sessions/route.ts +++ b/web/app/api/sessions/route.ts @@ -1,11 +1,14 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getSessionBoundaries } from '@/lib/sessions'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import { getSessionBoundaries } from '@/lib/sessions'; export const GET = withAuth(async (req: NextRequest) => { const p = req.nextUrl.searchParams; - const from = p.get('from') ? new Date(p.get('from')!) : new Date(Date.now() - 86_400_000); - const to = p.get('to') ? new Date(p.get('to')!) : new Date(); + const fromVal = p.get('from'); + const toVal = p.get('to'); + const from = fromVal ? new Date(fromVal) : new Date(Date.now() - 86_400_000); + const to = toVal ? new Date(toVal) : new Date(); const boundaries = await getSessionBoundaries(from, to); return NextResponse.json(boundaries); }); diff --git a/web/app/api/signals/route.ts b/web/app/api/signals/route.ts index 1ad11a0..4ac7ed8 100644 --- a/web/app/api/signals/route.ts +++ b/web/app/api/signals/route.ts @@ -1,8 +1,7 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from 'next/server'; + +import { withAuth, buildItemFilter } from '@/lib/apiHelpers'; 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; @@ -14,59 +13,30 @@ export const GET = withAuth(async (req: NextRequest) => { 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 limitStr = p.get('limit'); + const limit = limitStr ? parseInt(limitStr, 10) : null; const conditions: string[] = []; const values: unknown[] = []; - let i = 1; + const param = { current: 1 }; if (combinators.length > 0) { - conditions.push(`combinator = ANY($${i++})`); + conditions.push(`combinator = ANY($${param.current++})`); values.push(combinators); } - if (itemsWhitelist.length > 0) { - if (useRegex) { - const localeMap = getServerLocaleMap(); - const localeKeys = [...new Set(itemsWhitelist.flatMap((p) => matchKeys(p, localeMap)))]; - const sqlPattern = itemsWhitelist.map((p) => `(${p})`).join('|'); - const orConds = [`item_key ~* $${i++}`]; - values.push(sqlPattern); - if (localeKeys.length > 0) { - orConds.push(`item_key = ANY($${i++})`); - values.push(localeKeys); - } - conditions.push(`(${orConds.join(' OR ')})`); - } else { - conditions.push(`item_key = ANY($${i++})`); - values.push(itemsWhitelist); - } - } + const wlClause = buildItemFilter(itemsWhitelist, useRegex, true, values, param); + if (wlClause) conditions.push(wlClause); - if (itemsBlacklist.length > 0) { - if (useRegex) { - const localeMap = getServerLocaleMap(); - const localeKeys = [...new Set(itemsBlacklist.flatMap((p) => matchKeys(p, localeMap)))]; - const sqlPattern = itemsBlacklist.map((p) => `(${p})`).join('|'); - const andConds = [`item_key !~* $${i++}`]; - values.push(sqlPattern); - if (localeKeys.length > 0) { - andConds.push(`item_key != ALL($${i++})`); - values.push(localeKeys); - } - conditions.push(`(${andConds.join(' AND ')})`); - } else { - conditions.push(`item_key != ALL($${i++})`); - values.push(itemsBlacklist); - } - } + const blClause = buildItemFilter(itemsBlacklist, useRegex, false, values, param); + if (blClause) conditions.push(blClause); if (from) { - conditions.push(`real_time >= $${i++}`); + conditions.push(`real_time >= $${param.current++}`); values.push(new Date(from)); } if (to) { - conditions.push(`real_time <= $${i++}`); + conditions.push(`real_time <= $${param.current++}`); values.push(new Date(to)); } @@ -83,46 +53,18 @@ export const GET = withAuth(async (req: NextRequest) => { if ((orderBy === 'delta_asc' || orderBy === 'delta_desc') && limit !== null) { const baseConditions: string[] = []; const baseValues: unknown[] = []; - let j = 1; + const baseParam = { current: 1 }; if (combinators.length > 0) { - baseConditions.push(`combinator = ANY($${j++})`); + baseConditions.push(`combinator = ANY($${baseParam.current++})`); baseValues.push(combinators); } - if (itemsWhitelist.length > 0) { - if (useRegex) { - const localeMap = getServerLocaleMap(); - const localeKeys = [...new Set(itemsWhitelist.flatMap((p) => matchKeys(p, localeMap)))]; - const sqlPattern = itemsWhitelist.map((p) => `(${p})`).join('|'); - const orConds = [`item_key ~* $${j++}`]; - baseValues.push(sqlPattern); - if (localeKeys.length > 0) { - orConds.push(`item_key = ANY($${j++})`); - baseValues.push(localeKeys); - } - baseConditions.push(`(${orConds.join(' OR ')})`); - } else { - baseConditions.push(`item_key = ANY($${j++})`); - baseValues.push(itemsWhitelist); - } - } - if (itemsBlacklist.length > 0) { - if (useRegex) { - const localeMap = getServerLocaleMap(); - const localeKeys = [...new Set(itemsBlacklist.flatMap((p) => matchKeys(p, localeMap)))]; - const sqlPattern = itemsBlacklist.map((p) => `(${p})`).join('|'); - const andConds = [`item_key !~* $${j++}`]; - baseValues.push(sqlPattern); - if (localeKeys.length > 0) { - andConds.push(`item_key != ALL($${j++})`); - baseValues.push(localeKeys); - } - baseConditions.push(`(${andConds.join(' AND ')})`); - } else { - baseConditions.push(`item_key != ALL($${j++})`); - baseValues.push(itemsBlacklist); - } - } + + const wlBase = buildItemFilter(itemsWhitelist, useRegex, true, baseValues, baseParam); + if (wlBase) baseConditions.push(wlBase); + + const blBase = buildItemFilter(itemsBlacklist, useRegex, false, baseValues, baseParam); + if (blBase) baseConditions.push(blBase); const baseWhere = baseConditions.length > 0 ? `WHERE ${baseConditions.join(' AND ')}` : ''; const baseWhereAnd = baseConditions.length > 0 ? `AND ${baseConditions.join(' AND ')}` : ''; @@ -155,25 +97,27 @@ export const GET = withAuth(async (req: NextRequest) => { SELECT combinator, item_key, delta FROM deltas ORDER BY delta ${orderBy === 'delta_asc' ? 'ASC' : 'DESC'} - LIMIT $${j} + LIMIT $${baseParam.current} `; - const deltaResult = await pool.query<{ combinator: string; item_key: string; delta: number }>( - deltaQuery, - [...baseValues, limit], - ); + 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})`, + (_, idx) => + `(combinator = $${param.current + idx * 2} AND item_key = $${param.current + 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}`, + `WHEN combinator = $${param.current + idx * 2} AND item_key = $${param.current + idx * 2 + 1} THEN ${idx}`, ) .join(' '); const result = await pool.query( @@ -187,7 +131,11 @@ export const GET = withAuth(async (req: NextRequest) => { (orderBy === 'value_asc' || orderBy === 'value_desc' || orderBy === 'abs_desc') && limit !== null ) { - const latestVals = await pool.query<{ combinator: string; item_key: string; val: number }>( + 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} @@ -205,13 +153,14 @@ export const GET = withAuth(async (req: NextRequest) => { if (top.length === 0) return NextResponse.json([]); const seriesConditions = top.map( - (_, idx) => `(combinator = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`, + (_, idx) => + `(combinator = $${param.current + idx * 2} AND item_key = $${param.current + 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}`, + `WHEN combinator = $${param.current + idx * 2} AND item_key = $${param.current + idx * 2 + 1} THEN ${idx}`, ) .join(' '); const result = await pool.query( diff --git a/web/app/api/ups/route.ts b/web/app/api/ups/route.ts index f935cf4..4007620 100644 --- a/web/app/api/ups/route.ts +++ b/web/app/api/ups/route.ts @@ -1,12 +1,15 @@ -import { NextRequest, NextResponse } from 'next/server'; -import pool from '@/lib/db'; +import { type NextRequest, NextResponse } from 'next/server'; + import { withAuth } from '@/lib/apiHelpers'; +import pool from '@/lib/db'; export const GET = withAuth(async (req: NextRequest) => { const p = req.nextUrl.searchParams; const combinator = p.get('combinator'); - const from = p.get('from') ? new Date(p.get('from')!) : new Date(Date.now() - 86_400_000); - const to = p.get('to') ? new Date(p.get('to')!) : new Date(); + const fromVal = p.get('from'); + const toVal = p.get('to'); + const from = fromVal ? new Date(fromVal) : new Date(Date.now() - 86_400_000); + const to = toVal ? new Date(toVal) : new Date(); const conditions = ['real_time BETWEEN $1 AND $2']; const values: unknown[] = [from, to]; diff --git a/web/app/page.tsx b/web/app/page.tsx index 3bfbccc..79c3d6d 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -1,15 +1,17 @@ 'use client'; -import { useEffect, useState, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; -import { AppProvider, useApp } from '@/lib/context'; -import { setToken, fetchAlerts } from '@/lib/api'; -import { getLocaleMap } from '@/lib/localization'; -import type { AlertConfig } from '@/lib/types'; +import { useEffect, useState, Suspense } from 'react'; + import type { LocaleMap } from '@/lib/localization'; -import TimeRangeSelector from '@/components/TimeRangeSelector'; +import type { AlertConfig } from '@/lib/types'; + import AlertPanel from '@/components/AlertPanel'; import Dashboard from '@/components/Dashboard'; +import TimeRangeSelector from '@/components/TimeRangeSelector'; +import { setToken, fetchAlerts } from '@/lib/api'; +import { AppProvider, useApp } from '@/lib/context'; +import { getLocaleMap } from '@/lib/localization'; function AppShell({ alerts }: { alerts: AlertConfig[] }) { const { triggeredAlerts } = useApp(); diff --git a/web/bin/fix-colors.ts b/web/bin/fix-colors.ts index 4f5b243..2b6f99f 100644 --- a/web/bin/fix-colors.ts +++ b/web/bin/fix-colors.ts @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'fs'; const CSV = 'public/factorio_item_colors.csv'; function hexToHsl(h: string): [number, number, number] { - let r = parseInt(h.slice(1, 3), 16) / 255, + const r = parseInt(h.slice(1, 3), 16) / 255, g = parseInt(h.slice(3, 5), 16) / 255, b = parseInt(h.slice(5, 7), 16) / 255; const mx = Math.max(r, g, b), @@ -11,7 +11,7 @@ function hexToHsl(h: string): [number, number, number] { if (mx === mn) return [0, 0, Math.round(l * 100)]; const d = mx - mn, s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn); - let hue = 0; + let hue: number; if (mx === r) hue = ((g - b) / d + (g < b ? 6 : 0)) * 60; else if (mx === g) hue = ((b - r) / d + 2) * 60; else hue = ((r - g) / d + 4) * 60; @@ -61,7 +61,14 @@ function hueDist(a: number, b: number): number { return Math.min(d, 360 - d); } -const lines = readFileSync(CSV, 'utf-8').trim().split('\n'); +let rawText: string; +try { + rawText = readFileSync(CSV, 'utf-8'); +} catch (e) { + console.error('Failed to read CSV:', e); + process.exit(1); +} +const lines = rawText.trim().split('\n'); const header = lines[0]; const raw = lines .slice(1) @@ -108,8 +115,9 @@ for (let i = 0; i < n; i++) { const groups = new Map(); for (let i = 0; i < n; i++) { const root = find(i); - if (!groups.has(root)) groups.set(root, []); - groups.get(root)!.push(hsl[i]); + const group = groups.get(root) ?? []; + if (!groups.has(root)) groups.set(root, group); + group.push(hsl[i]); } let fixed = 0; diff --git a/web/components/AlertPanel.tsx b/web/components/AlertPanel.tsx index 063e948..0c6386b 100644 --- a/web/components/AlertPanel.tsx +++ b/web/components/AlertPanel.tsx @@ -1,11 +1,13 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; -import { useApp } from '@/lib/context'; -import { fetchAlerts, createAlert, updateAlert, deleteAlert } from '@/lib/api'; -import { resolveName, resolveKey } from '@/lib/localization'; + import type { AlertConfig } from '@/lib/types'; +import { fetchAlerts, createAlert, updateAlert, deleteAlert } from '@/lib/api'; +import { useApp } from '@/lib/context'; +import { resolveName, resolveKey } from '@/lib/localization'; + interface Props { open: boolean; onClose: () => void; @@ -121,6 +123,7 @@ function AlertForm({ />
{onCancel && (
@@ -288,12 +296,14 @@ export default function AlertPanel({ open, onClose }: Props) {