feat: auth foundation, similar tools, costs, redundancy, anonymous voting

This commit is contained in:
root
2026-07-29 21:59:29 +02:00
parent 8b3b7c9955
commit 59badeaa48
22 changed files with 793 additions and 15 deletions
+62
View File
@@ -0,0 +1,62 @@
import { Router, type IRouter } from "express";
import { sql } from "drizzle-orm";
import { db, toolsTable, ratingsTable } from "@workspace/db";
import { requireAuth } from "../middleware/auth";
const router: IRouter = Router();
router.get("/admin/redundancy", requireAuth, async (_req, res): Promise<void> => {
const tools = await db.select().from(toolsTable).orderBy(toolsTable.category, toolsTable.name);
const allRatings = await db
.select({ toolId: ratingsTable.toolId, usefulness: ratingsTable.usefulness, usability: ratingsTable.usability })
.from(ratingsTable);
const ratingsByTool = new Map<number, { usefulness: number; usability: number }[]>();
for (const r of allRatings) {
const arr = ratingsByTool.get(r.toolId) ?? [];
arr.push({ usefulness: r.usefulness, usability: r.usability });
ratingsByTool.set(r.toolId, arr);
}
function buildStats(t: typeof toolsTable.$inferSelect) {
const ratings = ratingsByTool.get(t.id) ?? [];
const count = ratings.length;
const avgU = count > 0 ? ratings.reduce((s, r) => s + r.usefulness, 0) / count : null;
const avgUs = count > 0 ? ratings.reduce((s, r) => s + r.usability, 0) / count : null;
return {
id: t.id, name: t.name, category: t.category, features: t.features ?? [], tags: t.tags ?? [],
ratingCount: count,
avgUsefulness: avgU, avgUsability: avgUs,
avgCombined: avgU != null && avgUs != null ? (avgU + avgUs) / 2 : null,
};
}
const grouped: Record<string, any[]> = {};
for (const t of tools) {
const g = grouped[t.category] ?? [];
g.push(buildStats(t));
grouped[t.category] = g;
}
const result = Object.entries(grouped).map(([category, items]) => {
const pairs: { a: any; b: any; overlap: number; scoreDiff: number }[] = [];
for (let i = 0; i < items.length; i++) {
for (let j = i + 1; j < items.length; j++) {
const aFeat = new Set(items[i].features);
const bFeat = new Set(items[j].features);
const shared = [...aFeat].filter((f) => bFeat.has(f)).length;
const total = new Set([...aFeat, ...bFeat]).size;
const overlap = total > 0 ? Math.round((shared / total) * 100) : 0;
const scoreDiff = (items[j].avgCombined ?? 0) - (items[i].avgCombined ?? 0);
pairs.push({ a: items[i], b: items[j], overlap, scoreDiff });
}
}
pairs.sort((a, b) => b.overlap - a.overlap || Math.abs(b.scoreDiff) - Math.abs(a.scoreDiff));
return { category, tools: items, pairs: pairs.slice(0, 5) };
});
res.json(result);
});
export default router;