# Project export: Kanni

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: To make life better, Kanni is the AI link between Kerala’s students, teachers, and parents, built to help every child from Class 1 to 12 learn with confidence and prepare for an AI-shaped future.
- Devpost: https://devpost.com/software/project-horizon-3ym728
- GitHub: https://github.com/Arnol-P-S/kanni.git
- Video: https://www.youtube.com/embed/i_EhsqxzLAM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

Here is a small moment that explains the whole project. A Class 8 student estimates how much rain a school roof can catch. She multiplies $160 \text{ m}^2 \times 100 \text{ mm} = 16{,}000$ litres, then "adds 80 percent efficiency" and writes 16,080 litres. A chatbot would hand her the correct 12,800 in two seconds. She would copy it, feel fine, and learn nothing about her own mistake. That moment is where most school AI goes wrong. It answers first, so the learner becomes a passenger. We started Kanni from the other end: the teacher. A better student chatbot helps one learner for one moment. A better planning tool helps one teacher prepare several routes through the same goal, anticipate the confusion before it happens, read how a student's thinking changed, and send one useful idea home to a family. So we built a product where AI does preparation and questioning, teachers make every decision, and the student has to do the actual thinking.

### What it does

Kanni connects four people around one curriculum-grounded "learning studio." The administrator creates real accounts, maps each student to a responsible teacher and parent, and manages versioned curriculum packs. They see workflow status and AI costs, never private student work. The teacher picks a source the school is allowed to use, sets a goal and driving question, and gets a complete plan: success criteria, lesson sequence, misconception probes, differentiated routes, maker choices, and a family activity. The learner works through a fixed sequence: predict, build a first version, critique it, revise it, explain the change, reflect. Only after a real first attempt can they ask the thinking coach for help, and the coach replies with questions and small experiments, never the answer. In our saved flow, it asks things like "what units should appear after each multiplication?" and the student finds the 80 percent error herself. Her revision lands on 12,800 litres, with covered tanks, a first-flush path, and an overflow route she added on her own. The parent receives one teacher-reviewed home activity and one next question. The private draft, the AI text, and anything resembling a score or diagnosis never reach that page. The teacher's review decision is not a label. It changes one concrete thing: how much support the next studio starts with (guided, light, or independent).

### How we built it

Next.js 16, React 19, strict TypeScript, PostgreSQL 18, Prisma 7, and Zod, deployed with Docker. The domain is a versioned state machine: every transition checks school, role, relationship, expected status, and record version. Privacy is enforced in the database queries themselves. The parent and administrator selects simply never include the submission fields, so there is nothing to leak at the page layer. The RAG path is deliberately small. Curriculum text is normalized, split into checksummed sections, and retrieved locally by relevance. GPT-5.6 Luna (through OpenRouter) handles two bounded requests per studio: one teacher plan drafted from up to six retrieved sections, and one set of thinking-coach questions built from the student's first attempt plus at most four sections. Every response must pass a strict Zod schema, and every citation must point at a section that was actually retrieved. One invented source ID and the entire draft is discarded. A second gate rejects answer-revealing language even when the structure is valid. How we collaborated with Codex The first version of this project was a static fractions lesson with four screens. It proved nothing. In one long Codex thread, we tore it out and rebuilt around the studio idea: new schemas, forward migrations, first-run school setup, role mapping, the plan editor, the evidence flow, the OpenRouter boundary, the evaluation set, and the test suites. Codex wrote fast; we spent our time on the decisions that shaped the product. Teachers are the lever. AI never completes student work. Production starts empty. Nothing calls a provider automatically. The best Codex moment was a bug hunt. Our clean-install browser test kept failing: setup looked correct, but the database stayed empty. Codex traced it to Prisma's PostgreSQL adapter choking on the void returned by pg_advisory_xact_lock, which silently rolled back the whole setup transaction. The fix kept the lock but called it through the execution API. The same test has passed ever since.

### Challenges we ran into

