Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ yarn-error.log*

# env files (can opt-in for committing if needed)
.env*
!.env.local.example

# vercel
.vercel
Expand Down
2 changes: 1 addition & 1 deletion .project/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ translation and linguistic context as you read.

### [002-build-v2-mvp](goals/002-build-v2-mvp.md) — active
<!-- - [google-oauth-flow](tasks/task-005_google-oauth-flow.md) — done 2026-06-13 -->
- [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
Expand Down
7 changes: 5 additions & 2 deletions .project/tasks/task-006_google-sheets-adapter.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
status: in-progress
status: in-review
owner: both
goal: "[[002-build-v2-mvp]]"
---
Expand All @@ -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.)

Expand Down Expand Up @@ -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.
Comment on lines +71 to +73

### ⏸ 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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/app/api/vocab/route.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 });

Expand Down
3 changes: 3 additions & 0 deletions src/modules/vocab-store/saveVocab.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getVocabSheetId } from './sheetSettings';
import { isClientVocabSavingEnabled } from './vocabSavingFlag';

const SHEET_ID_HEADER = 'x-vocab-sheet-id';

Expand All @@ -8,6 +9,8 @@ export async function saveVocab(
sourceLang: string,
targetLang: string,
): Promise<void> {
if (!isClientVocabSavingEnabled()) return;

const sheetId = getVocabSheetId();
// No sheet configured yet — nothing to save to. Reading is unaffected.
if (!sheetId) return;
Expand Down
13 changes: 13 additions & 0 deletions src/modules/vocab-store/vocabSavingFlag.ts
Original file line number Diff line number Diff line change
@@ -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);