diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..5624399 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,10 @@ +# Copy to .env.local and replace placeholder values. +AUTH_GOOGLE_ID=your-google-oauth-client-id +AUTH_GOOGLE_SECRET=your-google-oauth-client-secret +AUTH_SECRET=generate-with-openssl-rand-base64-32 +AUTH_URL=http://localhost:3000 + +# Optional: set to false/0/no/off to disable saving new vocabulary rows. +NEXT_PUBLIC_VOCAB_SAVING_ENABLED=true +# Optional server-side override (if unset, NEXT_PUBLIC_VOCAB_SAVING_ENABLED is used). +VOCAB_SAVING_ENABLED=true diff --git a/.gitignore b/.gitignore index 5ef6a52..b721bff 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.local.example # vercel .vercel diff --git a/.project/overview.md b/.project/overview.md index 8ec2fe9..00da7be 100644 --- a/.project/overview.md +++ b/.project/overview.md @@ -27,7 +27,7 @@ translation and linguistic context as you read. ### [002-build-v2-mvp](goals/002-build-v2-mvp.md) — active -- [google-sheets-adapter](tasks/task-006_google-sheets-adapter.md) — in-progress +- [google-sheets-adapter](tasks/task-006_google-sheets-adapter.md) — in-review - [oauth-token-refresh](tasks/task-007_oauth-token-refresh.md) — to-do - [vocab-sheet-connect-ux](tasks/task-008_vocab-sheet-connect-ux.md) — to-do - [save-failure-notifications](tasks/task-009_save-failure-notifications.md) — to-do diff --git a/.project/tasks/task-006_google-sheets-adapter.md b/.project/tasks/task-006_google-sheets-adapter.md index 9444ca4..45d4563 100644 --- a/.project/tasks/task-006_google-sheets-adapter.md +++ b/.project/tasks/task-006_google-sheets-adapter.md @@ -1,5 +1,5 @@ --- -status: in-progress +status: in-review owner: both goal: "[[002-build-v2-mvp]]" --- @@ -26,7 +26,7 @@ interfaces are already in place — this task designs the sheet and builds the a via `x-vocab-sheet-id` header) → adapter. Sheet chosen via "Vocab sheet" field in nav. - [x] Verify: sign in → save a word while reading → open Google Sheet → row appears. Confirmed live by [human] 2026-06-13. -- [ ] Add an env flag to enable/disable saving (default on), so the user can read without +- [x] Add an env flag to enable/disable saving (default on), so the user can read without writing to the sheet. (Impl detail: client `NEXT_PUBLIC_*` vs server-side gate — decide when building.) @@ -68,6 +68,9 @@ A word saved while reading shows up as a row in the user's Google Sheet. - 2026-06-13: Reopened (in-review → in-progress) to add an env on/off toggle for saving [human request]. The core sheet-save flow stays verified; only the toggle is outstanding. Sheet-connect UX + validation split to [[task-008_vocab-sheet-connect-ux]]. +- 2026-06-13: Added env save-toggle (`NEXT_PUBLIC_VOCAB_SAVING_ENABLED`, optional server + override `VOCAB_SAVING_ENABLED`) with default-on behavior. When disabled, client skips + save requests and server POST short-circuits safely. ### ⏸ PARKED 2026-06-13 — resume here (next agent, read this first) - **Only remaining step in this task:** the env on/off toggle for saving (see Steps). The diff --git a/README.md b/README.md index 0b12033..929cbe4 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,17 @@ A personal Finnish learning app — read Finnish text with interactive word-by-w ```bash npm install +cp .env.local.example .env.local npm run dev ``` Open [http://localhost:3000](http://localhost:3000). +### Environment + +- `NEXT_PUBLIC_VOCAB_SAVING_ENABLED` (default: `true`) — build-time client flag; set `false`/`0`/`no`/`off` to read without writing new vocabulary rows (requires rebuilding when changed). +- `VOCAB_SAVING_ENABLED` (optional server override) — runtime server flag with the same values; useful to force-disable POST saves without relying on a client rebuild. + ### Build for Production ```bash diff --git a/src/app/api/vocab/route.ts b/src/app/api/vocab/route.ts index c3d3e4c..dd9bdeb 100644 --- a/src/app/api/vocab/route.ts +++ b/src/app/api/vocab/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { auth } from '@/lib/auth'; import { createGoogleSheetsVocabRepository } from '@/modules/vocab-store/adapters/GoogleSheetsVocabRepository'; +import { isServerVocabSavingEnabled } from '@/modules/vocab-store/vocabSavingFlag'; const SHEET_ID_HEADER = 'x-vocab-sheet-id'; @@ -19,6 +20,10 @@ export async function POST(request: Request) { const session = await auth(); if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + if (!isServerVocabSavingEnabled()) { + return NextResponse.json({ ok: true, skipped: 'disabled' }); + } + const sheetId = request.headers.get(SHEET_ID_HEADER); if (!sheetId) return NextResponse.json({ error: 'No vocabulary sheet configured' }, { status: 400 }); diff --git a/src/modules/vocab-store/saveVocab.ts b/src/modules/vocab-store/saveVocab.ts index 4191489..d1fddaa 100644 --- a/src/modules/vocab-store/saveVocab.ts +++ b/src/modules/vocab-store/saveVocab.ts @@ -1,4 +1,5 @@ import { getVocabSheetId } from './sheetSettings'; +import { isClientVocabSavingEnabled } from './vocabSavingFlag'; const SHEET_ID_HEADER = 'x-vocab-sheet-id'; @@ -8,6 +9,8 @@ export async function saveVocab( sourceLang: string, targetLang: string, ): Promise { + if (!isClientVocabSavingEnabled()) return; + const sheetId = getVocabSheetId(); // No sheet configured yet — nothing to save to. Reading is unaffected. if (!sheetId) return; diff --git a/src/modules/vocab-store/vocabSavingFlag.ts b/src/modules/vocab-store/vocabSavingFlag.ts new file mode 100644 index 0000000..c7ec83f --- /dev/null +++ b/src/modules/vocab-store/vocabSavingFlag.ts @@ -0,0 +1,13 @@ +const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +const isEnabled = (value: string | undefined): boolean => { + // Default-on behavior: missing env values keep vocabulary saving enabled. + if (!value) return true; + return !FALSE_ENV_VALUES.has(value.trim().toLowerCase()); +}; + +export const isClientVocabSavingEnabled = (): boolean => + isEnabled(process.env.NEXT_PUBLIC_VOCAB_SAVING_ENABLED); + +export const isServerVocabSavingEnabled = (): boolean => + isEnabled(process.env.VOCAB_SAVING_ENABLED ?? process.env.NEXT_PUBLIC_VOCAB_SAVING_ENABLED);