Study/AGENTS.md
Elijah c84332d072
All checks were successful
Automated Container Build / build-and-push (push) Successful in 58s
Update agents.md, add share icon to main cards
2026-07-11 19:19:27 -07:00

26 KiB

Study Desk - AI Contributor Guide

This file is the project-specific operating guide for AI agents and human contributors working in this repository.

Project overview

Study Desk is a self-hosted, single-user study application. It organizes study material by class and supports:

  • Flashcard decks with Markdown content, self-grading, shuffle persistence, resume state, and card management.
  • Practice quizzes with multiple-choice and SATA questions, scoring, rationales, category breakdowns, retakes, and attempt history.
  • Importing validated JSON files and generating importable JSON from user-provided study material.
  • Material groups for organizing decks and quizzes.
  • Password-protected private pages with public, read-only share links for decks, quizzes, and groups.
  • Light and dark themes with responsive layouts for desktop and mobile use.

The application is intentionally a small single-user container rather than a multi-tenant SaaS product. There is no User model. Authentication state is held in an encrypted iron-session cookie, and content ownership is implicit in the one local installation.

When documentation conflicts with implementation, prefer the current source code and prisma/schema.prisma over the older planning document or the stock README.

Mandatory project rules

Next.js version and documentation

This repository uses Next.js 16.2.9, React 19.2.4, and the App Router. This is not a generic Next.js project. Before changing Next.js routing, layouts, dynamic parameters, server/client boundaries, middleware, or framework APIs:

  1. Read the relevant guide under node_modules/next/dist/docs/.
  2. Follow the version installed in this repository, not a remembered API from an older Next.js version.
  3. Pay attention to deprecation notices in build output. The current project still uses src/middleware.ts; Next.js currently warns that the middleware convention is moving toward proxy. Do not perform that migration as unrelated cleanup.

Relevant local references include:

  • node_modules/next/dist/docs/01-app/01-getting-started/05-server-and-client-components.md
  • node_modules/next/dist/docs/01-app/01-getting-started/04-linking-and-navigating.md
  • node_modules/next/dist/docs/01-app/03-api-reference/01-directives/use-client.md
  • node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/route.md
  • node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/dynamic-routes.md

General contribution behavior

  • Ask a clarifying question before proceeding when missing information would materially change the implementation or create a risky assumption. Otherwise make a narrow, documented assumption and continue.
  • Preserve existing user changes. Inspect git status before editing and do not use destructive commands such as git reset --hard or git checkout -- unless explicitly requested.
  • Use apply_patch for source and documentation edits.
  • Keep changes scoped to the requested behavior. Do not rewrite unrelated components, migrate frameworks, or clean up the whole repository during a feature fix.
  • Do not commit secrets, .env files, database contents, generated credentials, or private study material.
  • Prefer TypeScript types over any, even though some existing files have any and the current lint baseline is not clean.
  • Do not manually edit generated Prisma files under src/generated/prisma/.
  • Do not manually edit an already-applied migration. Change prisma/schema.prisma, then create a new migration.
  • Add dependencies only when the existing stack cannot reasonably solve the problem. If dependencies change, update package-lock.json and verify the build.
  • Keep route handlers, services, and UI components separated by responsibility as described below.

Stack and repository configuration

Area Current implementation
Language TypeScript, strict mode enabled
UI React 19 with Next.js 16 App Router
Styling Tailwind CSS v4 through src/app/globals.css and @tailwindcss/postcss
Fonts Manrope for interface text and Newsreader for editorial headings via next/font/google
Persistence SQLite with Prisma 7 and @prisma/adapter-better-sqlite3
Authentication iron-session cookie, Argon2 password hashing, in-memory login rate limiter
Validation Zod schemas in src/lib/validation/
Markdown react-markdown with remark-gfm
Drag and drop @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities
JSON repair jsonrepair through src/lib/jsonRepair.ts
Runtime Node 22-slim in the Docker image; local Node/npm should be compatible with the lockfile
Deployment Next standalone output in a single Docker container

