From 275c7273528914c5f6afbce36bcd981bb3a24430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:39:46 +0000 Subject: [PATCH 1/3] Add env toggle for vocab saving --- .env.local.example | 10 ++++++++++ .gitignore | 1 + .project/overview.md | 2 +- .project/tasks/task-006_google-sheets-adapter.md | 7 +++++-- README.md | 6 ++++++ src/app/api/vocab/route.ts | 11 +++++++++++ src/modules/vocab-store/saveVocab.ts | 9 +++++++++ 7 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 .env.local.example 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..2d24e1f 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`) — set `false`/`0`/`no`/`off` to read without writing new vocabulary rows. +- `VOCAB_SAVING_ENABLED` (optional server override) — same values as above. + ### Build for Production ```bash diff --git a/src/app/api/vocab/route.ts b/src/app/api/vocab/route.ts index c3d3e4c..e6fcef8 100644 --- a/src/app/api/vocab/route.ts +++ b/src/app/api/vocab/route.ts @@ -3,6 +3,13 @@ import { auth } from '@/lib/auth'; import { createGoogleSheetsVocabRepository } from '@/modules/vocab-store/adapters/GoogleSheetsVocabRepository'; const SHEET_ID_HEADER = 'x-vocab-sheet-id'; +const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +const isVocabSavingEnabled = (): boolean => { + const envValue = process.env.VOCAB_SAVING_ENABLED ?? process.env.NEXT_PUBLIC_VOCAB_SAVING_ENABLED; + if (!envValue) return true; + return !FALSE_ENV_VALUES.has(envValue.trim().toLowerCase()); +}; export async function GET(request: Request) { const session = await auth(); @@ -19,6 +26,10 @@ export async function POST(request: Request) { const session = await auth(); if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); + if (!isVocabSavingEnabled()) { + 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..873aa51 100644 --- a/src/modules/vocab-store/saveVocab.ts +++ b/src/modules/vocab-store/saveVocab.ts @@ -1,6 +1,13 @@ import { getVocabSheetId } from './sheetSettings'; const SHEET_ID_HEADER = 'x-vocab-sheet-id'; +const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +const isVocabSavingEnabled = (): boolean => { + const envValue = process.env.NEXT_PUBLIC_VOCAB_SAVING_ENABLED; + if (!envValue) return true; + return !FALSE_ENV_VALUES.has(envValue.trim().toLowerCase()); +}; export async function saveVocab( word: string, @@ -8,6 +15,8 @@ export async function saveVocab( sourceLang: string, targetLang: string, ): Promise { + if (!isVocabSavingEnabled()) return; + const sheetId = getVocabSheetId(); // No sheet configured yet — nothing to save to. Reading is unaffected. if (!sheetId) return; From 5f5562fb682ec50fd7748d27a4bccd2d2dbe8b90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:42:22 +0000 Subject: [PATCH 2/3] Apply validation follow-ups --- README.md | 2 +- src/app/api/vocab/route.ts | 10 ++-------- src/modules/vocab-store/saveVocab.ts | 10 ++-------- src/modules/vocab-store/vocabSavingFlag.ts | 12 ++++++++++++ 4 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 src/modules/vocab-store/vocabSavingFlag.ts diff --git a/README.md b/README.md index 2d24e1f..8ff7447 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Open [http://localhost:3000](http://localhost:3000). ### Environment - `NEXT_PUBLIC_VOCAB_SAVING_ENABLED` (default: `true`) — set `false`/`0`/`no`/`off` to read without writing new vocabulary rows. -- `VOCAB_SAVING_ENABLED` (optional server override) — same values as above. +- `VOCAB_SAVING_ENABLED` (optional server override) — same values as above; useful if you want to force-disable saves on the server without relying on a client build-time env value. ### Build for Production diff --git a/src/app/api/vocab/route.ts b/src/app/api/vocab/route.ts index e6fcef8..dd9bdeb 100644 --- a/src/app/api/vocab/route.ts +++ b/src/app/api/vocab/route.ts @@ -1,15 +1,9 @@ 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'; -const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); - -const isVocabSavingEnabled = (): boolean => { - const envValue = process.env.VOCAB_SAVING_ENABLED ?? process.env.NEXT_PUBLIC_VOCAB_SAVING_ENABLED; - if (!envValue) return true; - return !FALSE_ENV_VALUES.has(envValue.trim().toLowerCase()); -}; export async function GET(request: Request) { const session = await auth(); @@ -26,7 +20,7 @@ export async function POST(request: Request) { const session = await auth(); if (!session) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); - if (!isVocabSavingEnabled()) { + if (!isServerVocabSavingEnabled()) { return NextResponse.json({ ok: true, skipped: 'disabled' }); } diff --git a/src/modules/vocab-store/saveVocab.ts b/src/modules/vocab-store/saveVocab.ts index 873aa51..d1fddaa 100644 --- a/src/modules/vocab-store/saveVocab.ts +++ b/src/modules/vocab-store/saveVocab.ts @@ -1,13 +1,7 @@ import { getVocabSheetId } from './sheetSettings'; +import { isClientVocabSavingEnabled } from './vocabSavingFlag'; const SHEET_ID_HEADER = 'x-vocab-sheet-id'; -const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); - -const isVocabSavingEnabled = (): boolean => { - const envValue = process.env.NEXT_PUBLIC_VOCAB_SAVING_ENABLED; - if (!envValue) return true; - return !FALSE_ENV_VALUES.has(envValue.trim().toLowerCase()); -}; export async function saveVocab( word: string, @@ -15,7 +9,7 @@ export async function saveVocab( sourceLang: string, targetLang: string, ): Promise { - if (!isVocabSavingEnabled()) return; + if (!isClientVocabSavingEnabled()) return; const sheetId = getVocabSheetId(); // No sheet configured yet — nothing to save to. Reading is unaffected. diff --git a/src/modules/vocab-store/vocabSavingFlag.ts b/src/modules/vocab-store/vocabSavingFlag.ts new file mode 100644 index 0000000..87ded57 --- /dev/null +++ b/src/modules/vocab-store/vocabSavingFlag.ts @@ -0,0 +1,12 @@ +const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +const isEnabled = (value: string | undefined): boolean => { + 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); From 19372a9612325ca6182139f93d656c918c961de0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:43:22 +0000 Subject: [PATCH 3/3] Clarify env toggle behavior docs --- README.md | 4 ++-- src/modules/vocab-store/vocabSavingFlag.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ff7447..929cbe4 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Open [http://localhost:3000](http://localhost:3000). ### Environment -- `NEXT_PUBLIC_VOCAB_SAVING_ENABLED` (default: `true`) — set `false`/`0`/`no`/`off` to read without writing new vocabulary rows. -- `VOCAB_SAVING_ENABLED` (optional server override) — same values as above; useful if you want to force-disable saves on the server without relying on a client build-time env value. +- `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 diff --git a/src/modules/vocab-store/vocabSavingFlag.ts b/src/modules/vocab-store/vocabSavingFlag.ts index 87ded57..c7ec83f 100644 --- a/src/modules/vocab-store/vocabSavingFlag.ts +++ b/src/modules/vocab-store/vocabSavingFlag.ts @@ -1,6 +1,7 @@ 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()); };