chore: add prettier with config and format all files

This commit is contained in:
Sebastian Seedorf
2026-06-04 11:44:20 +02:00
parent d212ae3f30
commit cf9bb33ecb
50 changed files with 1290 additions and 714 deletions

View File

@@ -5,7 +5,8 @@ import { withAuth } from '@/lib/apiHelpers';
export const PUT = withAuth(async (req: NextRequest, { params }) => {
const { id } = await params;
const body = await req.json();
const { item_key, item_key_is_regex, combinator, signal_type, condition, threshold, active } = body;
const { item_key, item_key_is_regex, combinator, signal_type, condition, threshold, active } =
body;
const result = await pool.query(
`UPDATE alerts SET
@@ -29,4 +30,4 @@ export const DELETE = withAuth(async (_req: NextRequest, { params }) => {
const result = await pool.query('DELETE FROM alerts WHERE id = $1', [id]);
if (result.rowCount === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json({ ok: true });
});
});

View File

@@ -13,7 +13,10 @@ export const GET = withAuth(async () => {
if (alertsResult.rows.length === 0) return NextResponse.json([]);
const latestResult = await pool.query<{
combinator: string; item_key: string; green: number; red: number;
combinator: string;
item_key: string;
green: number;
red: number;
}>(
`SELECT DISTINCT ON (combinator, item_key) combinator, item_key, green, red
FROM signals
@@ -22,9 +25,7 @@ export const GET = withAuth(async () => {
const localeMap = getServerLocaleMap();
const latestMap = new Map(
latestResult.rows.map(r => [`${r.combinator}::${r.item_key}`, r]),
);
const latestMap = new Map(latestResult.rows.map((r) => [`${r.combinator}::${r.item_key}`, r]));
const triggered: TriggeredAlert[] = [];
@@ -49,14 +50,15 @@ export const GET = withAuth(async () => {
const value = alert.signal_type === 'green' ? vals.green : vals.red;
const fired = alert.condition === 'above' ? value > alert.threshold : value < alert.threshold;
if (fired) triggered.push({
...alert,
current_value: value,
combinator_match: combinator,
matched_item_key: item_key,
});
if (fired)
triggered.push({
...alert,
current_value: value,
combinator_match: combinator,
matched_item_key: item_key,
});
}
}
return NextResponse.json(triggered);
});
});

View File

@@ -10,15 +10,16 @@ export const GET = withAuth(async () => {
export const POST = withAuth(async (req: NextRequest) => {
const body = await req.json();
const {
item_key, item_key_is_regex = false,
combinator = null, signal_type = 'green', condition, threshold,
item_key,
item_key_is_regex = false,
combinator = null,
signal_type = 'green',
condition,
threshold,
} = body;
if (!item_key || !condition || threshold === undefined) {
return NextResponse.json(
{ error: 'item_key, condition, threshold required' },
{ status: 400 },
);
return NextResponse.json({ error: 'item_key, condition, threshold required' }, { status: 400 });
}
const result = await pool.query(
@@ -27,4 +28,4 @@ export const POST = withAuth(async (req: NextRequest) => {
[item_key, item_key_is_regex, combinator, signal_type, condition, threshold],
);
return NextResponse.json(result.rows[0], { status: 201 });
});
});

View File

@@ -6,17 +6,25 @@ export const PUT = withAuth(async (req: NextRequest, { params }) => {
const { id } = await params;
const body = await req.json();
const {
title, pos_x, pos_y, width, height,
signal_type, chart_type, viz_type,
filter_items_regex, y_scale,
series_limit, order_by,
title,
pos_x,
pos_y,
width,
height,
signal_type,
chart_type,
viz_type,
filter_items_regex,
y_scale,
series_limit,
order_by,
} = body;
const hasFilterCombinators = 'filter_combinators' in body;
const hasFilterItems = 'filter_items' in body;
const hasFilterCombinators = 'filter_combinators' in body;
const hasFilterItems = 'filter_items' in body;
const hasFilterItemsExclude = 'filter_items_exclude' in body;
const hasYMin = 'y_min' in body;
const hasYMax = 'y_max' in body;
const hasYMin = 'y_min' in body;
const hasYMax = 'y_max' in body;
const result = await pool.query(
`UPDATE charts SET
@@ -40,15 +48,28 @@ export const PUT = withAuth(async (req: NextRequest, { params }) => {
WHERE id = $23
RETURNING *`,
[
title, pos_x, pos_y, width, height, signal_type, chart_type, viz_type,
hasFilterCombinators, body.filter_combinators ?? null,
hasFilterItems, body.filter_items ?? null,
hasFilterItemsExclude, body.filter_items_exclude ?? null,
title,
pos_x,
pos_y,
width,
height,
signal_type,
chart_type,
viz_type,
hasFilterCombinators,
body.filter_combinators ?? null,
hasFilterItems,
body.filter_items ?? null,
hasFilterItemsExclude,
body.filter_items_exclude ?? null,
filter_items_regex,
hasYMin, body.y_min ?? null,
hasYMax, body.y_max ?? null,
hasYMin,
body.y_min ?? null,
hasYMax,
body.y_max ?? null,
y_scale,
series_limit, order_by,
series_limit,
order_by,
id,
],
);
@@ -61,4 +82,4 @@ export const DELETE = withAuth(async (_req: NextRequest, { params }) => {
const result = await pool.query('DELETE FROM charts WHERE id = $1', [id]);
if (result.rowCount === 0) return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json({ ok: true });
});
});

View File

@@ -11,12 +11,22 @@ export const POST = withAuth(async (req: NextRequest) => {
const body = await req.json();
const {
title,
pos_x = 0, pos_y = 0, width = 2, height = 4,
signal_type = 'both', chart_type = 'signals', viz_type = 'line',
filter_combinators = null, filter_items = null,
filter_items_exclude = null, filter_items_regex = false,
y_min = null, y_max = null, y_scale = 'linear',
series_limit = 20, order_by = 'value_asc',
pos_x = 0,
pos_y = 0,
width = 2,
height = 4,
signal_type = 'both',
chart_type = 'signals',
viz_type = 'line',
filter_combinators = null,
filter_items = null,
filter_items_exclude = null,
filter_items_regex = false,
y_min = null,
y_max = null,
y_scale = 'linear',
series_limit = 20,
order_by = 'value_asc',
} = body;
if (!title) return NextResponse.json({ error: 'title required' }, { status: 400 });
@@ -28,9 +38,25 @@ export const POST = withAuth(async (req: NextRequest) => {
y_min,y_max,y_scale,series_limit,order_by)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
RETURNING *`,
[title,pos_x,pos_y,width,height,signal_type,chart_type,viz_type,
filter_combinators,filter_items,filter_items_exclude,filter_items_regex,
y_min,y_max,y_scale,series_limit,order_by],
[
title,
pos_x,
pos_y,
width,
height,
signal_type,
chart_type,
viz_type,
filter_combinators,
filter_items,
filter_items_exclude,
filter_items_regex,
y_min,
y_max,
y_scale,
series_limit,
order_by,
],
);
return NextResponse.json(result.rows[0], { status: 201 });
});
});

View File

@@ -4,11 +4,11 @@ import { withAuth } from '@/lib/apiHelpers';
interface CircuitNetwork {
green: Record<string, number>;
red: Record<string, number>;
red: Record<string, number>;
}
interface IngestBody {
game_tick: number;
game_tick: number;
circuit_network: CircuitNetwork;
logistic_network: Record<string, number>;
}
@@ -31,10 +31,10 @@ export const POST = withAuth(async (req: NextRequest, { params }) => {
return NextResponse.json({ error: 'Missing game_tick' }, { status: 400 });
}
const green = circuit_network?.green ?? {};
const red = circuit_network?.red ?? {};
const logistic = logistic_network ?? {};
const allKeys = new Set([...Object.keys(green), ...Object.keys(red), ...Object.keys(logistic)]);
const green = circuit_network?.green ?? {};
const red = circuit_network?.red ?? {};
const logistic = logistic_network ?? {};
const allKeys = new Set([...Object.keys(green), ...Object.keys(red), ...Object.keys(logistic)]);
if (allKeys.size === 0) return NextResponse.json({ ok: true, rows: 0 });
@@ -47,7 +47,15 @@ export const POST = withAuth(async (req: NextRequest, { params }) => {
let idx = 1;
for (const key of allKeys) {
placeholders.push(`($${idx++},$${idx++},$${idx++},$${idx++},$${idx++},$${idx++},$${idx++})`);
values.push(realTime, game_tick, combinator, key, green[key] ?? 0, red[key] ?? 0, logistic[key] ?? null);
values.push(
realTime,
game_tick,
combinator,
key,
green[key] ?? 0,
red[key] ?? 0,
logistic[key] ?? null,
);
}
await client.query(
@@ -68,4 +76,4 @@ export const POST = withAuth(async (req: NextRequest, { params }) => {
} finally {
client.release();
}
});
});

View File

@@ -5,7 +5,7 @@ import { withAuth } from '@/lib/apiHelpers';
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 to = p.get('to') ? new Date(p.get('to')!) : new Date();
const boundaries = await getSessionBoundaries(from, to);
return NextResponse.json(boundaries);
});
});

View File

@@ -6,15 +6,15 @@ import { matchKeys } from '@/lib/localization';
export const GET = withAuth(async (req: NextRequest) => {
const p = req.nextUrl.searchParams;
const combinators = p.getAll('combinator');
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 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[] = [];
@@ -28,8 +28,8 @@ export const GET = withAuth(async (req: NextRequest) => {
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 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) {
@@ -46,8 +46,8 @@ export const GET = withAuth(async (req: NextRequest) => {
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 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) {
@@ -61,16 +61,24 @@ export const GET = withAuth(async (req: NextRequest) => {
}
}
if (from) { conditions.push(`real_time >= $${i++}`); values.push(new Date(from)); }
if (to) { conditions.push(`real_time <= $${i++}`); values.push(new Date(to)); }
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 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';
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[] = [];
@@ -84,8 +92,8 @@ export const GET = withAuth(async (req: NextRequest) => {
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 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) {
@@ -101,8 +109,8 @@ export const GET = withAuth(async (req: NextRequest) => {
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 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) {
@@ -116,7 +124,7 @@ export const GET = withAuth(async (req: NextRequest) => {
}
}
const baseWhere = baseConditions.length > 0 ? `WHERE ${baseConditions.join(' AND ')}` : '';
const baseWhere = baseConditions.length > 0 ? `WHERE ${baseConditions.join(' AND ')}` : '';
const baseWhereAnd = baseConditions.length > 0 ? `AND ${baseConditions.join(' AND ')}` : '';
const deltaQuery = `
@@ -158,21 +166,27 @@ export const GET = withAuth(async (req: NextRequest) => {
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 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 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])],
[...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) {
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
@@ -182,23 +196,27 @@ export const GET = withAuth(async (req: NextRequest) => {
);
let sorted = latestVals.rows;
if (orderBy === 'value_asc') sorted = [...sorted].sort((a, b) => a.val - b.val);
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));
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 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 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])],
[...values, ...top.flatMap((r) => [r.combinator, r.item_key])],
);
return NextResponse.json(result.rows);
}
@@ -209,4 +227,4 @@ export const GET = withAuth(async (req: NextRequest) => {
values,
);
return NextResponse.json(result.rows);
});
});

