# Manabou — Full Codebase Analysis (July 2026)

Scope: `backend/` (Laravel 13 API) and `frontend/` (Next.js 16 App Router).
This complements `backend/optimize-plan.md` (the ~6-week refactor plan from the earlier audit). Section 1 records what from that plan is already done; everything after that is **new findings**.

---

## 1. Status of the previous refactor plan

Most of the earlier plan has shipped — good progress:

| Plan item | Status |
| --- | --- |
| Kill dual API client (`lib/api.ts` + `hooks/use-api.ts`) | ✅ Deleted; single client in `lib/api/client.ts` |
| TanStack Query + `hooks/queries/*` + `lib/query-keys.ts` | ✅ Done |
| Turbopack build, `ignoreBuildErrors: false`, `optimizePackageImports`, bundle analyzer | ✅ Done in `next.config.ts` |
| Root `loading.tsx` / `error.tsx` / `not-found.tsx` | ✅ Present |
| Modularize `routes/api.php` (was 273 lines) | ✅ Now a 48-line aggregator + 15 files in `routes/v1/` |
| Form Requests / API Resources | 🟡 Started (7 Requests, 3 Resources) — most controllers still validate/serialize inline |
| Remove `sleep(3)` in `RoundTimeoutJob` | ✅ Gone |
| Split `app/review/page.tsx` (was 3,607 lines) | 🟡 Split to 1,136 — but the weight moved into `components/review/review-provider.tsx` (2,973 lines, see §4.1) |
| Rate limiting | 🟡 Good on battle/detective/games writes; **missing on auth endpoints** (see §2.3) |
| Navbar split / RSC conversion (Phase 5) | ❌ Not done — navbar is now 1,083 lines; all pages still `'use client'` |
| Cleanup pass (Phase 7: CSS, modal base, dead files) | ❌ Not done |

---

## 2. Security findings (fix these first)

### 2.1 HIGH — Public unauthenticated WhatsApp send endpoint
`backend/routes/v1/public.php` registers `GET /api/v1/test-fonnte` with **no auth and no throttle**. Anyone who discovers the URL can send WhatsApp messages to **any phone number** using your Fonnte token:

```
GET /api/v1/test-fonnte?target=628xxxxxxxx
```

Consequences: spam abuse from your business number, Fonnte quota/billing burn, number reputation damage, and the response leaks whether the token is configured.