Making agency concrete was the hardest design problem. A slogan about critical thinking costs nothing. A data model has to pay for it. That is why the submission stores six separate pieces of evidence instead of one final answer. Privacy across four roles was the second. A parent deserves context, not surveillance. We ended up writing separate relational select shapes per role and then asserting in browser tests that a unique sentence from the student's draft appears nowhere it shouldn't. Content rights changed the retrieval design too. Being able to read a textbook online is not permission to copy it. Kanni accepts original, CC BY, public-domain, or written-permission text, and forces known SCERT hosts to link-only treatment.

### What we learned

RAG is mostly not retrieval. It is rights, versions, checksums, allowlisted citations, failure behavior, and a human deciding what ships. We also learned that AI support needs a removal plan: Kanni records whether the student opened help and lets the teacher fade support studio by studio. And a clean-database test tells the truth in a way seeded demos never do. Our main browser test builds the entire school through the same screens a real school would use. Today the project runs 61 unit tests, 51 deterministic evaluation cases, and a four-role browser flow, all without spending a single AI credit.

### What's next

Before real school use: OIDC or SAML, MFA, password recovery, retention and deletion tools, and independent security and child-safety review. On the learning side: school-approved curriculum collections, embedding retrieval for larger packs, whole-class planning, and Malayalam copy reviewed by native speakers. Kanni currently supports one school per installation, Classes 6 to 9, and makes no claims about academic outcomes. It claims something smaller and testable: the student did the thinking, and everyone responsible can see it.

## README (from the GitHub repository)

# Kanni | കണ്ണി

Kanni is a teacher-first learning platform for Classes 6 to 9. A teacher starts
with curriculum the school is allowed to use. A learner then predicts, makes,
critiques, revises, explains, and reflects. The teacher reads that evidence and
decides how much support should come next. A parent receives one reviewed home
activity without receiving the learner's private draft.

The core idea is simple: AI can prepare support, but it must not do the learner's
thinking or make the teacher's decision.

## What is working

### School administrator

- creates teacher, student, and parent accounts
- maps each learner to an assigned teacher and parent
- adds, versions, archives, and restores permission-safe curriculum packs
- sees studio handoff status and aggregate AI usage
- cannot read raw learner submissions

### Teacher

- creates a learning studio for an assigned learner
- selects an active school curriculum pack or registers a permission-safe source
- receives a complete teacher-owned starting plan without calling AI
- can request one GPT-5.6 planning draft through OpenRouter
- reviews and edits success criteria, lesson sequence, differentiation,
  misconception probes, quick checks, interest routes, maker paths, Socratic
  prompts, reflection prompts, and the family activity
- publishes only after confirming a source review
- reads the learner's full thinking sequence and chooses the next scaffold level

### Learner

- chooses an interest route and what to make
- predicts before beginning
- creates a first version before opening support
- can request one curriculum-grounded set of creative questions and small actions
  after making that first version
- finds a weakness, revises the work, and explains why the revision is stronger
- reflects on what can be done with less help next time
- receives the teacher's next question and future scaffold decision

### Parent

- sees the learning goal, one teacher observation, and one reviewed activity
- sends a small response back to the teacher
- never receives the learner's prediction, draft, critique, revision, model text,
  score, rank, or diagnosis

## The connected learning loop

```text
Administrator approves a versioned, permission-safe curriculum pack
    -> teacher selects the pack and defines the learning goal
    -> teacher reviews a local plan or requests one grounded GPT-5.6 draft
    -> learner predicts and makes before optionally requesting grounded thinking help
    -> learner critiques, revises, explains, and reflects
    -> teacher reviews the evidence and selects guided, light, or independent support
    -> parent receives one reviewed activity and responds
    -> the teacher's scaffold decision becomes the next studio's starting level
```

Every handoff checks the persisted stage and record version. Repeated or stale
actions cannot skip a stage.

![Kanni learner-agency loop](docs/diagrams/render/agency-loop.png)

## Why the AI is bounded

