Remove obsolete project artifacts
Some checks failed
Verify and publish container / build-and-push (push) Failing after 33s

This commit is contained in:
Elijah 2026-08-08 18:50:16 -07:00
parent 5db7fc8afd
commit e8dffc9692
58 changed files with 174 additions and 7563 deletions

View file

@ -2,6 +2,18 @@ import { describe, expect, it } from "vitest";
import { summarizeActivityBuckets, summarizeActivityRows } from "./activityService";
describe("summarizeActivityRows", () => {
it("summarizes the remaining activity categories without Arcade fields", () => {
const now = new Date("2026-08-07T18:00:00.000Z");
const summary = summarizeActivityBuckets([
{ date: "2026-08-07", type: "FLASHCARD", count: 2 },
{ date: "2026-08-07", type: "QUIZ_QUESTION", count: 3 },
], now);
expect(summary).toMatchObject({ flashcards: 2, questions: 3, currentStreak: 0 });
expect(summary.days.find((day) => day.date === summary.today)).toMatchObject({ flashcards: 2, questions: 3, total: 5 });
expect(summary).not.toHaveProperty("arcade");
});
it("keeps a 53-week display while preserving a longer current streak", () => {
const now = new Date("2026-08-07T18:00:00.000Z");
const activities = Array.from({ length: 400 }, (_, age) =>

View file

@ -10,7 +10,6 @@ export interface DailyActivity {
date: string;
flashcards: number;
questions: number;
arcade: number;
total: number;
level: 0 | 1 | 2 | 3 | 4;
}
@ -21,7 +20,6 @@ export interface ActivitySummary {
activeDays: number;
flashcards: number;
questions: number;
arcade: number;
currentStreak: number;
}
@ -48,7 +46,7 @@ function getLevel(total: number): 0 | 1 | 2 | 3 | 4 {
}
function emptyDay(date: string): DailyActivity {
return { date, flashcards: 0, questions: 0, arcade: 0, total: 0, level: 0 };
return { date, flashcards: 0, questions: 0, total: 0, level: 0 };
}
export async function recordActivity(type: StudyActivityType) {
@ -96,7 +94,6 @@ export function summarizeActivityBuckets(
const day = totals.get(date) ?? emptyDay(date);
if (activity.type === "FLASHCARD") day.flashcards += activity.count;
if (activity.type === "QUIZ_QUESTION") day.questions += activity.count;
if (activity.type === "ARCADE_GROUP") day.arcade += activity.count;
day.total += activity.count;
day.level = getLevel(day.total);
totals.set(date, day);
@ -109,7 +106,6 @@ export function summarizeActivityBuckets(
const visibleDays = days.filter((day) => day.date <= today && day.date <= endDate);
const flashcards = visibleDays.reduce((sum, day) => sum + day.flashcards, 0);
const questions = visibleDays.reduce((sum, day) => sum + day.questions, 0);
const arcade = visibleDays.reduce((sum, day) => sum + day.arcade, 0);
let streakDate = (totals.get(today)?.total ?? 0) >= STREAK_THRESHOLD
? today
@ -126,7 +122,6 @@ export function summarizeActivityBuckets(
activeDays: visibleDays.filter((day) => day.total > 0).length,
flashcards,
questions,
arcade,
currentStreak,
};
}

View file

@ -1,228 +0,0 @@
import { prisma } from "@/lib/db";
import { parseConnectionsImportBatch } from "@/lib/arcade/connectionsImport";
import { replayConnectionsAttempt } from "@/lib/arcade/connectionsEngine";
import { parseCrosswordImportBatch } from "@/lib/arcade/crosswordImport";
import { replayCrosswordAttempt } from "@/lib/arcade/crosswordAttempt";
import type { ArcadeAttemptCreateInput, CrosswordAttemptCreateInput } from "@/lib/validation/arcadeSchemas";
import type {
ArcadeAttemptSummary,
ArcadeGameKey,
ArcadePackSummary,
NormalizedArcadePack,
NormalizedConnectionsPack,
NormalizedCrosswordPack,
} from "@/types/arcade";
function attemptSummary(attempt: {
id: string;
score: number;
maxScore: number;
accuracy: number;
durationSeconds: number;
mistakes: number;
completedAt: Date;
resultsJson?: string;
}): ArcadeAttemptSummary {
return {
id: attempt.id,
score: attempt.score,
maxScore: attempt.maxScore,
accuracy: attempt.accuracy,
durationSeconds: attempt.durationSeconds,
mistakes: attempt.mistakes,
completedAt: attempt.completedAt.toISOString(),
...(attempt.resultsJson ? { results: JSON.parse(attempt.resultsJson) } : {}),
};
}
function parseImport(gameType: ArcadeGameKey, rawJson: string) {
return gameType === "connections" ? parseConnectionsImportBatch(rawJson) : parseCrosswordImportBatch(rawJson);
}
export function previewArcadeImport(gameType: ArcadeGameKey, rawJson: string) {
return parseImport(gameType, rawJson);
}
export async function createArcadePacks(input: {
classId: string;
gameType: ArcadeGameKey;
rawJson: string;
name?: string;
names?: string[];
warningsAcknowledged: boolean;
}) {
const classItem = await prisma.class.findUnique({ where: { id: input.classId }, select: { id: true } });
if (!classItem) throw new Error("Class not found");
const batch = parseImport(input.gameType, input.rawJson);
if (input.names && input.names.length !== batch.count) {
throw new Error("A name is required for every imported pack.");
}
if (batch.packs.some((preview) => preview.warnings.length > 0) && !input.warningsAcknowledged) {
throw new Error("Import warnings must be acknowledged before saving.");
}
const maxOrder = await prisma.arcadePack.aggregate({
where: { classId: input.classId, gameType: input.gameType },
_max: { sortOrder: true },
});
return prisma.$transaction(async (transaction) => {
const created = [];
for (const [index, preview] of batch.packs.entries()) {
created.push(await transaction.arcadePack.create({
data: {
classId: input.classId,
gameType: input.gameType,
name: input.names?.[index]?.trim() || (batch.count === 1 ? input.name?.trim() : undefined) || preview.name,
description: preview.description,
schemaVersion: preview.normalized.schemaVersion,
sourceJson: JSON.stringify(preview.source),
normalizedJson: JSON.stringify(preview.normalized),
validationReportJson: JSON.stringify({ wasRepaired: preview.wasRepaired, warnings: preview.warnings }),
sortOrder: (maxOrder._max.sortOrder ?? -1) + index + 1,
},
}));
}
return created;
});
}
export async function listArcadePacks(classId: string, gameType: ArcadeGameKey): Promise<ArcadePackSummary[]> {
const packs = await prisma.arcadePack.findMany({
where: { classId, gameType },
orderBy: { sortOrder: "asc" },
include: { attempts: { orderBy: { completedAt: "desc" } } },
});
return packs.map((pack) => {
const normalized = JSON.parse(pack.normalizedJson) as NormalizedArcadePack;
const isConnections = normalized.type === "connections";
return {
id: pack.id,
classId: pack.classId,
gameType: pack.gameType as ArcadeGameKey,
name: pack.name,
description: pack.description,
schemaVersion: pack.schemaVersion,
itemCount: isConnections ? normalized.content.length * 4 : normalized.content.length,
defaultAllowedMistakes: isConnections ? normalized.settings.allowedMistakes : 4,
defaultInstantCheck: isConnections ? false : normalized.settings.allowInstantCheck,
defaultAllowHints: isConnections ? true : normalized.settings.allowHints,
bestScore: pack.attempts.length ? Math.max(...pack.attempts.map((attempt) => attempt.score)) : null,
latestAttempt: pack.attempts[0] ? attemptSummary(pack.attempts[0]) : null,
};
});
}
export async function getArcadePack(id: string) {
const pack = await prisma.arcadePack.findUnique({
where: { id },
include: { class: { select: { slug: true, name: true } } },
});
if (!pack) return null;
return {
...pack,
gameType: pack.gameType as ArcadeGameKey,
normalized: JSON.parse(pack.normalizedJson) as NormalizedArcadePack,
validationReport: JSON.parse(pack.validationReportJson),
};
}
export async function updateArcadePack(id: string, name: string) {
const existing = await prisma.arcadePack.findUnique({ where: { id }, select: { id: true } });
if (!existing) return null;
return prisma.arcadePack.update({ where: { id }, data: { name } });
}
export async function deleteArcadePack(id: string) {
const existing = await prisma.arcadePack.findUnique({ where: { id }, select: { id: true } });
if (!existing) return false;
await prisma.arcadePack.delete({ where: { id } });
return true;
}
export async function listArcadeAttempts(arcadePackId: string) {
const attempts = await prisma.arcadeAttempt.findMany({
where: { arcadePackId },
orderBy: { completedAt: "desc" },
take: 20,
});
return attempts.map(attemptSummary);
}
export async function createArcadeAttempt(arcadePackId: string, input: ArcadeAttemptCreateInput) {
const pack = await prisma.arcadePack.findUnique({ where: { id: arcadePackId } });
if (!pack) return null;
const normalized = JSON.parse(pack.normalizedJson) as NormalizedConnectionsPack;
if (normalized.type !== "connections") throw new Error("This attempt does not match the pack game type.");
const replay = replayConnectionsAttempt(
normalized,
input.submissions,
input.settings,
input.durationSeconds
);
if (!replay.complete) throw new Error("Only completed Connections rounds can be saved.");
const solvedGroups = replay.result.groups.filter((group) => group.solved).length;
return prisma.$transaction(async (transaction) => {
const attempt = await transaction.arcadeAttempt.create({
data: {
arcadePackId,
mode: input.mode,
score: replay.result.score,
maxScore: replay.result.maxScore,
accuracy: replay.result.accuracy,
durationSeconds: replay.result.durationSeconds,
mistakes: replay.result.mistakes,
hintsUsed: replay.result.hintsUsed,
settingsJson: JSON.stringify(input.settings),
resultsJson: JSON.stringify(replay.result),
seed: input.seed,
},
});
if (solvedGroups > 0) {
await transaction.studyActivity.createMany({
data: Array.from({ length: solvedGroups }, () => ({ type: "ARCADE_GROUP" })),
});
}
return attemptSummary({ ...attempt, resultsJson: attempt.resultsJson });
});
}
export async function createCrosswordAttempt(arcadePackId: string, input: CrosswordAttemptCreateInput) {
const pack = await prisma.arcadePack.findUnique({ where: { id: arcadePackId } });
if (!pack) return null;
const normalized = JSON.parse(pack.normalizedJson) as NormalizedCrosswordPack;
if (normalized.type !== "crossword") throw new Error("This attempt does not match the pack game type.");
const result = replayCrosswordAttempt(
normalized,
input.seed,
input.settings,
input.finalAnswers,
input.actions,
input.durationSeconds,
input.gaveUp
);
const correctWords = result.entries.filter((entry) => !entry.omitted && entry.correct && !entry.revealedWord).length;
return prisma.$transaction(async (transaction) => {
const attempt = await transaction.arcadeAttempt.create({
data: {
arcadePackId,
mode: input.mode,
score: result.score,
maxScore: result.maxScore,
accuracy: result.accuracy,
durationSeconds: result.durationSeconds,
mistakes: result.mistakes,
hintsUsed: result.hintsUsed,
settingsJson: JSON.stringify(input.settings),
resultsJson: JSON.stringify(result),
seed: input.seed,
},
});
if (correctWords > 0) {
await transaction.studyActivity.createMany({
data: Array.from({ length: correctWords }, () => ({ type: "ARCADE_WORD" })),
});
}
return attemptSummary({ ...attempt, resultsJson: attempt.resultsJson });
});
}

View file

@ -1,7 +1,6 @@
import { prisma } from "@/lib/db";
import { ARCADE_SERVER_REGISTRY } from "@/lib/arcade/registry";
export type LlmInstructionType = "flashcards" | "quizzes" | "connections" | "crossword";
export type LlmInstructionType = "flashcards" | "quizzes";
const DEFAULT_LLM_INSTRUCTIONS_FLASHCARDS = `You are generating study materials in a strict JSON format for import into a
personal study app. The overall output must be raw JSON with no wrapping
@ -53,9 +52,7 @@ Material:
function instructionConfig(type: LlmInstructionType) {
if (type === "flashcards") return { key: "llmInstructionsFlashcards", value: DEFAULT_LLM_INSTRUCTIONS_FLASHCARDS };
if (type === "quizzes") return { key: "llmInstructionsQuizzes", value: DEFAULT_LLM_INSTRUCTIONS_QUIZZES };
if (type === "connections") return { key: "llmInstructionsConnections", value: ARCADE_SERVER_REGISTRY.connections.defaultInstructions };
return { key: "llmInstructionsCrossword", value: ARCADE_SERVER_REGISTRY.crossword.defaultInstructions };
return { key: "llmInstructionsQuizzes", value: DEFAULT_LLM_INSTRUCTIONS_QUIZZES };
}
export async function getLlmInstructions(type: LlmInstructionType): Promise<string> {