**Fix:** delete the route (it's a leftover smoke test). If you need it for dev, wrap in `if (app()->environment('local'))` or move behind `auth:sanctum` + an admin gate. The near-duplicate `backend/routes/debug_fonnte.php` is not registered anywhere but is tracked in git and even prints a token preview — delete it too.

### 2.2 HIGH — Path traversal in `GeoImageController`
`routes/v1/public.php`:

```php
Route::get('/geo/images/{filename}', [GeoImageController::class, 'show'])
    ->where('filename', '.*');   // ← allows slashes
```

and the controller concatenates unchecked:

```php
$path = storage_path("app/public/geo/{$filename}");
return response()->file($path);
```

A request like `/api/v1/geo/images/..%2F..%2F..%2F..%2F.env` (depending on server URL normalization) can serve arbitrary files readable by PHP — including `backend/.env` with the OpenAI key, DB credentials, and Reverb secrets.

**Fix (small):**

```php
public function show(string $filename): BinaryFileResponse
{
    if (!preg_match('/^[A-Za-z0-9._-]+$/', $filename) || str_contains($filename, '..')) {
        abort(404);
    }
    $path = storage_path('app/public/geo/' . $filename);
    ...
}
```

and drop the `.*` route constraint (or change it to `[A-Za-z0-9._-]+`).

### 2.3 MEDIUM — No rate limiting on auth endpoints
`/auth/login`, `/auth/register`, `/auth/forgot-password`, `/auth/reset-password` have no `throttle` middleware — open to credential stuffing and password-reset email/WhatsApp flooding. The WhatsApp OTP routes (`/auth/whatsapp/send|verify|resend`) are authenticated but also unthrottled, so a logged-in user can drain Fonnte quota.

**Fix:** `->middleware('throttle:5,1')` on login/register/forgot/reset; `throttle:3,1` on OTP send/resend; `throttle:10,1` on verify. One-line changes in `routes/v1/public.php` and `routes/api.php`.

### 2.4 LOW — `give-coins.php` tracked at backend root
A bootstrap script that grants coins to a hard-coded user ID. It isn't web-accessible (outside `public/`), but it's tracked in git and would run if ever copied into a web root. Replace with an Artisan command (`php artisan nihongo:give-coins {user} {amount}`) or delete.

### 2.5 LOW — Unauthenticated push-open tracking
`POST /push/opened/{id}` is public and unthrottled — an attacker can spoof open events and pollute notification analytics. Add `throttle:60,1` and/or a signed URL (`URL::signedRoute`) embedded in the push payload.

---

## 3. Bugs & robustness

### 3.1 OpenAI HTTP calls have no explicit timeout
In `app/Services/AiProviderService.php`, the Gemini paths use `Http::timeout(config('services.gemini.timeout', 120))`, but the OpenAI paths rely on Laravel's default 30s timeout — `gpt-4o` generations (detective cases, long sentences) can exceed that and surface as opaque `ConnectionException`s. Add a `services.openai.timeout` config (60–120s) and `->retry(2, 500)` for idempotent generations, mirroring the Gemini path.

### 3.2 Per-item serialization in `DailyMixController`
`DailyMixController` (536 lines) hand-builds card arrays inside `->map()` closures in two places (lines ~61 and ~457) — duplicated shapes that will drift. The `CardReviewResource` from Phase 3 already exists; use it in both spots so the wire format has one source of truth. Same pattern in `KanjiController::class` (line ~111) and `WordController` (line ~260).

### 3.3 Frontend build artifacts and scratch files tracked in git
`frontend/tmp-diff-main.txt` and `frontend/tmp-main-review-modals.tsx` (a full stale copy of the review modals) are committed. The stale `.tsx` copy is compiled by `tsc` (it's under `include: **/*.tsx`) and is a trap for future edits to the wrong file. Delete both, add `tmp-*` and `deploy.zip` to `.gitignore`.

### 3.4 `next.config.ts` TODO — production image host
`images.remotePatterns` still only allows `localhost`/`127.0.0.1` for backend storage (line 27's TODO). Mnemonic images and geo images served from the production Laravel host will fail through `next/image` once deployed. Add the production hostname.

---

## 4. Optimization & code reduction

### 4.1 Unify the three parallel review experiences (biggest win)
There are three separately implemented SRS review flows:

| Flow | Files | ~Lines |
| --- | --- | --- |
| Vocab review | `app/review/page.tsx` + `components/review/review-provider.tsx` + `review-modals.tsx` + `review-card.tsx` | ~5,900 |
| Kanji review | `app/kanji/review/page.tsx` + `app/kanji/review/mcq/page.tsx` | ~1,100 |
| Sentence review | `app/sentences/review/page.tsx` | ~630 |

All three implement the same loop: fetch queue → show prompt → reveal → grade (again/hard/good/easy) → advance → session summary. The SM-2 grading, keyboard shortcuts, reveal state, progress bar, and completion screen are re-implemented in each. Extract a generic `<SrsSession>` orchestrator (queue source, card renderer, and grade mutation passed as props/slots) and each flow becomes a thin adapter. Estimated net reduction: 2,000–3,000 lines, plus future review types (grammar SRS, §6.2) become nearly free.

`components/review/review-provider.tsx` at **2,973 lines** is now the single worst file in the codebase — the earlier "split the review page" phase mostly relocated the problem. Apply the same `useReducer` + sub-hook decomposition the old plan prescribed for `page.tsx`.

### 4.2 Remaining oversized files (>800 lines)
`app/profile/page.tsx` (1,347), `app/shadowing/[audioKey]/page.tsx` (1,332), `app/onboarding/page.tsx` (1,146), `components/navbar.tsx` (1,083), `app/battle/[id]/page.tsx` (1,007), `app/kanji/page.tsx` (863), `components/ui/kanji-detail-modal.tsx` (856), `app/assessment/[level]/page.tsx` (848). The old plan's ESLint `max-lines` budget was never added — add it now (`warn` at 500, `error` at 1,000) so this list stops growing.

### 4.3 Backend: finish Resources/Form Requests rollout
7 Form Requests and 3 Resources exist, but 40+ controllers still validate inline. Highest-value next targets by size and traffic: `Admin/AdminAssessmentController` (540), `DailyMixController` (536), `KanjiController` (496), `WordController` (464), `BattleController` (476). This also unblocks deleting most of `frontend/types/mappers/` (old plan Phase 7).

### 4.4 Queue/cache/session all on `database` driver
`.env.example` ships `QUEUE_CONNECTION=database`, `CACHE_STORE=database`, `SESSION_DRIVER=database`. Fine for dev; in production every AI job, cache hit, and battle event competes with app queries on the same MySQL connection. Battles are latency-sensitive (Reverb broadcasts fired from queued jobs). Move at least queue + cache to Redis in production — config-only change.

### 4.5 Frontend tests: zero
Backend has 373 test methods; frontend has **no test runner at all** (no jest/vitest/playwright in `package.json`). The old plan's Phase 4 called for Playwright smoke tests before the review split — still the right call, and prerequisite for the §4.1 unification. Minimum viable suite: login → dashboard → review 3 cards → grade → summary; battle join; deck CRUD.

---

## 5. Smaller improvements

- **Per-route `loading.tsx`**: only the root one exists. Add skeletons for `/review`, `/dashboard`, `/decks`, `/kanji` (hot routes).
- **`server.js` + `deploy:cpanel` script vs Vercel**: the repo carries both a cPanel deploy path and Vercel deployment. Pick one as canonical and document it in the README; delete the other or mark it legacy (stale deploy paths cause the kind of `api/`-folder surprise that broke the Vercel deploy).
- **`routes/web.php` admin login without throttle** (`POST /login`) — same fix as §2.3.
- **`docs/` and `MONITORING_TOOLS_MANUAL_TESTING.md`** — worth consolidating into `docs/` so the backend root stops accumulating loose files (`nihongo_kotoba.csv`, postman collection, etc.).
- **Model naming drift**: `Reading` vs `ReadingPassage` vs `ReadingSession` vs `ReadingPracticeAttempt`, and both `DetectiveProgress` and `UserDetectiveProgress` exist. Worth a short ADR on naming (`User*` prefix = per-user state) before the model count (85+) grows further.

---

## 6. Feature suggestions

Grounded in what already exists (PWA + push, SRS engine, AI provider tiers, WebSocket battles, game economy):

1. **Offline review sync** — the PWA already caches shadowing audio offline. Extend to reviews: queue grades in IndexedDB (a wrapper already exists at `lib/indexed-db.ts`) when offline and replay them against `POST /reviews` on reconnect. Commuter studying is the core use case for a Japanese-learning app.
2. **Grammar SRS** — grammar points (`GrammarPoint`, `UserGrammarStat`) track accuracy but aren't scheduled. Once §4.1's generic `<SrsSession>` exists, adding a grammar queue to `ReviewQueueService` reuses the whole review UI.
3. **Daily review reminder via existing channels** — you already have Web Push *and* a WhatsApp integration (Fonnte). A "you have N cards due" nightly notification (user-configurable channel/time) is a proven retention lever; the `NotificationLog` model suggests half the plumbing exists.
4. **Anki import/export** — `.apkg` is a SQLite zip; an import pipeline into `Deck`/`Card` would dramatically lower switching cost for the largest pool of prospective users. Export keeps trust ("your data isn't locked in").
5. **Listening-first review mode** — cards already carry audio; add a review mode where the prompt is audio-only and the user types what they hear (dictation). Pairs with the existing wanakana IME binding, exercises a skill JLPT tests heavily.
6. **Battle spectator/replay** — battles already broadcast every round over Reverb; persisting the event stream per battle enables replays and shareable results, feeding the existing invite system.
7. **Public deck sharing** — `DeckFavoriteController` exists; a `is_public` flag + browse page turns user decks into community content with minimal backend work.

---

## 7. Suggested priority order

| # | Item | Effort | Type | Status |
| --- | --- | --- | --- | --- |
| 1 | Delete `/test-fonnte` route + `debug_fonnte.php` (§2.1) | minutes | security | ✅ done 2026-07-02 (also removed `give-coins.php`) |
| 2 | Fix geo image path traversal (§2.2) | minutes | security | ✅ done — route constraint + controller validation |
| 3 | Throttle auth/OTP endpoints (§2.3, §5) | <1h | security | ✅ done — login/register/forgot/reset 5/min, OTP send/resend 3/min, verify/init 10/min, push-opened 60/min, admin web login 5/min |
| 4 | Delete tracked tmp/scratch files, gitignore them (§3.3, §2.4) | <1h | hygiene | ✅ done — also deleted `deploy.zip`, ignored `tmp-*`/`tsconfig.tsbuildinfo` |
| 5 | OpenAI timeout + retry (§3.1) | <1h | robustness | ✅ done — facade timeout was already 120s; fixed the raw-HTTP path (`callOpenAIRaw`) |
| 6 | Production image host in `next.config.ts` (§3.4) | minutes | bug | ✅ done — `nihongo-api.jabbar.id` |
| 7 | Playwright smoke suite (§4.5) | 1–2d | testing | ✅ done 2026-07-02 — `frontend/e2e/` (5 tests, all passing): auth setup via storage state, dashboard, decks, full review golden path (start Pomodoro session → answer card → grade → advance), unauthenticated login page. Run with `bun run test:e2e`; fixtures auto-seed via `php artisan nihongo:e2e-seed` (new command); servers auto-start (backend :8000, frontend prod build :3100) |
| 8 | Unify review flows behind `<SrsSession>` (§4.1) | 4–6d | optimization | 🟡 phase 1 done 2026-07-02 — shared `hooks/use-srs-session.ts` (generic queue/reveal/grade/summary state machine) + `components/srs/session-screens.tsx`; kanji and sentence review migrated onto it (e2e-verified, also fixed the `?mode=cram` first-load bug). **Remaining:** migrate vocab review (`components/review/review-provider.tsx`, 2,973 lines) onto the same hook — the largest and riskiest piece, needs its own effort with the smoke suite as the net |
| 9 | Finish Resources/Requests rollout (§4.3) | 2–3d | maintainability | 🟡 review-path done 2026-07-02 — 8 new Form Requests (kanji review/MCQ, review sync, sentence review, card store/update, profile update) wired into KanjiController, ReviewController, SentenceReviewController, WordController, ProfileController; verified via backend tests + e2e. **Remaining:** DetectiveController (10 inline validates), CanDoController (5), admin controllers, and the Resource side (DailyMix/Kanji/Word `->map()` shapes intentionally left — their wire formats diverge from `CardReviewResource`, so each needs its own Resource, not a forced reuse) |
| 10 | Redis for queue/cache in prod (§4.4) | <1d | performance | 🟡 documented in `.env.example`; actual switch is a production env change |
| 11 | Grammar SRS + offline sync (§6.1–6.2) | 1–2w | features | ⬜ pending |

Also done 2026-07-02: ESLint `max-lines` budget (warn at 500) in `eslint.config.mjs`; `app/kanji/loading.tsx` (review/dashboard/decks already existed); removed `NEXT_PUBLIC_VAPID_PRIVATE_KEY` from `frontend/.env.production` (a Web Push **private** key must live only on the backend; it was unreferenced in frontend code, but `NEXT_PUBLIC_` vars get inlined into the client bundle if ever used).

**Pre-existing issues discovered while verifying (not caused by these changes):**
- Backend test suite: 73 of 379 tests fail on a clean tree — most from `Class "Database\Seeders\RolePermissionSeeder" does not exist`. Fix the seeder reference and re-run before trusting CI.
- Frontend lint: 252 pre-existing errors (205 × `@typescript-eslint/no-explicit-any`) — `bun run lint` was already failing before the `max-lines` rule was added.