Kanni does not use a general student chatbot. It has two separate, explicit, and
one-use provider calls per studio: a teacher planning draft and a student thinking
coach after a first attempt.

When a teacher presses the AI planning button, Kanni:

1. retrieves up to six sections from the curriculum pack stored for that studio;
2. sends the goal, driving question, class, and retrieved sections to GPT-5.6;
3. requires a strict Zod object with the complete teacher-plan structure;
4. rejects the entire draft if any cited section ID was not retrieved;
5. stores request status, token counts, latency, model, prompt version, and cost,
   but not the provider prompt or learner work;
6. leaves publishing to the teacher.

When a student presses the thinking-coach button, Kanni:

1. requires a first attempt of at least 60 characters;
2. screens the attempt for personal data, high-risk text, and prompt injection;
3. retrieves at most four relevant sections;
4. sends only the goal, driving question, class, first attempt, and those sections;
5. requires three or four question-and-action steps plus a self-check;
6. rejects unknown citations, unsafe text, malformed output, and anything outside
   the strict object before the student sees it.

There is no automatic request, retry, provider fallback, web search, tool use, or
model conversation history. Kanni never asks GPT-5.6 to produce a completed
student answer. It does not add account names, family notes, passwords, membership
records, or the learner's later critique, revision, explanation, and reflection.

The local plan remains usable when AI is off, rejected, over budget, or unavailable.

## Clean Docker test build

The judge deployment starts with an empty PostgreSQL database. It does not create
sample schools, hidden accounts, passwords, or learner records.

After the `build-week-submission` release tag is published:

```bash
docker compose -f compose.judge.yaml up -d --wait
```

Open <http://localhost:3001>. The first screen asks you to create the school and
administrator. Use the administrator workspace to create the other three roles
and connect the support circle. AI is off in this portable build, so it cannot
spend provider credit.

Stop it with:

```bash
docker compose -f compose.judge.yaml down
```

Add `-v` only when you intentionally want to delete the local judge database and
repeat first-run setup.

No public hosting is required for this Education-category submission. The source
repository and this Docker path give judges a complete local test route.

## Local development

Requirements:

- Node 24, using the version in `.nvmrc`
- pnpm 11 through Corepack
- Docker with Compose

```bash
nvm install
nvm use
corepack enable
corepack pnpm install --frozen-lockfile
cp .env.example .env
docker compose up -d --wait db
corepack pnpm db:migrate:deploy
corepack pnpm dev
```

Open <http://localhost:3000>. Kanni redirects an empty installation to `/setup`.
There is no seed command. Create accounts through the administrator workspace.

## Production-style Docker deployment

Create deployment-specific settings first:

```bash
cp .env.production.example .env.production
corepack pnpm env:production:check
./deploy.sh deploy
```

The production Compose stack builds separate migration and application targets,
runs migrations before the application starts, keeps PostgreSQL on an internal
network, drops container capabilities, uses read-only application filesystems,
and exposes the app on `APP_HOST:APP_PORT`. The post-build release check removes
local environment files and stale development cache from the standalone artifact;
Docker injects runtime configuration through Compose instead.

Useful commands:

```bash
./deploy.sh status
./deploy.sh logs app
./deploy.sh backup
./deploy.sh stop
```

Kanni does not configure TLS or an ingress rate limit. Put a TLS reverse proxy in
front of any shared deployment and keep AI disabled until the external request
and spend controls are in place. Complete first-run setup while the application is
still bound to loopback, before making the reverse proxy public.

## Optional OpenRouter configuration

Codex credits pay for work done in Codex. They cannot pay for OpenRouter or OpenAI
API traffic. Runtime model calls need a separate OpenRouter balance.

Set these only after creating a provider key, a hard spend limit, and an ingress
rate limit:

```dotenv
GROWTH_AI_PROVIDER=openrouter
GROWTH_AI_MODEL=openai/gpt-5.6-luna
OPENROUTER_API_KEY=your_provider_key
GROWTH_AI_ENABLED=true
GROWTH_AI_RATE_LIMIT_CONFIRMED=true
GROWTH_AI_SPEND_LIMIT_CONFIRMED=true
# Keep student AI off until the school has reviewed external student-data processing.
GROWTH_AI_STUDENT_HELP_ENABLED=false
GROWTH_AI_STUDENT_DATA_REVIEW_CONFIRMED=false
```

Allowed models are `openai/gpt-5.6-luna` and `openai/gpt-5.6-sol`. Luna is the
default for the bounded structured tasks. Each studio can claim one teacher plan
request. Student thinking-coach requests remain unavailable unless both separate
student flags are set to `t

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 117 recognized source files, 811 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- PostgreSQL (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 135)

```
.dockerignore
.env.example
.env.production.example
.github/workflows/ci.yml
.github/workflows/publish-test-build.yml
.gitignore
.nvmrc
AGENTS.md
app/actions/admin.ts
app/actions/auth.ts
app/actions/preferences.ts
app/actions/setup.ts
app/actions/studio.ts
app/api/health/route.ts
app/error.tsx
app/global-error.tsx
app/globals.css
app/icon.tsx
app/layout.tsx
app/login/page.tsx
app/not-found.tsx
app/page.tsx
app/parent/page.tsx
app/portal/admin/page.tsx
app/portal/layout.tsx
app/portal/page.tsx
app/portal/parent/page.tsx
app/portal/student/page.tsx
app/portal/teacher/page.tsx
app/privacy/page.tsx
app/setup/page.tsx
app/teacher/page.tsx
app/terms/page.tsx
components/admin-forms.tsx
components/create-studio-form.tsx
components/language-switcher.tsx
components/login-form.tsx
components/nodes-mark.tsx
components/portal-chrome.tsx
components/public-footer.tsx
components/public-header.tsx
components/setup-form.tsx
components/student-studio-form.tsx
components/teacher-plan-editor.tsx
components/teacher-plan-view.tsx
compose.judge.yaml
compose.production.yaml
compose.recording.yaml
compose.yaml
CONTENT-LICENSE.md
data/backup/.gitkeep
deploy.sh
docker/db/init/01-create-test-db.sql
Dockerfile
docs/diagrams/agency-loop.html
docs/diagrams/rich.css
docs/diagrams/system-architecture.html
docs/mermaid-config.json
docs/mermaid-puppeteer.json
docs/SYSTEM-DESIGN.md
eslint.config.mjs
eval/cases.ts
eval/deterministic-results.json
lib/admin-contracts.ts
lib/ai/capability-policy.ts
lib/ai/prompt-context.ts
lib/ai/studio-ai.ts
lib/auth-constants.ts
lib/auth.ts
lib/curriculum/rag.ts
lib/db.ts
lib/i18n.ts
lib/installation.ts
lib/navigation.ts
lib/permissions.ts
lib/production-environment.ts
lib/recording/rainwater-flow.ts
lib/safety/input-guard.ts
lib/school-data.ts
lib/setup-contracts.ts
lib/studio/contracts.ts
lib/studio/grounding.ts
lib/studio/plan.ts
lib/studio/workflow.ts
LICENSE
next.config.ts
package.json
playwright.config.ts
pnpm-workspace.yaml
postcss.config.mjs
prisma.config.ts
prisma/migrations/20260717044237_init_school_platform/migration.sql
prisma/migrations/20260717114500_preserve_learning_cycle_history/migration.sql
prisma/migrations/20260717170000_agency_grounded_ai/migration.sql
prisma/migrations/20260717173000_backfill_agency_prompts/migration.sql
prisma/migrations/20260717213000_create_critique_revise/migration.sql
prisma/migrations/20260718164500_teacher_first_learning_studio/migration.sql
prisma/migrations/20260721113000_curriculum_library_student_help/migration.sql
prisma/migrations/migration_lock.toml
prisma/schema.prisma
proxy.ts
README.md
scripts/capture-submission-screenshots.ts
scripts/crop-rich-diagrams.py
scripts/prepare-recording-flow.ts
scripts/render-rich-diagrams.ts
scripts/review-recording-flow.ts
scripts/run-evals.ts
scripts/run-live-evals.ts
scripts/sanitize-standalone-build.mjs
scripts/validate-production-environment.ts
SECURITY.md
submission/DEVPOST.md
submission/PROJECT-STORY.md
submission/REVIEW-KIT.md
submission/VIDEO-CAPTIONS.srt
submission/VIDEO-SCRIPT.md
tests/e2e/kanni.spec.ts
tests/e2e/pages.ts
tests/unit/admin-contracts.test.ts
[15 more files omitted for size]
```

### Dependencies

- package.json: @axe-core/playwright@4.12.1, @fontsource/noto-sans@5.3.0, @fontsource/noto-sans-malayalam@5.3.0, @openrouter/ai-sdk-provider@3.0.0, @playwright/test@1.61.1, @prisma/adapter-pg@7.8.0, @prisma/client@7.8.0, @tailwindcss/postcss@4.3.3, @testing-library/jest-dom@6.9.1, @testing-library/react@16.3.2, @types/node@24.13.3, @types/pg@8.20.0, @types/react@19.2.17, @types/react-dom@19.2.3, @typescript/native@npm:typescript@7.0.2, @vitest/coverage-v8@4.1.10, ai@7.0.31, bcryptjs@3.0.3, dotenv@17.4.2, eslint@9.39.5, eslint-config-next@16.2.10, jsdom@29.1.1, lucide-react@1.25.0, next@16.2.10, pg@8.22.0, postcss@8.5.19, prisma@7.8.0, react@19.2.7, react-dom@19.2.7, server-only@0.0.1, tailwindcss@4.3.3, tsx@4.23.1, typescript@npm:@typescript/typescript6@6.0.2, vitest@4.1.10, zod@4.4.3

### Recent commits (newest first)

- fix
- final fixes and Demo preparation
- Build agency-centered four-role learning platform
- Build agency-centered school learning loop
- Build production-ready connected school platform
- Build connected four-role learning cycle
- Base app
- Initial commit from Create Next App

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

```