View File

@@ -6,7 +6,7 @@ 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 to = p.get('to') ? new Date(p.get('to')!) : new Date();
const conditions = ['real_time BETWEEN $1 AND $2'];
const values: unknown[] = [from, to];
@@ -17,7 +17,9 @@ export const GET = withAuth(async (req: NextRequest) => {
}
const result = await pool.query<{
real_time: Date; game_tick: string; combinator: string;
real_time: Date;
game_tick: string;
combinator: string;
}>(
`SELECT real_time, game_tick, combinator
FROM tick_timing
@@ -43,17 +45,17 @@ export const GET = withAuth(async (req: NextRequest) => {
const prev = combiRows[i - 1];
const curr = combiRows[i];
const deltaRealMs = curr.real_time.getTime() - prev.real_time.getTime();
const deltaTicks = parseInt(curr.game_tick, 10) - parseInt(prev.game_tick, 10);
const deltaTicks = parseInt(curr.game_tick, 10) - parseInt(prev.game_tick, 10);
// Skip session gaps and bad data
if (deltaRealMs > 30 * 60 * 1000 || deltaRealMs <= 0 || deltaTicks <= 0) continue;
points.push({
real_time: curr.real_time.toISOString(),
game_tick: parseInt(curr.game_tick, 10),
real_time: curr.real_time.toISOString(),
game_tick: parseInt(curr.game_tick, 10),
combinator: combi,
ups: Math.round((deltaTicks / deltaRealMs) * 1000 * 10) / 10,
ups: Math.round((deltaTicks / deltaRealMs) * 1000 * 10) / 10,
});
}
}
return NextResponse.json(points);
});
});

View File

@@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
:root {
--background: #111827;
@@ -15,4 +15,3 @@ body {
color: var(--foreground);
font-family: ui-sans-serif, system-ui, sans-serif;
}

View File

@@ -1,17 +1,15 @@
import type { Metadata } from "next";
import "./globals.css";
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: "Factorio Dashboard",
description: "Factorio signal monitor",
title: 'Factorio Dashboard',
description: 'Factorio signal monitor',
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className="h-full antialiased">
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}
}

View File

@@ -48,8 +48,8 @@ function DashboardApp() {
const searchParams = useSearchParams();
const token = searchParams.get('token') ?? '';
const [ready, setReady] = useState(false);
const [alerts, setAlerts] = useState<AlertConfig[]>([]);
const [ready, setReady] = useState(false);
const [alerts, setAlerts] = useState<AlertConfig[]>([]);
const [localeMap, setLocaleMap] = useState<LocaleMap>(new Map());
useEffect(() => {
@@ -72,9 +72,7 @@ function DashboardApp() {
if (!ready) {
return (
<div className="flex min-h-screen items-center justify-center text-gray-400">
Loading
</div>
<div className="flex min-h-screen items-center justify-center text-gray-400">Loading</div>
);
}
@@ -91,4 +89,4 @@ export default function Page() {
<DashboardApp />
</Suspense>
);
}
}