Important configuration files:

  • package.json - scripts and dependency versions.
  • next.config.ts - standalone output and better-sqlite3 as a server external package.
  • tsconfig.json - strict TypeScript, bundler module resolution, and @/* mapped to src/*.
  • eslint.config.mjs - Next core-web-vitals and Next TypeScript rules.
  • postcss.config.mjs - Tailwind v4 PostCSS integration.
  • prisma.config.ts - Prisma schema and SQLite URL resolution.
  • Dockerfile, docker-compose.yml, docker-compose.override.yml, docker-entrypoint.sh - container build and runtime.

Commands and local setup

Install and run locally

From the repository root:

npm install
npx prisma generate
npm run dev

Then open http://localhost:3000.

On Windows PowerShell, this environment may block the npm.ps1 and npx.ps1 shims because of execution policy. Use npm.cmd and npx.cmd when necessary:

npm.cmd install
npx.cmd prisma generate
npm.cmd run dev

The default local database is dev.db when DATABASE_URL is file:./dev.db. There is no seed script; classes and study material are created through the UI or API.

Environment variables

Copy .env.example to .env for local work. The current variables are:

Variable Purpose
DATABASE_URL Prisma SQLite URL. Local default: file:./dev.db.
SESSION_SECRET Secret used to encrypt the iron-session cookie. Use a random value of at least 32 characters outside local throwaway development.
ADMIN_PASSWORD_HASH Present in .env.example, but the current login implementation does not read it. See authentication below.
SECURE_COOKIES If set to true in production, the session cookie is marked secure.

The .env file is ignored by Git. Never paste real secrets into source files, documentation, logs, or commits.

Password initialization behavior

The current implementation initializes the password on the first login when the Setting row admin_password_hash does not exist:

  1. The first password submitted at /login is Argon2-hashed and stored in the Setting table.
  2. Later logins verify against that stored hash.
  3. ADMIN_PASSWORD_HASH is currently not wired into this flow despite appearing in .env.example and the planning document.

Do not change this behavior casually. If environment-driven password provisioning is added, update the login route, setup-status route, deployment documentation, and security handling together.

Available scripts

npm run dev       # Next development server
npm run build     # Production build; also runs TypeScript checking
npm run start     # Start the built app locally
npm run lint      # ESLint

There is currently no test runner configured in package.json. Use the production build plus focused manual/browser verification for UI changes.

Database workflow

After changing prisma/schema.prisma:

npx prisma generate
npx prisma migrate dev --name describe_the_change

Use npx prisma migrate deploy for an existing deployment. The Docker entrypoint runs prisma migrate deploy before starting the server.

Be careful with dev.db: it may contain the developer's actual study data. Do not delete or reset it as part of normal feature work. Inspect migration status and make a backup before any destructive database operation.

Docker details

The production image is a three-stage Node 22-slim build:

  1. Install dependencies.
  2. Generate Prisma and run next build.
  3. Run the Next standalone server with SQLite data mounted at /app/data.

The runtime image sets DATABASE_URL=file:/app/data/study.db and PORT=3726. docker-entrypoint.sh applies migrations and then starts server.js.

Known compose detail: docker-compose.yml currently publishes 3000:3000, while the production Dockerfile sets the runtime port to 3726. Verify or correct that mapping before relying on the production compose file. docker-compose.override.yml is a development override that runs npm run dev and uses port 3000.

Current project structure

The important source tree is:

prisma/
  schema.prisma                 # SQLite/Prisma source of truth
  migrations/                   # Applied database migrations

src/
  app/
    layout.tsx                  # Root metadata, fonts, theme bootstrap
    globals.css                 # Tailwind v4, design tokens, global styles
    icon.svg
    login/page.tsx              # Public login/setup screen
    (protected)/
      layout.tsx                # Session check and authenticated navbar shell
      page.tsx                   # Authenticated class library
      [classSlug]/
        layout.tsx              # Class lookup, header, and study-mode tabs
        page.tsx                 # Redirects to flashcards
        flashcards/page.tsx     # Grouped deck library and drag/drop ordering
        flashcards/[deckId]/page.tsx
                                  # Deck study/manage screen
        quizzes/page.tsx        # Grouped quiz library and drag/drop ordering
        quizzes/[quizId]/page.tsx
                                  # Quiz taking/history screen
    shared/[classSlug]/[type]/[token]/
      page.tsx                  # Public read-only share route
      SharedViewer.tsx          # Public deck/quiz viewer
      SharedGroupViewer.tsx     # Public group selector/viewer
    api/
      .../route.ts              # Route handlers; see API map below

  components/
    flashcards/                 # Flashcard viewer, manager, and list
    quizzes/                    # Quiz viewer, results, history, breakdown
    import/                     # Import modal and Create/Import/Generate tabs
    ui/                         # Navbar, class header/tabs, theme, share menu

  services/
    classService.ts
    deckService.ts
    cardService.ts
    quizService.ts
    progressService.ts
    shareService.ts
    settingsService.ts          # Persistence for LLM instruction templates

  lib/
    db.ts                       # Prisma singleton with better-sqlite3 adapter
    auth.ts                     # iron-session helpers
    rateLimiter.ts              # In-memory login sliding-window limiter
    scoring.ts                  # Quiz scoring
    shuffle.ts                  # Stable order/session helpers
    jsonRepair.ts               # JSON parse/repair pipeline
    validation/importSchemas.ts

  config/studyModes.ts          # Central list of flashcards/quizzes modes
  middleware.ts                 # Auth gate and public-path exceptions
  generated/prisma/             # Generated Prisma client; do not hand edit

public/                         # Static assets

(protected) is a route group and is not part of the URL. The classSlug, deckId, quizId, and share token segments are dynamic route parameters.

Page routes and API surface

Page routes

Route Behavior
/login Public password setup/login page.
/ Authenticated class library.
/<classSlug> Redirects to /<classSlug>/flashcards.
/<classSlug>/flashcards Grouped flashcard deck library.
/<classSlug>/flashcards/<deckId> Flashcard study or card-management view.
/<classSlug>/quizzes Grouped quiz library.
/<classSlug>/quizzes/<quizId> Quiz-taking or attempt-history view.
/shared/<classSlug>/flashcards/<token> Public shared deck.
/shared/<classSlug>/quizzes/<token> Public shared quiz.
/shared/<classSlug>/groups/<token> Public shared group; ?itemId=... can select an item.

API routes

All API routes are behind src/middleware.ts unless they are under /api/auth. The API is currently consumed by client components with fetch.

Endpoint Methods and purpose
/api/auth/login POST login, first-time password setup, rate limiting, progressive lockout.
/api/auth/logout POST destroy the session.
/api/auth/setup-status GET whether the password setting exists.
/api/classes GET list classes; POST create class.
/api/classes/[id] PATCH rename; DELETE class and cascaded content.
/api/decks POST validate and import a deck.
/api/decks/list GET decks for classId.
/api/decks/[id] GET deck with cards; PATCH rename/description; DELETE.
/api/decks/[id]/cards POST append a card.
/api/cards/[id] PATCH edit card; DELETE card.
/api/decks/reorder PATCH persist deck order/group placement.
/api/quizzes POST validate and import a quiz.
/api/quizzes/list GET quizzes for classId.
/api/quizzes/[id] GET quiz with questions/options; PATCH rename/description; DELETE.
/api/quizzes/[id]/attempt GET attempt history; POST score and save an attempt.
/api/quizzes/reorder PATCH persist quiz order/group placement.
/api/material-groups GET groups by classId and optional type; POST create group.
/api/material-groups/[id] PATCH rename/group metadata; DELETE group.
/api/progress GET, PATCH, and DELETE resume progress for deck/quiz and mode.
/api/settings/llm-instructions GET/PATCH saved generation instructions by type; __RESET__ restores defaults.
/api/share GET share state; POST toggle a deck, quiz, or group link.

Architecture and coding conventions

Server and client components

  • Use Server Components by default for layouts and pages that only fetch or compose data.
  • Add "use client" only where state, event handlers, effects, browser APIs, drag/drop, or client-side fetches are needed.
  • Once a file is a Client Component, its directly imported module graph is included in the client bundle. Keep server-only database/auth code out of client imports.
  • Do not access window, localStorage, navigator, or the clipboard API from a Server Component.
  • Dynamic server route params use the current Next.js 16 conventions in this repository. Follow nearby examples such as src/app/(protected)/[classSlug]/layout.tsx and API RouteContext types instead of inventing older parameter types.
  • Prefer Link for normal internal navigation. Use useRouter only when navigation follows an action or requires imperative behavior.

API handlers and services

The preferred API route shape is:

  1. Parse and validate request input.
  2. Call one function in the matching service or low-level utility.
  3. Return a NextResponse with a useful status and JSON body.

Business logic and Prisma calls belong in src/services/ or a focused src/lib/ utility. Reuse an existing service before creating a new one. SKILL.md contains the project-specific workflow for adding an API route.

A few older routes still contain direct Prisma calls or loose input handling. Follow the preferred pattern for new work without broad refactoring unless the task specifically requests it.

Prisma and database access

  • Import the shared client from @/lib/db; do not instantiate a new PrismaClient in a request or component.
  • Prisma uses the generated client at src/generated/prisma, configured by prisma/schema.prisma.
  • Keep database names and relation behavior consistent with the schema: Deck and QuizSet belong to a Class; cards/questions/options cascade from their parent; group deletion sets item groupId to null; share links and progress cascade with their content.
  • Use transactions for multi-row reorder/update operations where partial writes would be harmful.
  • Handle missing records as 404s at the route boundary rather than leaking Prisma exceptions.

Validation and input handling

  • Reuse flashcardImportSchema and quizImportSchema from src/lib/validation/importSchemas.ts for import payloads on both client preview and server commit.
  • Do not trust client-side validation alone.
  • Validate IDs, enum-like values (DECK/QUIZ, SEQUENTIAL/SHUFFLED, question types), required strings, and array shapes at the API boundary.
  • Keep stored JSON fields parseable and handle malformed or stale JSON defensively. jsonRepair.ts is for repairing user/LLM-generated import text, not for hiding malformed database state.

UI and design system

  • Use the CSS variables and Tailwind aliases defined in src/app/globals.css: bg-bg-base, bg-bg-surface, bg-bg-surface-alt, text-text-heading, text-text-secondary, text-text-muted, primary, primary-hover, border, border-light, success, and error.
  • Preserve both light and dark theme behavior. Avoid introducing one-off colors when an existing token is appropriate.
  • Use editorial-title for major serif headings, font-sans/Manrope for interface text, and the existing card/modal shadows and rounded-corner language.
  • Maintain responsive behavior at mobile widths. Study pages, library cards, modals, and action rows must remain usable without horizontal overflow.
  • Use visible focus states, descriptive title/aria-label text for icon-only buttons, semantic buttons/links, and keyboard-accessible controls.
  • Keep Markdown rendering consistent with the existing ReactMarkdown + remarkGfm usage and .markdown-content styles.
  • Use the existing @dnd-kit patterns when changing sortable cards or groups. Do not attach drag listeners to nested action buttons.

Domain behavior to preserve

Flashcards

  • A deck contains ordered Flashcard rows with front and back strings.
  • Study progress is stored in StudyProgress as JSON order/results and can be sequential or shuffled.
  • Deleting a card can leave a stale ID in saved orderJson; the shuffle/session helpers filter and clamp stale IDs when resuming.
  • The deck page supports study and manage views. Keep the existing viewer-header share action when adding other share entry points.

Quizzes

  • A quiz contains ordered questions, each with ordered answer options.
  • MULTIPLE_CHOICE questions must have exactly one correct option.
  • SATA questions may have multiple correct options and use partial credit in src/lib/scoring.ts.
  • Attempts are persisted as QuizAttempt rows with score, maximum score, answer JSON, retake flag, and completion time.
  • Partial retakes score only the questions included in the submitted answer set.
  • The quiz viewer persists in-progress answers and position through /api/progress.

Imports and generation

Flashcard import shape:

{
  "type": "flashcards",
  "deckName": "Deck name",
  "description": "Optional description",
  "cards": [{ "front": "...", "back": "..." }]
}

Quiz import shape:

{
  "type": "quiz",
  "quizName": "Quiz name",
  "description": "Optional description",
  "questions": [
    {
      "type": "multiple_choice",
      "prompt": "...",
      "category": "topic",
      "options": [{ "text": "...", "correct": true }],
      "rationale": "..."
    }
  ]
}
  • Import UI is split into ImportModal, CreateTab, ImportTab, and GenerateTab. Keep that separation rather than creating one large modal component.
  • Generation instructions are stored in the Setting table and are editable/resettable through /api/settings/llm-instructions. The app does not call an LLM itself; the Generate tab prepares instructions and accepts generated JSON from the user.
  • Generated text may contain Markdown inside JSON string values. Preserve it and render it safely through the existing Markdown components.

Sharing

  • ShareLink rows use UUID IDs as public tokens and can target a DECK, QUIZ, or GROUP.
  • src/components/ui/ShareMenu.tsx is the shared UI for loading share state, toggling a link, and copying the public URL. Reuse it rather than duplicating share URL or group-inheritance logic.
  • Public URLs are /shared/<classSlug>/<type>/<token>, where item types are flashcards, quizzes, and groups.
  • A deck or quiz can appear publicly shared through its group. The API returns the group token and the UI includes ?itemId=... when copying a link to an item in a shared group.
  • Shared viewers are read-only and should not write authenticated progress. Preserve the distinction between protected study viewers and public shared viewers.

Data model summary

The source of truth is prisma/schema.prisma:

  • Class - named study space with a unique slug and sort order.
  • Deck - flashcard collection belonging to a class, optionally assigned to a MaterialGroup.
  • Flashcard - ordered front/back card belonging to a deck.
  • QuizSet - quiz collection belonging to a class, optionally assigned to a MaterialGroup.
  • Question - ordered quiz prompt, type, rationale, and category.
  • AnswerOption - ordered answer option with isCorrect.
  • StudyProgress - resumable deck/quiz state. Unique by parent and mode.
  • QuizAttempt - completed quiz score and submitted answer JSON.
  • MaterialGroup - typed (DECK or QUIZ) class grouping for library organization.
  • ShareLink - optional one-to-one link for a deck, quiz, or group.
  • Setting - key/value storage for the admin password hash and editable LLM instructions.
  • AuthSecurity - singleton lockout/rate-limit state persisted in SQLite.

Most parent relations use cascading deletion. Treat deletion of a class, deck, quiz, question, or group as a data-affecting operation and preserve confirmation behavior in the UI.

Authentication and security boundaries

src/middleware.ts allows these paths without a session:

  • /login
  • /api/auth/*
  • /shared/*
  • Static assets and Next internals

Everything else requires the study-app-session cookie. Individual API routes generally rely on middleware for the authenticated boundary, so a new public endpoint must be an explicit, reviewed decision.

The login route has two protections:

  • An in-memory per-IP sliding-window limiter: 10 attempts per minute.
  • Progressive lockout stored in AuthSecurity: one minute after 5 failures, five minutes after 10, thirty minutes after 15, and twenty-four hours after 20.

Do not log passwords, session secrets, full share tokens, or private study content. Treat public share routes as intentionally public and validate that the token, type, and class slug agree before returning content.

Verification checklist

Before editing

  • Read git status --short.
  • Inspect the target page/component, its API route, and its service before changing behavior.
  • Read the relevant Next.js local guide if framework behavior is involved.
  • Check whether the change affects the Prisma schema, migrations, auth boundary, public sharing, or generated client.

During implementation

  • Keep the route/service/component boundaries intact.
  • Add validation for new external input.
  • Reuse existing design tokens, shared components, and services.
  • Consider loading, error, empty, mobile, keyboard, and both theme states.
  • Avoid synchronous state updates directly inside effects when a state initializer, derived value, event handler, or async callback is more appropriate. The current ESLint configuration reports these patterns.

After editing

Run the narrowest relevant checks, then the full build when possible:

npm run lint
npm run build

If lint is already failing, report baseline failures separately from failures introduced by the change. Do not silence rules or add broad ESLint disables just to make a task appear clean.

For UI changes, manually verify at least the affected authenticated route and its corresponding public/shared route if applicable. A useful smoke-test path is:

  1. Log in or complete first-time setup.
  2. Create or open a class.
  3. Import or edit representative flashcards and quizzes.
  4. Exercise the changed action, including loading/error/empty states.
  5. Check study resume/restart, drag/drop if relevant, and public sharing if relevant.
  6. Check light/dark themes and a narrow mobile viewport.

Known repository conditions

  • README.md is still mostly the default create-next-app README and is not a reliable description of the current product.
  • study-app-implementation-plan.md is a valuable architectural reference, but some details are historical and the current source code wins.
  • task.md records completed implementation phases and is useful for feature context.
  • SKILL.md is the project-specific workflow for adding API routes; follow it for new route work.
  • The current full lint command has pre-existing errors in several files, including existing any usage and React effect-rule violations. Do not attribute those to an unrelated small change without checking the changed lines.
  • A successful next build is the primary repository-wide compile/type verification currently available.
  • Next build output currently warns about the deprecated middleware file convention. Treat that as known baseline unless the task is specifically about the migration.

Extending the application

For a genuinely new study mode, follow the existing extensibility pattern:

  1. Add the domain model and migration in Prisma if persistence is needed.
  2. Add a focused service in src/services/.
  3. Add API routes under src/app/api/.
  4. Add the page route and client components.
  5. Add the mode to src/config/studyModes.ts if it belongs in shared class navigation.
  6. Update public sharing, progress, import, or scoring only if the new mode needs those capabilities.
  7. Verify auth, mobile layout, theme support, and any public/private boundary.

Do not add a new mode by placing unrelated logic into deckService.ts, quizService.ts, or a giant page component. The existing separation is intentional and is the main maintainability boundary in this project.