### CONTENT-LICENSE.md

```markdown
# Kanni original content license

The original teaching prompts, planning templates, interface copy, safety cards,
and evaluation cases in this repository are licensed under the Creative Commons
Attribution 4.0 International License, CC BY 4.0.

Copyright 2026 Arnol P. S.

You may share and adapt this original content for any purpose if you provide
appropriate credit, link to the license, and indicate whether changes were made.

License text: <https://creativecommons.org/licenses/by/4.0/legalcode>

This license does not apply to curriculum material entered by a school, third-party
names, links, trademarks, or other third-party material. Kanni does not bundle or
redistribute SCERT textbook content or logos. SCERT-hosted material must remain a
link-only reference unless the school has separate permission to copy it.

```

### Dockerfile

```
FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS base

ENV PNPM_HOME=/pnpm
ENV PATH=$PNPM_HOME:$PATH
ENV NEXT_TELEMETRY_DISABLED=1
RUN corepack enable
WORKDIR /app

FROM base AS dependencies
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY prisma ./prisma
COPY prisma.config.ts ./
RUN pnpm install --frozen-lockfile

FROM base AS builder
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN pnpm db:generate
RUN pnpm build

FROM dependencies AS operator
COPY . .
RUN pnpm db:generate
USER node
CMD ["node", "--conditions=react-server", "--import", "tsx", "scripts/prepare-recording-flow.ts"]

FROM base AS migrator
COPY --from=dependencies --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml prisma.config.ts ./
COPY --chown=node:node prisma ./prisma
USER node
CMD ["node", "node_modules/prisma/build/index.js", "migrate", "deploy"]

FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS runner

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs \
  && adduser --system --uid 1001 nextjs

COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

```

### package.json

```
{
  "name": "kanni",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "license": "MIT",
  "packageManager": "pnpm@11.15.1+sha512.81350b07e53c9538a02f1f2303b4290fa2d7be04e56e2a970c4cc4b417dc761de196edabd49d55c7dc9580db81007c44143e4e3d7e462b3000d23c255122d065",
  "engines": {
    "node": ">=24 <25",
    "pnpm": ">=11 <12"
  },
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack && node scripts/sanitize-standalone-build.mjs",
    "start": "next start",
    "postinstall": "prisma generate",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit",
    "typecheck:compat": "tsc6 --noEmit",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:e2e": "playwright test",
    "db:generate": "prisma generate",
    "db:migrate": "prisma migrate dev",
    "db:migrate:deploy": "prisma migrate deploy",
    "db:studio": "prisma studio --hostname 127.0.0.1 --port 5556",
    "env:production:check": "node --import tsx scripts/validate-production-environment.ts",
    "db:test:prepare": "prisma migrate reset --force",
    "eval": "node --import tsx scripts/run-evals.ts",
    "eval:live": "node --conditions=react-server --import tsx scripts/run-live-evals.ts",
    "recording:prepare": "node --conditions=react-server --import tsx scripts/prepare-recording-flow.ts",
    "recording:review": "node --conditions=react-server --import tsx scripts/review-recording-flow.ts",
    "diagrams": "node --import tsx scripts/render-rich-diagrams.ts",
    "screenshots": "node --import tsx scripts/capture-submission-screenshots.ts"
  },
  "dependencies": {
    "@fontsource/noto-sans": "5.3.0",
    "@fontsource/noto-sans-malayalam": "5.3.0",
    "@openrouter/ai-sdk-provider": "3.0.0",
    "@prisma/adapter-pg": "7.8.0",
    "@prisma/client": "7.8.0",
    "ai": "7.0.31",
    "bcryptjs": "3.0.3",
    "dotenv": "17.4.2",
    "lucide-react": "1.25.0",
    "next": "16.2.10",
    "pg": "8.22.0",
    "react": "19.2.7",
    "react-dom": "19.2.7",
    "server-only": "0.0.1",
    "zod": "4.4.3"
  },
  "devDependencies": {
    "@axe-core/playwright": "4.12.1",
    "@playwright/test": "1.61.1",
    "@tailwindcss/postcss": "4.3.3",
    "@testing-library/jest-dom": "6.9.1",
    "@testing-library/react": "16.3.2",
    "@types/node": "24.13.3",
    "@types/pg": "8.20.0",
    "@types/react": "19.2.17",
    "@types/react-dom": "19.2.3",
    "@typescript/native": "npm:typescript@7.0.2",
    "@vitest/coverage-v8": "4.1.10",
    "eslint": "9.39.5",
    "eslint-config-next": "16.2.10",
    "jsdom": "29.1.1",
    "postcss": "8.5.19",
    "prisma": "7.8.0",
    "tailwindcss": "4.3.3",
    "tsx": "4.23.1",
    "typescript": "npm:@typescript/typescript6@6.0.2",
    "vitest": "4.1.10"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "@fontsource/noto-sans/400.css";
import "@fontsource/noto-sans/600.css";
import "@fontsource/noto-sans/700.css";
import "@fontsource/noto-sans-malayalam/400.css";
import "@fontsource/noto-sans-malayalam/600.css";
import "@fontsource/noto-sans-malayalam/700.css";
import "./globals.css";

import { getRequestLocale } from "@/lib/i18n";

export const metadata: Metadata = {
  title: {
    default: "Kanni | കണ്ണി",
    template: "%s | Kanni",
  },
  description:
    "Kanni connects students, teachers, parents, and school leaders around the next useful learning step.",
};

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const locale = await getRequestLocale();
  return (
    <html lang={locale}>
      <body>{children}</body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import {
  ArrowRight,
  BookOpenCheck,
  CheckCircle2,
  House,
  School,
  UserRoundCog,
} from "lucide-react";
import Link from "next/link";

import { NodesMark } from "@/components/nodes-mark";
import { PublicFooter } from "@/components/public-footer";
import { PublicHeader } from "@/components/public-header";
import { copy, getRequestLocale } from "@/lib/i18n";
import { isInstallationConfigured } from "@/lib/installation";

export default async function HomePage() {
  const [locale, configured] = await Promise.all([
    getRequestLocale(),
    isInstallationConfigured(),
  ]);
  const roles = [
    {
      icon: BookOpenCheck,
      title: copy(locale, { en: "Student", ml: "വിദ്യാർത്ഥി" }),
      detail: copy(locale, {
        en: "Try, ask for support, revise, and explain what changed.",
        ml: "ശ്രമിക്കുക, സഹായം തേടുക, വീണ്ടും ശ്രമിക്കുക, എന്താണ് മാറിയതെന്ന് വിശദീകരിക്കുക.",
      }),
    },
    {
      icon: School,
      title: copy(locale, { en: "Teacher", ml: "അധ്യാപകൻ" }),
      detail: copy(locale, {
        en: "Plan for likely misconceptions, choose support, and review evidence.",
        ml: "സാധ്യമായ തെറ്റിദ്ധാരണകൾ മുൻകൂട്ടി കാണുക, പിന്തുണ തിരഞ്ഞെടുക്കുക, പഠന തെളിവ് പരിശോധിക്കുക.",
      }),
    },
    {
      icon: House,
      title: copy(locale, { en: "Parent", ml: "രക്ഷിതാവ്" }),
      detail: copy(locale, {
        en: "See one clear update and one teacher-reviewed activity for home.",
        ml: "വ്യക്തമായ ഒരു അപ്ഡേറ്റും അധ്യാപകൻ പരിശോധിച്ച ഒരു വീട്ടുപ്രവർത്തനവും കാണുക.",
      }),
    },
    {
      icon: UserRoundCog,
      title: copy(locale, { en: "School", ml: "സ്കൂൾ" }),
      detail: copy(locale, {
        en: "Connect each learner to the responsible adults and monitor handoffs without reading private work.",
        ml: "ഓരോ പഠിതാവിനെയും ഉത്തരവാദിത്തമുള്ള മുതിർന്നവരുമായി ബന്ധിപ്പിച്ച് സ്വകാര്യ പഠനപ്രവർത്തി വായിക്കാതെ കൈമാറ്റങ്ങൾ നിരീക്ഷിക്കുക.",
      }),
    },
  ];

  return (
    <>
      <PublicHeader locale={locale} returnTo="/" />
      <main id="main-content">
        <section className="page-shell product-hero">
          <div className="product-hero-copy">
            <p className="eyebrow">
              {copy(locale, {
                en: "Curriculum-grounded learning studios",
                ml: "പാഠ്യപദ്ധതിയെ അടിസ്ഥാനമാക്കിയ പഠന സ്റ്റുഡിയോകൾ",
              })}
            </p>
            <h1>
              {copy(locale, {
                en: "Teachers prepare the ground. Learners do the thinking.",
                ml: "അധ്യാപകർ പഠനത്തിന് വഴിയൊരുക്കുന്നു. ചിന്തിക്കുന്നത് പഠിതാക്കളാണ്.",
              })}
            </h1>
            <p className="hero-lead">
              {copy(locale, {
                en: "A teacher builds from a source the school is allowed to use. The learner predicts, makes, critiques, revises, and reflects. The teacher then reduces or changes support, and the parent receives one reviewed home activity.",
                ml: "സ്കൂളിന് ഉപയോഗിക്കാൻ അനുമതിയുള്ള ഉറവിടത്തിൽ നിന്ന് അധ്യാപകൻ പ്രവർത്തനം തയ്യാറാക്കുന്നു. പഠിതാവ് പ്രവചിക്കുന്നു, നിർമ്മിക്കുന്നു, വിമർശിക്കുന്നു, തിരുത്തുന്നു, ചിന്തിക്കുന്നു. തുടർന്ന് അധ്യാപകൻ പിന്തുണ കുറയ്ക്കുകയോ മാറ്റുകയോ ചെയ്യുന്നു. രക്ഷിതാവിന് പരിശോധിച്ച ഒരു വീട്ടുപ്രവർത്തനം ലഭിക്കുന്നു.",
              })}
            </p>
            <div className="hero-actions">
              <Link className="button primary" href={configured ? "/login" : "/setup"}>
                {configured
                  ? copy(locale, { en: "Sign in to Kanni", ml: "കണ്ണിയിൽ സൈൻ ഇൻ ചെയ്യുക" })
                  : copy(locale, { en: "Set up your school", ml: "നിങ്ങളുടെ സ്കൂൾ സജ്ജമാക്കുക" })}
                <ArrowRight size={19} aria-hidden="true" />
              </Link>
              <a className="button secondary" href="#how-it-works">
                {copy(locale, { en: "See how it works", ml: "ഇത് എങ്ങനെ പ്രവർത്തിക്കുന്നു" })}
              </a>
            </div>
            <ul className="hero-proof-list">
              <li>
                <CheckCircle2 aria-hidden="true" />
                {copy(locale, { en: "Teacher-requested AI only", ml: "അധ്യാപകൻ ആവശ്യപ്പെടുമ്പോൾ മാത്രം AI" })}
              </li>
              <li>
                <CheckCircle2 aria-hidden="true" />
                {copy(locale, { en: "Source IDs checked before use", ml: "ഉപയോഗത്തിന് മുമ്പ് ഉറവിട ഐഡികൾ പരിശോധിക്കുന്നു" })}
              </li>
              <li>
                <CheckCircle2 aria-hidden="true" />
                {copy(locale, { en: "Scaffolds fade by teacher decision", ml: "അധ്യാപക തീരുമാനപ്രകാരം പിന്തുണ കുറയുന്നു" })}
              </li>
            </ul>
          </div>
          <div className="product-hero-visual" aria-label="Connected learning support circle">
            <div className="hero-mark-wrap"><NodesMark className="hero-mark" /></div>
            <div className="support-node support-node-student"><BookOpenCheck aria-hidden="true" /><span>{roles[0].title}</span></div>
            <div className="support-node support-node-teacher"><School aria-hidden="true" /><span>{roles[1].title}</span></div>
            <div className="support-node support-node-parent"><House aria-hidden="true" /><span>{roles[2].title}</span></div>
            <div className="support-node support-node-school"><UserRoundCog aria-hidden="true" /><span>{roles[3].title}</span></div>
            <p>{copy(locale, { en: "One source. Several learner routes. One reviewed loop.", ml: "ഒരു ഉറവിടം. പല പഠനവഴികൾ. പരിശോധിച്ച ഒരു പഠനചക്രം." })}</p>
          </div>
        </section>

        <section className="role-value-section">
          <div className="page-shell">
            <div className="section-heading centered-heading">
              <p className="eyebrow">{copy(locale, { en: "Built around responsibility", ml: "ഉത്തരവാദിത്തത്തെ കേന്ദ്രീകരിച്ച്" })}</p>
              <h2>{copy(locale, { en: "One platform, four different jobs", ml: "ഒരു പ്ലാറ്റ്ഫോം, നാല് വ്യത്യസ്ത ചുമതലകൾ" })}</h2>
            </div>
            <div className="role-valu
[truncated — 3374 more characters]
```

### app/parent/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function ParentPage() {
  redirect("/portal/parent");
}

```

### app/teacher/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function TeacherPage() {
  redirect("/portal/teacher");
}

```

### app/portal/layout.tsx

```typescript
import type { ReactNode } from "react";

export const dynamic = "force-dynamic";

export default function PortalLayout({ children }: { children: ReactNode }) {
  return children;
}

```

### app/portal/page.tsx

```typescript
import { redirect } from "next/navigation";

import { homeForRole, requireActor } from "@/lib/auth";

export default async function PortalPage() {
  const actor = await requireActor();
  redirect(homeForRole(actor.role));
}

```

[106 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]