Skip to content

Move PHOTO-TEXTE to Cloudflare and refine bilingual workflow - #33

Open
mani1261790 wants to merge 11 commits into
mainfrom
dev
Open

Move PHOTO-TEXTE to Cloudflare and refine bilingual workflow#33
mani1261790 wants to merge 11 commits into
mainfrom
dev

Conversation

@mani1261790

@mani1261790 mani1261790 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • rebuild the entry editor as a responsive four-step bilingual workflow
  • add manual French learning annotations with the requested green/orange/yellow rules and independent known-range boxes
  • add matching PPTX and PDF exports with embedded Japanese font support
  • move language switching to the global header and polish multilingual Gothic typography, navigation states, and app branding
  • migrate authentication, relational data, and object storage from Supabase to Better Auth, Cloudflare D1, and R2
  • retain existing-user passwords through a first-login Supabase credential bridge
  • deploy a dedicated Cloudflare dev Worker while sharing production D1/R2 data as requested
  • replace the Worker-side OpenAI SDK path with a Cloudflare-native Responses API call so French/Japanese generation works consistently

Deployment model

  • production Worker: photo-texte
  • dev Worker: photo-texte-dev
  • dev and production share D1 photo-texte and R2 photo-texte-content
  • dev deploys no longer auto-apply migrations to the shared database
  • legacy dev-only D1/R2 resources are retained and not deleted

Validation

  • npm test -- --run (19 tests)
  • npx tsc --noEmit
  • npm run build
  • npm run cf:build:dev
  • Cloudflare dev deployment and HTTP smoke checks
  • live signup/upload/entry/French-to-Japanese/Japanese-to-French/account-cleanup flow
  • shared D1 counts unchanged after smoke test (9 users, 25 entries)
  • desktop and 375px browser checks in Japanese and French
  • header font measured at 14px in both languages; hover/focus styling verified
  • PWA manifest and 512px icon verified from the deployed Worker
  • visual comparison of direct PDF output and PPTX-converted PDF

Summary by CodeRabbit

  • New Features

    • Added PDF export alongside PPTX, with improved filenames and download support.
    • Introduced a bilingual, per-photo entry editor with previews, navigation, text editing, and translation.
    • Added correction annotations, colored highlights, and known-range marking.
    • Added Japanese/French language switching with saved preferences.
    • Added authenticated photo and export downloads.
  • Improvements

    • Updated diff comparisons, responsive layouts, accessibility, navigation, and branding.
    • Improved image processing and export presentation quality.
  • Authentication

    • Added streamlined sign-in, sign-up, session handling, and legacy password migration.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
photo-texte Ready Ready Preview Aug 9, 2026 4:39pm

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR migrates the application to Cloudflare and Better Auth, replaces learning highlights with correction annotations, adds a staged per-photo editor, introduces PDF export, updates PPTX export, persists language selection, and enforces photo state transitions.

Changes

Cloudflare platform and authentication

Layer / File(s) Summary
Cloudflare runtime, storage, and authentication
cloudflare/migrations/*, lib/cloudflare/*, lib/auth/*, scripts/migrate-supabase-to-cloudflare.mjs, wrangler.jsonc, package.json
Adds D1/R2 clients, Better Auth sessions, legacy password migration, signed object access, image sanitization, Cloudflare deployment configuration, and Supabase migration tooling.
Application API migration
app/api/*, lib/api/fetcher.ts, app/api/me/route.ts, app/login/page.tsx, components/LanguageProvider.tsx, components/TopNav.tsx
Moves authenticated access to Cloudflare clients and same-origin sessions. Adds language updates, account deletion, profile email rollback, and format-aware export/download handling.

Editor and export workflow

Layer / File(s) Summary
Annotation and diff contracts
lib/learning/annotations.ts, lib/api/schemas.ts, lib/diff/read-only.ts, tests/*
Adds versioned text-range annotations, known ranges, normalization, segmentation, and diff-side reconstruction with tests.
Per-photo editor workspace
components/EntryWizard.tsx, components/CorrectionAnnotationEditor.tsx, components/EntryDiffComparison.tsx, components/EntriesDashboard.tsx, app/entries/new/page.tsx, app/globals.css
Refactors editing into staged per-photo workflows with final-text editing, annotation controls, diff comparison, hints, memos, photo navigation, and responsive layouts.
PDF and PPTX generation
lib/exports/presentation.ts, lib/pdf/generator.ts, lib/pptx/generator.ts, lib/workflows/export.ts
Adds shared presentation contracts and layout utilities, creates annotated PDF output, and updates PPTX output to use shared annotation rendering.
Export API and downloads
app/api/entries/[id]/export/*/route.ts, app/api/exports/[token]/download/route.ts, lib/exports/download.ts, tests/export-content.test.ts
Adds PDF export and format-specific files, MIME types, filenames, content-disposition headers, and parity tests.

Photo state validation

Layer / File(s) Summary
Workflow state enforcement
supabase/migrations/202608050001_allow_final_fr_edits.sql, cloudflare/migrations/0004_photo_state_trigger.sql, tests/state-machine.test.ts
Validates photo status progression and prevents edits to locked fields while allowing final French edits before export.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant ExportAPI
  participant Workflow
  participant Generator
  participant Storage
  Editor->>ExportAPI: Request PDF or PPTX export
  ExportAPI->>Workflow: Pass format and memo option
  Workflow->>Generator: Generate annotated presentation
  Generator-->>Workflow: Return file buffer
  Workflow->>Storage: Upload format-specific file
  Storage-->>Editor: Return download metadata
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: the Cloudflare migration and bilingual workflow refinement.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfad837f6e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

},
);
setExportUrl(`/api/exports/${result.token}/download`);
const result = await apiFetch<{ token: string }>(`/api/entries/${id}/export/${format}`, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Flush final text before export

When a learner edits the final French text and then exports without pressing save, or clicks export while saveFinalText is still in flight, this request reads the saved DB value because exportFile only waits for memos and dirty annotations. The on-screen comparison is driven by activeFinalDraft, so the generated PPTX/PDF can silently contain the previous final text; disable/export-save while final drafts differ from photo.final_fr or while finalSavingId is set before making this API call.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (10)
lib/exports/presentation.ts (1)

86-92: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

computePhotoGrid supports at most 10 photos.

For n > 10, the returned grid holds only 10 cells. Both generators iterate every photo and compute row = Math.floor(index / cols), so photo 11 lands on a third row that falls outside the grid box and off the slide. The 10-photo limit is enforced elsewhere, so this is an edge case, but a clamp keeps the layout self-consistent.

♻️ Proposed change
 export function computePhotoGrid(n: number): { cols: number; rows: number } {
   if (n <= 3) return { cols: 3, rows: 1 };
   if (n === 4) return { cols: 2, rows: 2 };
   if (n <= 6) return { cols: 3, rows: 2 };
   if (n <= 9) return { cols: 3, rows: 3 };
-  return { cols: 5, rows: 2 };
+  if (n <= 10) return { cols: 5, rows: 2 };
+  const cols = 5;
+  return { cols, rows: Math.ceil(n / cols) };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/exports/presentation.ts` around lines 86 - 92, Update computePhotoGrid so
values of n greater than 10 are clamped to the supported 10-photo layout before
selecting the grid dimensions, preserving the existing layouts for counts from 1
through 10 and ensuring the returned grid always has capacity for every
generated photo.
app/api/exports/[token]/download/route.ts (1)

47-47: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider persisting the export format.

The extension check works today because lib/workflows/export.ts builds object_path as ${hash}.${format}. The contract is implicit: any path that is not .pdf is served as PPTX. A format column on the exports table would make the response MIME type follow the recorded format instead of a filename convention. This is safe to defer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/exports/`[token]/download/route.ts at line 47, Defer changes to the
format-detection logic in the download route; the current ExportFormat
derivation from file.object_path remains acceptable. If implementing this later,
persist the export format on the exports record and use that recorded value
instead of inferring it from the filename extension.
tests/export-content.test.ts (1)

165-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a PDF case with Japanese text.

All PDF inputs here are Latin-only, so the test cannot reach the standard-font encoding limit described in the lib/pdf/generator.ts comment. Add one case where titleFr and draftFr contain Japanese characters. That case fails today and passes once font selection follows the text content.

💚 Proposed additional case
+  it('generates a PDF when the title and draft contain Japanese', async () => {
+    const pdfBuffer = await generatePhotoTextePdf({
+      titleFr: '公園の写真',
+      photos: [{
+        position: 1,
+        draftFr: 'Je visite un parc 公園.',
+        jpAuto: '私は公園を訪れます。',
+        jpIntent: '私は静かに公園を訪れます。',
+        finalFr: 'Je visite calmement un parc 公園.',
+      }],
+    });
+    expect(pdfBuffer.subarray(0, 5).toString()).toBe('%PDF-');
+  }, 20_000);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/export-content.test.ts` around lines 165 - 194, Add a Japanese-text PDF
test alongside the existing generatePhotoTextePdf coverage, using Japanese
characters in titleFr and draftFr while preserving the current PDF/PPTX
page-count, dimensions, and header assertions. Ensure the case exercises font
selection based on text content and fails with the current standard-font path
before the generator fix.
lib/pptx/generator.ts (1)

366-379: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use one text box for the corrected text.

This loop adds one addText shape for every placement, so longer corrected text can become hundreds of separate shapes on one slide. Keep the segment layout for the known-range outlines, but render words as text runs in a single box. pptxgenjs supports per-run highlight for the annotation background color.

Severity/labels:

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pptx/generator.ts` around lines 366 - 379, Update the rendering loop
around measured.placements to create one slide.addText text box containing text
runs rather than one shape per placement. Preserve the existing placement-based
layout for known-range outlines, and assign each run’s position-independent
styling plus its annotation background through pptxgenjs highlight using the
appropriate placement color.
lib/api/schemas.ts (1)

18-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind the version literal to the shared constant.

learningHighlightsSchema hardcodes 2, while lib/learning/annotations.ts exports CORRECTION_ANNOTATION_VERSION. If the version increments, the schema and the normalizer can drift, and valid payloads would be rejected or stale payloads accepted.

♻️ Proposed refactor
+import { CORRECTION_ANNOTATION_VERSION } from "`@/lib/learning/annotations`";
+
 const learningHighlightsSchema = z.object({
-  version: z.literal(2),
+  version: z.literal(CORRECTION_ANNOTATION_VERSION),
   textSignature: z.string().min(1).max(120),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/api/schemas.ts` around lines 18 - 23, Update learningHighlightsSchema to
derive its z.literal version value from the shared CORRECTION_ANNOTATION_VERSION
exported by annotations.ts instead of hardcoding 2, keeping schema validation
aligned with the normalizer.
lib/learning/annotations.ts (1)

36-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the signature prefix with the annotation version.

The signature string starts with v1:, but CORRECTION_ANNOTATION_VERSION is 2. The value is only compared against itself, so behavior is correct today. The mismatch is still misleading for future version changes.

♻️ Proposed refactor
-  return `v1:${text.length}:${(hash >>> 0).toString(36)}`;
+  return `v${CORRECTION_ANNOTATION_VERSION}:${text.length}:${(hash >>> 0).toString(36)}`;

Note: this changes stored signatures, so existing annotations reset on first load. If that is not acceptable, keep the literal and add a comment that the prefix is independent of the annotation version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/learning/annotations.ts` around lines 36 - 43, Update
getAnnotationTextSignature to derive its signature prefix from
CORRECTION_ANNOTATION_VERSION instead of the hardcoded v1 literal, ensuring the
prefix remains aligned with the annotation version and preserving the existing
signature format.
app/globals.css (1)

52-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The base --minimum-font-size value has no effect.

Every consumer wraps the variable in max(11px, var(--minimum-font-size)) (lines 181, 1615, 1633). The root value of 10.5px is therefore always overridden by the 11px floor, and only the French override of 12px changes rendering. Either raise the root value to the intended floor and drop the max() calls, or set the root value to 11px so the declaration matches the behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/globals.css` around lines 52 - 57, Update the base --minimum-font-size
declaration in the global styles to 11px so it matches the effective minimum
enforced by all max(11px, var(--minimum-font-size)) consumers, while preserving
the French 12px override.
components/EntryWizard.tsx (1)

158-164: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider debouncing the diff computation.

activeDiffTokens recomputes computeReadOnlyDiff on every keystroke in the final-text textarea. draft_fr and final_fr accept up to 8000 characters each, and diffWords runs on the main thread. On long texts this can add input lag. Debounce activeFinalDraft before the diff, or compute the diff only against the saved final_fr.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/EntryWizard.tsx` around lines 158 - 164, Reduce input lag in the
activeDiffTokens computation by debouncing activeFinalDraft before passing it to
computeReadOnlyDiff, or by using the saved activePhoto.final_fr value for the
diff instead. Preserve the existing empty-token behavior when no active final
text exists and update the relevant memo dependencies accordingly.
components/CorrectionAnnotationEditor.tsx (1)

125-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Every word becomes a separate tab stop.

Each token renders with role="button" and tabIndex={0}. The final French text can hold up to 8000 characters, so keyboard users must press Tab through hundreds of tokens to reach the toolbar and the save button. Use a roving tabindex: keep tabIndex={0} on one token and tabIndex={-1} on the rest, and move focus with the arrow keys. Alternatively, make the surface one focusable region and select ranges with keyboard commands inside it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/CorrectionAnnotationEditor.tsx` around lines 125 - 145, The token
list in CorrectionAnnotationEditor is creating a tab stop for every word.
Implement roving tabindex so only the active token has tabIndex={0}, all others
use tabIndex={-1}, and arrow-key handlers move focus and update the active token
while preserving Enter/Space range selection. Ensure the toolbar and save button
remain reachable without tabbing through every token.
app/entries/new/page.tsx (1)

564-575: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mark the active photo tab for assistive technology.

The tab buttons show the active state only through the pill-active class. Add aria-current so screen-reader users learn which photo is active.

♻️ Proposed refactor
                       className={index === activeIndex ? "pill pill-active" : "pill"}
+                      aria-current={index === activeIndex ? "true" : undefined}
                       onClick={() => setActiveIndex(index)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/entries/new/page.tsx` around lines 564 - 575, Update the photo tab
buttons rendered in the photos.map block to expose the active state through
aria-current, setting it for the button whose index matches activeIndex and
leaving inactive buttons unset. Preserve the existing className and
setActiveIndex behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/entries/`[id]/export/pdf/route.ts:
- Line 17: Use the shared `export` rate-limit key in both the PDF route’s
`assertRateLimit` call and the PPTX route’s corresponding call, preserving the
existing budget and interval so the limit applies across all export formats.

In `@app/entries/new/page.tsx`:
- Around line 658-664: Update the input rendered by replaceInputRef to remove it
from keyboard and screen-reader navigation while preserving programmatic
activation from the “Remplacer cette photo” button; add the appropriate
non-interactive tab/accessibility handling alongside the existing
visually-hidden class.

In `@app/globals.css`:
- Line 1817: Update the font-family declaration to remove quotes from the valid
family names Manrope and Avenir Next, while preserving the existing fallback
order and sans-serif fallback.
- Around line 70-80: Update the .visually-hidden rule by replacing the
deprecated clip declaration with clip-path: inset(50%). Preserve overflow:
hidden and all other existing accessibility-related declarations unchanged.

In `@components/EntryWizard.tsx`:
- Around line 231-238: Update the useEffect around loadAll to invalidate the
previous load during cleanup when id or router changes. Add an ignore flag (or
request-abort mechanism) and ensure loadAll checks it before every state setter
for photos, drafts, annotations, errors, and related loading state, preventing
stale responses from updating the current entry.
- Around line 373-401: Update saveFinalText so unchanged final text preserves
pending annotation edits: include the current activeAnnotations in the PATCH
when textChanged is false, or avoid replacing local annotations and clearing
annotationDirtyByPhotoId in that branch. Ensure exportFile still recognizes
those edits as dirty until they are successfully persisted.

In `@components/LanguageProvider.tsx`:
- Around line 59-66: Serialize the PATCH requests initiated by setLanguage so
each language persistence write waits for the previous one to settle before
starting, preserving selection order on the server. Keep the immediate
setLanguageState update and unauthenticated early return unchanged, and maintain
the existing error-swallowing behavior for failed requests.

In `@lib/diff/read-only.ts`:
- Around line 27-36: The token reconstruction path in reconstructDiffSides must
preserve original whitespace exactly. Replace the whitespace-insensitive
diffWords usage at lib/diff/read-only.ts:27-36 with diffWordsWithSpace, and add
a whitespace-only reconstruction case at tests/diff-readonly.test.ts:19-29
covering differences such as repeated spaces or newline versus space.

In `@lib/pdf/generator.ts`:
- Around line 230-241: Update embedPhoto and its drawPhoto call sites to cache
each embedded photo per PDFDocument and reuse the cached PDFImage across the
grid, Étape 1, and Étape 4 renders. Perform the base64 decode, sharp PNG
conversion, and pdf.embedPng only once per document/photo, and resize the PNG to
the maximum dimensions needed by the 6-inch panel before embedding.
- Around line 23-30: Resolve the font used by regularFontPath through the module
system or move it to a repository-relative asset instead of constructing a
process.cwd()-based node_modules path. Update next.config.mjs tracing includes
to reference the resulting font location, and adjust package.json only as needed
to remove or retain the font dependency consistently with the chosen approach.
- Around line 214-227: Replace heading-based font selection in the rendered text
path with a shared content-based font selection helper, reusing the existing CJK
detection behavior. Apply this helper consistently to text drawn by
drawTextPanel, drawSlideTitle, drawAnnotatedTextPanel, and the shown body block,
ensuring user text with CJK characters uses fonts.japanese instead of
fonts.regular or fonts.bold.

In `@supabase/migrations/202608050001_allow_final_fr_edits.sql`:
- Around line 37-47: Freeze final_fr during export by updating the trigger logic
in supabase/migrations/202608050001_allow_final_fr_edits.sql lines 37-47 to
reject changes whenever new.status is EXPORTED, including transitions from
FINAL_FR_READY, while preserving the existing immutable source-field checks. Add
a state-machine test in tests/state-machine.test.ts lines 27-34 that changes
final_fr while setting status to EXPORTED and asserts the trigger rejects the
update.

In `@tests/learning-notes.test.ts`:
- Around line 25-27: Update the cleanup in the test’s finally block to restore
OPENAI_API_KEY whenever originalKey is defined, including an empty string; only
delete the environment variable when originalKey is undefined.

---

Nitpick comments:
In `@app/api/exports/`[token]/download/route.ts:
- Line 47: Defer changes to the format-detection logic in the download route;
the current ExportFormat derivation from file.object_path remains acceptable. If
implementing this later, persist the export format on the exports record and use
that recorded value instead of inferring it from the filename extension.

In `@app/entries/new/page.tsx`:
- Around line 564-575: Update the photo tab buttons rendered in the photos.map
block to expose the active state through aria-current, setting it for the button
whose index matches activeIndex and leaving inactive buttons unset. Preserve the
existing className and setActiveIndex behavior.

In `@app/globals.css`:
- Around line 52-57: Update the base --minimum-font-size declaration in the
global styles to 11px so it matches the effective minimum enforced by all
max(11px, var(--minimum-font-size)) consumers, while preserving the French 12px
override.

In `@components/CorrectionAnnotationEditor.tsx`:
- Around line 125-145: The token list in CorrectionAnnotationEditor is creating
a tab stop for every word. Implement roving tabindex so only the active token
has tabIndex={0}, all others use tabIndex={-1}, and arrow-key handlers move
focus and update the active token while preserving Enter/Space range selection.
Ensure the toolbar and save button remain reachable without tabbing through
every token.

In `@components/EntryWizard.tsx`:
- Around line 158-164: Reduce input lag in the activeDiffTokens computation by
debouncing activeFinalDraft before passing it to computeReadOnlyDiff, or by
using the saved activePhoto.final_fr value for the diff instead. Preserve the
existing empty-token behavior when no active final text exists and update the
relevant memo dependencies accordingly.

In `@lib/api/schemas.ts`:
- Around line 18-23: Update learningHighlightsSchema to derive its z.literal
version value from the shared CORRECTION_ANNOTATION_VERSION exported by
annotations.ts instead of hardcoding 2, keeping schema validation aligned with
the normalizer.

In `@lib/exports/presentation.ts`:
- Around line 86-92: Update computePhotoGrid so values of n greater than 10 are
clamped to the supported 10-photo layout before selecting the grid dimensions,
preserving the existing layouts for counts from 1 through 10 and ensuring the
returned grid always has capacity for every generated photo.

In `@lib/learning/annotations.ts`:
- Around line 36-43: Update getAnnotationTextSignature to derive its signature
prefix from CORRECTION_ANNOTATION_VERSION instead of the hardcoded v1 literal,
ensuring the prefix remains aligned with the annotation version and preserving
the existing signature format.

In `@lib/pptx/generator.ts`:
- Around line 366-379: Update the rendering loop around measured.placements to
create one slide.addText text box containing text runs rather than one shape per
placement. Preserve the existing placement-based layout for known-range
outlines, and assign each run’s position-independent styling plus its annotation
background through pptxgenjs highlight using the appropriate placement color.

In `@tests/export-content.test.ts`:
- Around line 165-194: Add a Japanese-text PDF test alongside the existing
generatePhotoTextePdf coverage, using Japanese characters in titleFr and draftFr
while preserving the current PDF/PPTX page-count, dimensions, and header
assertions. Ensure the case exercises font selection based on text content and
fails with the current standard-font path before the generator fix.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef27f2aa-f104-4108-865a-e7f53d7d7734

📥 Commits

Reviewing files that changed from the base of the PR and between f8496cb and dfad837.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (41)
  • app/api/entries/[id]/diff/route.ts
  • app/api/entries/[id]/export/pdf/route.ts
  • app/api/entries/[id]/export/pptx/route.ts
  • app/api/entries/[id]/memos/auto/route.ts
  • app/api/entries/[id]/photos/[photoId]/diff/route.ts
  • app/api/exports/[token]/download/route.ts
  • app/api/me/route.ts
  • app/entries/new/page.tsx
  • app/globals.css
  • app/layout.tsx
  • app/settings/page.tsx
  • components/CorrectionAnnotationEditor.tsx
  • components/DiffReadOnly.tsx
  • components/EntriesDashboard.tsx
  • components/EntryDiffComparison.tsx
  • components/EntryWizard.tsx
  • components/LanguageProvider.tsx
  • components/TopNav.tsx
  • components/UnknownWords.tsx
  • lib/ai/client.ts
  • lib/api/response.ts
  • lib/api/schemas.ts
  • lib/diff/read-only.ts
  • lib/exports/download.ts
  • lib/exports/presentation.ts
  • lib/learning/annotations.ts
  • lib/learning/context.ts
  • lib/learning/highlight.ts
  • lib/pdf/generator.ts
  • lib/pptx/download.ts
  • lib/pptx/generator.ts
  • lib/workflows/export.ts
  • next.config.mjs
  • package.json
  • supabase/migrations/202608050001_allow_final_fr_edits.sql
  • tests/correction-annotations.test.ts
  • tests/diff-readonly.test.ts
  • tests/export-content.test.ts
  • tests/learning-highlight.test.ts
  • tests/learning-notes.test.ts
  • tests/state-machine.test.ts
💤 Files with no reviewable changes (6)
  • components/UnknownWords.tsx
  • components/DiffReadOnly.tsx
  • lib/pptx/download.ts
  • lib/learning/context.ts
  • tests/learning-highlight.test.ts
  • lib/learning/highlight.ts

try {
const { id } = await context.params;
const { user, client } = await authedClient(req);
assertRateLimit(user.id, 'pdf_export', 12, 60_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Share one rate-limit bucket across export formats.

The PPTX route uses the key pptx_export with the same budget. Because the keys differ, one user can start 24 exports per minute across the two routes. Each export renders a full presentation and converts every photo through sharp on the request thread. Use one export key so the budget covers total export cost.

🛡️ Proposed change
-    assertRateLimit(user.id, 'pdf_export', 12, 60_000);
+    assertRateLimit(user.id, 'export', 12, 60_000);

Apply the same key in app/api/entries/[id]/export/pptx/route.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/entries/`[id]/export/pdf/route.ts at line 17, Use the shared `export`
rate-limit key in both the PDF route’s `assertRateLimit` call and the PPTX
route’s corresponding call, preserving the existing budget and interval so the
limit applies across all export formats.

Comment thread app/entries/new/page.tsx
Comment on lines +658 to +664
<input
ref={replaceInputRef}
type="file"
accept="image/*"
onChange={handleReplaceFileChange}
className="visually-hidden"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the hidden replace input from the tab order.

.visually-hidden hides the input visually but keeps it focusable. The input has no label and no accessible name, so keyboard and screen-reader users reach an unnamed file control. The visible "Remplacer cette photo" button already triggers it.

🛡️ Proposed fix
             <input
               ref={replaceInputRef}
               type="file"
               accept="image/*"
               onChange={handleReplaceFileChange}
               className="visually-hidden"
+              tabIndex={-1}
+              aria-hidden="true"
             />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
ref={replaceInputRef}
type="file"
accept="image/*"
onChange={handleReplaceFileChange}
className="visually-hidden"
/>
<input
ref={replaceInputRef}
type="file"
accept="image/*"
onChange={handleReplaceFileChange}
className="visually-hidden"
tabIndex={-1}
aria-hidden="true"
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/entries/new/page.tsx` around lines 658 - 664, Update the input rendered
by replaceInputRef to remove it from keyboard and screen-reader navigation while
preserving programmatic activation from the “Remplacer cette photo” button; add
the appropriate non-interactive tab/accessibility handling alongside the
existing visually-hidden class.

Comment thread app/globals.css
Comment on lines +70 to +80
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the deprecated clip property.

Stylelint reports clip as deprecated. Use clip-path: inset(50%) instead. Keep overflow: hidden so the content stays hidden in browsers that ignore clip-path.

♻️ Proposed fix
     overflow: hidden !important;
-    clip: rect(0, 0, 0, 0) !important;
+    clip-path: inset(50%) !important;
     white-space: nowrap !important;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip-path: inset(50%) !important;
white-space: nowrap !important;
border: 0 !important;
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 77-77: Deprecated property "clip" (property-no-deprecated)

(property-no-deprecated)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/globals.css` around lines 70 - 80, Update the .visually-hidden rule by
replacing the deprecated clip declaration with clip-path: inset(50%). Preserve
overflow: hidden and all other existing accessibility-related declarations
unchanged.

Source: Linters/SAST tools

Comment thread app/globals.css Outdated
border-radius: 14px;
background: #f8fafc;
color: #0f172a;
font-family: "Manrope", "Avenir Next", sans-serif;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the quotes around the font family names.

Stylelint reports font-family-name-quotes for "Manrope". Manrope and Avenir Next are valid unquoted CSS identifiers.

♻️ Proposed fix
-    font-family: "Manrope", "Avenir Next", sans-serif;
+    font-family: Manrope, Avenir Next, sans-serif;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
font-family: "Manrope", "Avenir Next", sans-serif;
font-family: Manrope, Avenir Next, sans-serif;
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 1817-1817: Expected no quotes around "Manrope" (font-family-name-quotes)

(font-family-name-quotes)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/globals.css` at line 1817, Update the font-family declaration to remove
quotes from the valid family names Manrope and Avenir Next, while preserving the
existing fallback order and sans-serif fallback.

Source: Linters/SAST tools

Comment on lines 231 to 238
useEffect(() => {
if (!getAccessToken()) {
router.replace("/login");
return;
}
setMemoDraft("");
setMemoDraftTouched(false);
setMemoPendingSave(false);
setMemoSavedAt(null);
memoAutoRequestedRef.current = null;
loadAll().catch((err) => setError((err as Error).message));
void loadAll().catch((err) => setError((err as Error).message));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, router]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancel the initial load when the effect re-runs.

loadAll calls several state setters after await. The effect depends on id and router. If id changes before the requests resolve, the earlier response still writes photos, drafts, and annotations for the previous entry. Add an ignore flag and check it before each setter, or abort the requests in the cleanup function.

🛡️ Proposed fix
   useEffect(() => {
     if (!getAccessToken()) {
       router.replace("/login");
       return;
     }
-    void loadAll().catch((err) => setError((err as Error).message));
+    let active = true;
+    void loadAll(() => active).catch((err) => {
+      if (active) setError((err as Error).message);
+    });
+    return () => {
+      active = false;
+    };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [id, router]);

loadAll must then check the predicate before it calls each setter.

🧰 Tools
🪛 React Doctor (0.9.3)

[error] 231-231: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.

In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.

(no-set-state-after-await-in-effect)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/EntryWizard.tsx` around lines 231 - 238, Update the useEffect
around loadAll to invalidate the previous load during cleanup when id or router
changes. Add an ignore flag (or request-abort mechanism) and ensure loadAll
checks it before every state setter for photos, drafts, annotations, errors, and
related loading state, preventing stale responses from updating the current
entry.

Source: Linters/SAST tools

Comment thread lib/pdf/generator.ts Outdated
Comment thread lib/pdf/generator.ts
Comment on lines +214 to +227
const fitted = fitPresentationText(content || " ", 16, maxChars, 7);
const bodyFont = heading.includes("japonais") || heading.includes("(JP)")
? fonts.japanese
: fonts.regular;
drawTextBlock(page, fitted.text, {
x: area.x + layout.textPad,
y: area.y + 0.45,
w: area.w - layout.textPad * 2,
h: area.h - 0.55,
}, {
font: bodyFont,
size: fitted.size,
minSize: 5,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

pdf-lib StandardFonts Helvetica WinAnsi cannot encode error non-Latin characters

💡 Result:

The error "WinAnsi cannot encode" occurs in pdf-lib because the library's built-in Standard Fonts (like Helvetica, Times-Roman, Courier) use the WinAnsi encoding, which is restricted to a limited subset of Latin characters [1][2][3]. These fonts do not support Unicode characters beyond this legacy encoding, and the library throws an error when it encounters unsupported characters—such as those from Cyrillic, Asian, or other non-Latin scripts—instead of rendering them as empty boxes [4][2]. To resolve this issue, you must embed a custom font that supports the required Unicode characters [5][4][2]. Recommended Solutions: 1. Embed a Custom Font: Use the embedFont method with a TrueType (.ttf) or OpenType (.otf) font file that includes the necessary character glyphs [5][4]. const fontBytes = await fs.promises.readFile('path/to/your/font.ttf'); const customFont = await pdfDoc.embedFont(fontBytes); page.drawText('Your text here', { font: customFont }); 2. Filter Unsupported Characters: If you must use Standard Fonts and cannot embed a custom font, you can filter the input string to remove or replace unsupported characters [1]. You can check which characters are supported by a font using the getCharacterSet method: const charSet = font.getCharacterSet; // Filter or replace unsupported characters in your string before drawing const sanitizedText = text.split('').map(char => charSet.includes(char)? char: '?').join(''); Note: Simply replacing characters with a fallback (like "?") is a common workaround but will result in the loss of original data [1][6]. Embedding a comprehensive font (e.g., Noto Sans) is the standard approach for full Unicode support [5][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)lib/pdf/generator\.ts$|(^|/)package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$' || true

echo "== generator outline =="
ast-grep outline lib/pdf/generator.ts --view compact || true

echo "== relevant draws and font selection =="
rg -n "drawTextPanel|drawSlideTitle|drawAnnotatedTextPanel|fitPresentationText|fonts\\.(regular|bold|japanese)|japonais|\\(JP\\)|widthOfTextAtSize|wrapParagraph" lib/pdf/generator.ts

echo "== font/loading section =="
sed -n '1,80p' lib/pdf/generator.ts
echo "== relevant page render sections =="
sed -n '150,250p' lib/pdf/generator.ts
sed -n '280,330p' lib/pdf/generator.ts
sed -n '380,420p' lib/pdf/generator.ts
sed -n '470,530p' lib/pdf/generator.ts

Repository: mani1261790/PHOTO-TEXTE

Length of output: 13918


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact drawAnnotatedTextPanel =="
sed -n '338,415p' lib/pdf/generator.ts

echo "== exact drawPhotoSteps section =="
sed -n '300,360p' lib/pdf/generator.ts
sed -n '430,485p' lib/pdf/generator.ts

echo "== package pdf-lib version =="
node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('package.json','utf8'));
console.log({ dependencies: p.dependencies?.['pdf-lib'], devDependencies: p.devDependencies?.['pdf-lib'] });
const lock = JSON.parse(fs.readFileSync('package-lock.json','utf8'));
for (const key of ['dependencies/pdf-lib','node_modules/pdf-lib']) {
  console.log(key, lock?.packages?.[key]?.version ?? lock?.dependencies?.['pdf-lib']?.version);
}
JS

echo "== deterministic content/font selection probe from source =="
python3 - <<'PY'
from pathlib import Path
src = Path('lib/pdf/generator.ts').read_text()
for fn in ['drawTextPanel', 'drawSlideTitle', 'drawAnnotatedTextPanel']:
    i = src.index(f'function {fn}')
    j = src.find('\nfunction ', i+1)
    if j == -1:
        j = src.find('\nasync function ', i+1)
    body = src[i:j if j != -1 else len(src)]
    print(f'-- {fn} --')
    for line in body.splitlines()[:35]:
        if 'fonts.' in line or 'drawTextPanel' in line or 'drawTextBlock' in line or 'drawHeading' in line or 'placement.text' in line:
            print(line.strip())
print('-- CJK helpers/tests in source --')
for s in ['hasCJK', /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test', 'japonais', '(JP)']:
    if isinstance(s,str):
        print(s, "=>", s in src)
    else:
        print(s, "=>", src.count(str(s)))
PY

Repository: mani1261790/PHOTO-TEXTE

Length of output: 7721


Select the font from rendered text, not from the heading.

fonts.regular and fonts.bold map to pdf-lib Standard Fonts, which throw for characters outside WinAnsi, including CJK. User text can reach these fonts through draftFr and finalFr in drawTextPanel, titles in drawSlideTitle, and placement text in drawAnnotatedTextPanel. Use a single content-based font selection helper for all draw calls, as the existing CJK check does elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pdf/generator.ts` around lines 214 - 227, Replace heading-based font
selection in the rendered text path with a shared content-based font selection
helper, reusing the existing CJK detection behavior. Apply this helper
consistently to text drawn by drawTextPanel, drawSlideTitle,
drawAnnotatedTextPanel, and the shown body block, ensuring user text with CJK
characters uses fonts.japanese instead of fonts.regular or fonts.bold.

Comment thread lib/pdf/generator.ts
Comment on lines +230 to +241
async function embedPhoto(pdf: PDFDocument, data?: string): Promise<PDFImage | null> {
if (!data) return null;
const match = data.match(/^data:[^;]+;base64,(.+)$/s);
if (!match) return null;
try {
const source = Buffer.from(match[1], "base64");
const png = await sharp(source).png().toBuffer();
return await pdf.embedPng(png);
} catch {
return null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Cache embedded photos per document.

embedPhoto runs on every drawPhoto call. Each photo is drawn three times: on the grid page (line 323), on the Étape 1 slide (line 443), and on the Étape 4 slide (line 461). Each call re-decodes the base64 data, re-encodes it through sharp, and embeds a separate image object. For 10 photos this triples the sharp work and stores 30 image objects instead of 10, which inflates the PDF size. The PNG re-encode also has no resize, so a full-resolution photo is stored far larger than the 6-inch panel needs.

⚡ Proposed change
-async function embedPhoto(pdf: PDFDocument, data?: string): Promise<PDFImage | null> {
+const imageCache = new WeakMap<PDFDocument, Map<string, PDFImage | null>>();
+
+async function embedPhoto(pdf: PDFDocument, data?: string): Promise<PDFImage | null> {
   if (!data) return null;
+  let cache = imageCache.get(pdf);
+  if (!cache) {
+    cache = new Map();
+    imageCache.set(pdf, cache);
+  }
+  if (cache.has(data)) return cache.get(data)!;
   const match = data.match(/^data:[^;]+;base64,(.+)$/s);
   if (!match) return null;
   try {
     const source = Buffer.from(match[1], "base64");
-    const png = await sharp(source).png().toBuffer();
-    return await pdf.embedPng(png);
+    const png = await sharp(source)
+      .resize({ width: 1600, height: 1600, fit: "inside", withoutEnlargement: true })
+      .png()
+      .toBuffer();
+    const image = await pdf.embedPng(png);
+    cache.set(data, image);
+    return image;
   } catch {
+    cache.set(data, null);
     return null;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function embedPhoto(pdf: PDFDocument, data?: string): Promise<PDFImage | null> {
if (!data) return null;
const match = data.match(/^data:[^;]+;base64,(.+)$/s);
if (!match) return null;
try {
const source = Buffer.from(match[1], "base64");
const png = await sharp(source).png().toBuffer();
return await pdf.embedPng(png);
} catch {
return null;
}
}
const imageCache = new WeakMap<PDFDocument, Map<string, PDFImage | null>>();
async function embedPhoto(pdf: PDFDocument, data?: string): Promise<PDFImage | null> {
if (!data) return null;
let cache = imageCache.get(pdf);
if (!cache) {
cache = new Map();
imageCache.set(pdf, cache);
}
if (cache.has(data)) return cache.get(data)!;
const match = data.match(/^data:[^;]+;base64,(.+)$/s);
if (!match) return null;
try {
const source = Buffer.from(match[1], "base64");
const png = await sharp(source)
.resize({ width: 1600, height: 1600, fit: "inside", withoutEnlargement: true })
.png()
.toBuffer();
const image = await pdf.embedPng(png);
cache.set(data, image);
return image;
} catch {
cache.set(data, null);
return null;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/pdf/generator.ts` around lines 230 - 241, Update embedPhoto and its
drawPhoto call sites to cache each embedded photo per PDFDocument and reuse the
cached PDFImage across the grid, Étape 1, and Étape 4 renders. Perform the
base64 decode, sharp PNG conversion, and pdf.embedPng only once per
document/photo, and resize the PNG to the maximum dimensions needed by the
6-inch panel before embedding.

Comment on lines +37 to +47
if old.status = 'FINAL_FR_READY' then
if new.entry_id is distinct from old.entry_id
or new.user_id is distinct from old.user_id
or new.position is distinct from old.position
or new.photo_asset_id is distinct from old.photo_asset_id
or new.draft_fr is distinct from old.draft_fr
or new.jp_auto is distinct from old.jp_auto
or new.jp_intent is distinct from old.jp_intent then
raise exception 'source fields are immutable after FINAL_FR_READY';
end if;
end if;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Freeze final_fr during the export transition.

A single update can change final_fr and set status to EXPORTED. The old.status = 'EXPORTED' check runs only on later updates. This violates the requirement that final French text is editable only before export.

  • supabase/migrations/202608050001_allow_final_fr_edits.sql#L37-L47: Reject a final_fr change whenever new.status = 'EXPORTED', including a transition from FINAL_FR_READY.
  • tests/state-machine.test.ts#L27-L34: Add a case that changes final_fr while setting status to EXPORTED and expects the trigger to reject the update.
📍 Affects 2 files
  • supabase/migrations/202608050001_allow_final_fr_edits.sql#L37-L47 (this comment)
  • tests/state-machine.test.ts#L27-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/202608050001_allow_final_fr_edits.sql` around lines 37 -
47, Freeze final_fr during export by updating the trigger logic in
supabase/migrations/202608050001_allow_final_fr_edits.sql lines 37-47 to reject
changes whenever new.status is EXPORTED, including transitions from
FINAL_FR_READY, while preserving the existing immutable source-field checks. Add
a state-machine test in tests/state-machine.test.ts lines 27-34 that changes
final_fr while setting status to EXPORTED and asserts the trigger rejects the
update.

Comment on lines +25 to +27
} finally {
if (originalKey) process.env.OPENAI_API_KEY = originalKey;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore OPENAI_API_KEY exactly.

If the original value is '', the cleanup leaves the variable deleted. Restore the original value unless it was undefined.

Proposed fix
     } finally {
-      if (originalKey) process.env.OPENAI_API_KEY = originalKey;
+      if (originalKey === undefined) {
+        delete process.env.OPENAI_API_KEY;
+      } else {
+        process.env.OPENAI_API_KEY = originalKey;
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} finally {
if (originalKey) process.env.OPENAI_API_KEY = originalKey;
}
} finally {
if (originalKey === undefined) {
delete process.env.OPENAI_API_KEY;
} else {
process.env.OPENAI_API_KEY = originalKey;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/learning-notes.test.ts` around lines 25 - 27, Update the cleanup in the
test’s finally block to restore OPENAI_API_KEY whenever originalKey is defined,
including an empty string; only delete the environment variable when originalKey
is undefined.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
lib/cloudflare/client.ts (1)

401-404: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Object.hasOwn for the table allowlist check.

table in TABLES also returns true for inherited Object.prototype keys such as constructor and toString. TABLES['constructor'] then resolves to Object, and the builder proceeds with an undefined scope. The path fails later with a TypeError instead of the intended Unknown D1 table error. All current callers pass literal table names, so this is not reachable today.

♻️ Proposed fix
   from(table: string): QueryBuilderLike<any[]> {
-    if (!(table in TABLES)) throw new Error(`Unknown D1 table: ${table}`);
+    if (!Object.hasOwn(TABLES, table)) throw new Error(`Unknown D1 table: ${table}`);
     return new QueryBuilder(this.env.DB, table as TableName, this.userId) as unknown as QueryBuilderLike<any[]>;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/cloudflare/client.ts` around lines 401 - 404, Update the allowlist check
in QueryBuilderLike.from to use an own-property check via Object.hasOwn instead
of the in operator, so inherited keys such as constructor and toString are
rejected with the existing Unknown D1 table error.
app/api/storage/[bucket]/[...path]/route.ts (1)

21-29: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap the handler body in error handling.

getAppEnv() at line 21 and CONTENT_BUCKET.get at line 26 can reject. This handler has no try/catch, unlike the other routes in this migration, which route failures through handleApiError. An unhandled rejection returns a framework 500 that does not match the { error: { code } } shape used at lines 18, 23, and 28.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/storage/`[bucket]/[...path]/route.ts around lines 21 - 29, The
storage route handler should wrap the environment lookup, signature
verification, and bucket fetch in try/catch handling, routing failures through
the existing handleApiError utility so rejected operations return the standard
error shape. Preserve the current INVALID_SIGNATURE and OBJECT_NOT_FOUND
responses for expected conditions.
app/api/me/route.ts (2)

106-112: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚖️ Poor tradeoff

The email rollback is unchecked and the two writes are not atomic.

The email lives in the D1 user table and the encrypted copy lives in user_profiles. These are two separate writes with no transaction. Lines 108-111 attempt a compensating write but discard its result. If the rollback fails, the user table holds the new email while user_profiles holds the encrypted old email.

Consider writing both through a single env.DB.batch call so the pair commits or fails together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/me/route.ts` around lines 106 - 112, Update the email-change flow
around the `nextEmail` branch to replace the separate profile and `user` writes
with one `env.DB.batch` operation containing both updates, preserving their
existing parameters and ordering so the plaintext and encrypted email changes
commit or fail together. Remove the compensating rollback write and its
unchecked result.

78-92: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Normalize the new email lookup and write.

user.email already has a unique SQLite constraint, so this cannot create duplicate rows. The read then write is still TOCTOU, but the UNIQUE constraint enforces the final invariant; the remaining issue is that case-insensitive lookup can write mixed-case emails and diverge from migrated lowercase values. Use lowercase nextEmail for both the SELECT check and the UPDATE bind, and let the constraint handle the uniqueness result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/me/route.ts` around lines 78 - 92, Normalize nextEmail to lowercase
before the duplicate check and update in the profile update flow. Use the
normalized value for the SELECT bind and the UPDATE bind, while retaining the
existing uniqueness handling and success checks.
components/TopNav.tsx (1)

42-47: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard repeated logout submissions.

handleLogout remains active while fetch waits. Two clicks can send two POST /api/auth/sign-out requests. Add a loggingOut guard and disable the button while the request is pending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/TopNav.tsx` around lines 42 - 47, Update handleLogout in TopNav to
guard against concurrent submissions by tracking a loggingOut state, returning
early when logout is already pending, and setting the guard before the sign-out
fetch and clearing it after completion. Bind the logout button’s disabled state
to loggingOut so it remains disabled while the request is pending.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 1-15: Reorder the environment variables in the dotenv template
according to dotenv-linter’s expected key order, including the newly added
Supabase-related keys, while preserving every key and its current value or empty
placeholder.

In `@app/api/me/route.ts`:
- Around line 153-161: Check the results of all six delete operations in the
account-deletion flow before removing the user row. Use each returned result’s
error field, fail the request when any deletion reports an error, and only
execute the user deletion and success response after every table deletion
succeeds.

In `@components/TopNav.tsx`:
- Around line 42-47: Update handleLogout so local state clearing and navigation
to /login occur only after the /api/auth/sign-out request succeeds with a
successful response. Remove the catch that suppresses failures, check the fetch
response status, and preserve the current logout transition for successful
sign-outs while leaving it incomplete when the request fails or returns non-2xx.

In `@lib/workflows/export.ts`:
- Around line 49-55: Update loadJapanesePdfFont to read the R2ObjectBody using
its supported arrayBuffer() method instead of bytes(), while preserving the
existing missing-font validation and return type behavior.

In `@package.json`:
- Around line 17-26: Update the package.json scripts.lint entry to invoke the
repository’s chosen linter directly instead of next lint, and add the
corresponding linter dependency and configuration required for it to run. Keep
the existing lint script interface intact while ensuring it targets the project
source files.

In `@README.md`:
- Line 145: Update the README migration instructions by replacing `9利用者全員` with
the appropriate Japanese phrasing: use `9人の利用者全員` for a fixed count, or `全利用者`
when the procedure applies to all users.

In `@scripts/migrate-supabase-to-cloudflare.mjs`:
- Around line 54-80: Update fetchTable to request a stable ascending id order on
every table query before applying the range, and stop pagination when a batch is
empty rather than only when it is short. Also add pre-migration count
comparisons between the source Supabase users/table data and the destination
database, failing or reporting a mismatch before inserts proceed; do not rely on
INSERT OR IGNORE to validate completeness.
- Line 22: Remove the literal fallback from the cloudflareAccountId
initialization and require CLOUDFLARE_ACCOUNT_ID to be present, matching the
validation behavior used for NEXT_PUBLIC_SUPABASE_URL and
SUPABASE_SERVICE_ROLE_KEY. Ensure the migration exits or throws before launching
the wrangler child process when the variable is unset.

---

Nitpick comments:
In `@app/api/me/route.ts`:
- Around line 106-112: Update the email-change flow around the `nextEmail`
branch to replace the separate profile and `user` writes with one `env.DB.batch`
operation containing both updates, preserving their existing parameters and
ordering so the plaintext and encrypted email changes commit or fail together.
Remove the compensating rollback write and its unchecked result.
- Around line 78-92: Normalize nextEmail to lowercase before the duplicate check
and update in the profile update flow. Use the normalized value for the SELECT
bind and the UPDATE bind, while retaining the existing uniqueness handling and
success checks.

In `@app/api/storage/`[bucket]/[...path]/route.ts:
- Around line 21-29: The storage route handler should wrap the environment
lookup, signature verification, and bucket fetch in try/catch handling, routing
failures through the existing handleApiError utility so rejected operations
return the standard error shape. Preserve the current INVALID_SIGNATURE and
OBJECT_NOT_FOUND responses for expected conditions.

In `@components/TopNav.tsx`:
- Around line 42-47: Update handleLogout in TopNav to guard against concurrent
submissions by tracking a loggingOut state, returning early when logout is
already pending, and setting the guard before the sign-out fetch and clearing it
after completion. Bind the logout button’s disabled state to loggingOut so it
remains disabled while the request is pending.

In `@lib/cloudflare/client.ts`:
- Around line 401-404: Update the allowlist check in QueryBuilderLike.from to
use an own-property check via Object.hasOwn instead of the in operator, so
inherited keys such as constructor and toString are rejected with the existing
Unknown D1 table error.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bc08b86-e53c-4217-8a2c-bd6aab254050

📥 Commits

Reviewing files that changed from the base of the PR and between dfad837 and 37195c7.

⛔ Files ignored due to path filters (4)
  • app/apple-icon.png is excluded by !**/*.png
  • app/icon.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • public/icon-192.png is excluded by !**/*.png
📒 Files selected for processing (65)
  • .env.example
  • .gitignore
  • README.md
  • app/api/assets/photo/route.ts
  • app/api/auth/[...all]/route.ts
  • app/api/auth/login/route.ts
  • app/api/auth/signup/route.ts
  • app/api/entries/[id]/diff/route.ts
  • app/api/entries/[id]/export/pdf/route.ts
  • app/api/entries/[id]/export/pptx/route.ts
  • app/api/entries/[id]/lock_intent/route.ts
  • app/api/entries/[id]/memos/auto/route.ts
  • app/api/entries/[id]/memos/route.ts
  • app/api/entries/[id]/photos/[photoId]/diff/route.ts
  • app/api/entries/[id]/photos/[photoId]/lock_intent/route.ts
  • app/api/entries/[id]/photos/[photoId]/route.ts
  • app/api/entries/[id]/photos/[photoId]/translate/route.ts
  • app/api/entries/[id]/photos/route.ts
  • app/api/entries/[id]/rewrite/route.ts
  • app/api/entries/[id]/route.ts
  • app/api/entries/[id]/translate/route.ts
  • app/api/entries/multi/route.ts
  • app/api/entries/route.ts
  • app/api/exports/[token]/download/route.ts
  • app/api/internal/supabase-keepalive/route.ts
  • app/api/me/route.ts
  • app/api/memos/[id]/route.ts
  • app/api/storage/[bucket]/[...path]/route.ts
  • app/apple-icon.tsx
  • app/entries/new/page.tsx
  • app/icon-192.tsx
  • app/icon.tsx
  • app/login/page.tsx
  • app/manifest.ts
  • cloudflare/migrations/0001_initial.sql
  • cloudflare/migrations/0002_updated_at_triggers.sql
  • cloudflare/migrations/0003_photo_limit_trigger.sql
  • cloudflare/migrations/0004_photo_state_trigger.sql
  • components/TopNav.tsx
  • lib/api/fetcher.ts
  • lib/auth/better-auth.ts
  • lib/auth/legacy-supabase.ts
  • lib/auth/session.ts
  • lib/cloudflare/authed.ts
  • lib/cloudflare/client.ts
  • lib/cloudflare/context.ts
  • lib/cloudflare/storage-signature.ts
  • lib/env.ts
  • lib/image/sanitize.ts
  • lib/pdf/generator.ts
  • lib/supabase/client.ts
  • lib/workflows/export.ts
  • lib/workflows/rewrite.ts
  • next-env.d.ts
  • next.config.mjs
  • open-next.config.ts
  • package.json
  • scripts/migrate-supabase-to-cloudflare.mjs
  • tests/export-content.test.ts
  • tests/learning-notes.test.ts
  • tests/legacy-auth-migration.test.ts
  • tests/rls-policy.test.ts
  • tsconfig.json
  • vercel.json
  • wrangler.jsonc
💤 Files with no reviewable changes (7)
  • vercel.json
  • lib/env.ts
  • lib/supabase/client.ts
  • app/api/internal/supabase-keepalive/route.ts
  • app/icon.tsx
  • app/icon-192.tsx
  • app/apple-icon.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/learning-notes.test.ts
  • app/api/entries/[id]/export/pdf/route.ts
  • app/api/entries/[id]/memos/auto/route.ts
  • app/entries/new/page.tsx
  • app/api/entries/[id]/diff/route.ts
  • app/api/exports/[token]/download/route.ts
  • app/api/entries/[id]/export/pptx/route.ts
  • tests/export-content.test.ts
  • app/api/entries/[id]/photos/[photoId]/diff/route.ts

Comment thread .env.example
Comment on lines 1 to +15
APP_MASTER_KEY_B64=
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o-mini
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
STORAGE_SIGNING_SECRET=
PHOTO_BUCKET=photos
EXPORT_BUCKET=exports

# Supabase migration and temporary first-login bridge only.
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
LEGACY_SUPABASE_URL=
LEGACY_SUPABASE_ANON_KEY=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the dotenv-linter key order.

The supplied dotenv-linter report flags the new keys as unordered. Sort the keys to keep the environment-template lint check clean.

🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 4-4: [UnorderedKey] The BETTER_AUTH_SECRET key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 5-5: [UnorderedKey] The BETTER_AUTH_URL key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 7-7: [UnorderedKey] The PHOTO_BUCKET key should go before the STORAGE_SIGNING_SECRET key

(UnorderedKey)


[warning] 8-8: [UnorderedKey] The EXPORT_BUCKET key should go before the OPENAI_API_KEY key

(UnorderedKey)


[warning] 12-12: [UnorderedKey] The NEXT_PUBLIC_SUPABASE_ANON_KEY key should go before the NEXT_PUBLIC_SUPABASE_URL key

(UnorderedKey)


[warning] 14-14: [UnorderedKey] The LEGACY_SUPABASE_URL key should go before the NEXT_PUBLIC_SUPABASE_ANON_KEY key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The LEGACY_SUPABASE_ANON_KEY key should go before the LEGACY_SUPABASE_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 1 - 15, Reorder the environment variables in the
dotenv template according to dotenv-linter’s expected key order, including the
newly added Supabase-related keys, while preserving every key and its current
value or empty placeholder.

Source: Linters/SAST tools

Comment thread app/api/me/route.ts
Comment on lines +153 to +161
await client.from('exports').delete();
await client.from('memos').delete();
await client.from('entry_photos').delete();
await client.from('entries').delete();
await client.from('assets').delete();
await client.from('user_profiles').delete();

const env = await getAppEnv();
await env.DB.prepare('DELETE FROM "user" WHERE id = ?').bind(user.id).run();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check the delete results before deleting the user row.

Each .delete() call returns { data, error } instead of throwing. QueryBuilder.execute in lib/cloudflare/client.ts lines 274-283 converts every failure into an error object. This code discards all six results, then deletes the user row and returns { deleted: true }.

If any table delete fails, the response still reports success. Check each result and fail the request when a delete returns an error.

🐛 Proposed fix
-    await client.from('exports').delete();
-    await client.from('memos').delete();
-    await client.from('entry_photos').delete();
-    await client.from('entries').delete();
-    await client.from('assets').delete();
-    await client.from('user_profiles').delete();
+    for (const table of ['exports', 'memos', 'entry_photos', 'entries', 'assets', 'user_profiles']) {
+      const { error } = await client.from(table).delete();
+      if (error) {
+        badRequest('ACCOUNT_DELETE_FAILED', 'Unable to delete account data');
+      }
+    }
 
     const env = await getAppEnv();
-    await env.DB.prepare('DELETE FROM "user" WHERE id = ?').bind(user.id).run();
+    const removal = await env.DB.prepare('DELETE FROM "user" WHERE id = ?').bind(user.id).run();
+    if (!removal.success) {
+      badRequest('ACCOUNT_DELETE_FAILED', 'Unable to delete account');
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/me/route.ts` around lines 153 - 161, Check the results of all six
delete operations in the account-deletion flow before removing the user row. Use
each returned result’s error field, fail the request when any deletion reports
an error, and only execute the user deletion and success response after every
table deletion succeeds.

Comment thread components/TopNav.tsx
Comment on lines +42 to +47
const handleLogout = async () => {
await fetch('/api/auth/sign-out', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'same-origin'
}).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)TopNav\.tsx$|^components/|src/lib/auth|lib/auth|app/api/auth|middleware|better-auth|package\.json' || true

echo
echo "== TopNav outline/section =="
if [ -f components/TopNav.tsx ]; then
  wc -l components/TopNav.tsx
  cat -n components/TopNav.tsx | sed -n '1,140p'
fi

echo
echo "== auth token store =="
if [ -f lib/auth/token-store.ts ]; then
  cat -n lib/auth/token-store.ts
fi

echo
echo "== usages of handleLogout/clearAccessToken/sign-out/auth session =="
rg -n "handleLogout|clearAccessToken|sign-out|useAuth|session|HttpOnly|better-auth|accessToken" . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 12648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== token fetcher =="
cat -n lib/api/fetcher.ts

echo
echo "== Better Auth route and config =="
cat -n app/api/auth/[...all]/route.ts
cat -n lib/auth/better-auth.ts

echo
echo "== lock metadata around better-auth package =="
node - <<'JS'
const fs = require('fs');
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
const pk = lock.packages?.['node_modules/better-auth'];
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log(JSON.stringify({
  packageManager: pkg.packageManager || null,
  dependency: pkg.dependencies?.['better-auth'] || null,
  locked: pk && { version: pk.version, resolved: pk.resolved },
}, null, 2));
JS

Repository: mani1261790/PHOTO-TEXTE

Length of output: 4312


🌐 Web query:

Better Auth version 1.6.26 signOut session cookie HttpOnly session invalidation route /sign-out

💡 Result:

In Better Auth version 1.6.26, the signOut process is handled via the /sign-out endpoint (typically POST /api/auth/sign-out) [1][2][3]. When called, this endpoint invalidates the user's session in the database and clears the associated session cookies [1]. Key technical details regarding the signOut process: 1. Cookie Invalidation: Better Auth explicitly clears session-related cookies upon sign-out [1][4]. This includes the primary session token cookie (often named better-auth.session_token) and, if cookie caching is enabled, the session data cache cookie (better-auth.session_data) [5][6]. The library uses a helper function (deleteSessionCookie) to ensure these are expired in the browser [1][4][6]. 2. HttpOnly Cookies: Better Auth utilizes HttpOnly and secure attributes for its session cookies by default in production environments, which protects them from being accessed via client-side JavaScript [5]. Because the cookies are HttpOnly, the browser automatically handles the storage and transmission, and the /sign-out endpoint clears them by sending Set-Cookie headers with an expiration date in the past [1]. 3. Implementation: - Client-side: You can initiate the sign-out process using the client SDK: await authClient.signOut; [7][2] - Server-side/API: The underlying endpoint is a POST request to /sign-out, which requires the session cookies to be present in the request headers [1][2][3]. If you experience issues where a session appears to persist after sign-out, ensure that your application or framework is correctly forwarding the Set-Cookie headers returned by the /sign-out response to the browser [4]. Frameworks like Next.js or custom reverse proxies can sometimes interfere with cookie propagation if not configured to pass these headers through [4][5]. Recent versions, including 1.6.26, have included fixes for session cleanup and ensuring that both the session_token and session_data cookies are consistently cleared [8][4][9]. [6] confirms through automated integration testing that the sign-out process successfully clears both cookies in environments where cookie caching is active.

Citations:


Broken Authentication (CWE-613): Insufficient Session Expiration

Reachability: External

Do not report logout as complete when the server call fails.

fetch ignores network failures and does not enforce the non-2xx sign-out response, so handleLogout still clears local state and navigates to /login while a failed Better Auth session may still be valid. Complete the UI transition only after /api/auth/sign-out returns success.

🧰 Tools
🪛 React Doctor (0.9.3)

[error] 43-43: This async handler awaits a mutating request and only flips state after the await, so a fast double-click or double Enter fires the request twice. Add a leading if (busy) return guard (or set a flag before the await and disable the control) to close the re-entry window.

An async onClick/onSubmit handler on a host control that awaits a mutating request and sets state only afterward stays interactive across the await, so a double-click fires the write twice. Add a leading if (busy) return guard, or set a flag before the await inside try and reset it in finally while the control is disabled.

(no-async-event-handler-without-reentry-guard)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/TopNav.tsx` around lines 42 - 47, Update handleLogout so local
state clearing and navigation to /login occur only after the /api/auth/sign-out
request succeeds with a successful response. Remove the catch that suppresses
failures, check the fetch response status, and preserve the current logout
transition for successful sign-outs while leaving it incomplete when the request
fails or returns non-2xx.

Comment thread lib/workflows/export.ts
Comment on lines +49 to +55
async function loadJapanesePdfFont(): Promise<Uint8Array> {
const env = await getAppEnv();
const object = await env.CONTENT_BUCKET.get("system/fonts/NotoSansJP-Regular.ttf");
if (!object) {
badRequest("PDF_FONT_MISSING", "PDF Japanese font is not installed");
}
return object.bytes();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'CONTENT_BUCKET\.get|object\.bytes|arrayBuffer\(' lib/workflows/export.ts

# Confirm the current Cloudflare R2 body API in the official reference.
curl -fsSL 'https://developers.cloudflare.com/r2/api/workers/workers-api-reference/' |
  rg -n -C 2 'R2ObjectBody|arrayBuffer|bytes' || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate R2 API type declarations or dependencies without running repository code.
git ls-files | rg -n '(^|/)(wrangler\.json.*|package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|\.wrangler/types|.*d\.ts|`@cloudflare/workers-types`|workerd|cloudflare|types)' || true

# If local Cloudflare Worker type declarations are present, inspect R2ObjectBody method signatures.
python3 - <<'PY'
import subprocess, re
result = subprocess.run(["git","ls-files"], text=True, stdout=subprocess.PIPE)
files = result.stdout.splitlines()
matches = []
for f in files:
    try:
        data = open(f, "rt", encoding="utf-8", errors="ignore").read()
    except Exception:
        continue
    if "R2ObjectBody" in data and ("arrayBuffer" in data or ".bytes" in data):
        matches.append(f)
print("candidate type files:")
for f in matches[:50]:
    print(f)
for f in matches:
    try:
        lines = open(f, "rt", encoding="utf-8", errors="ignore").readlines()
    except Exception:
        continue
    for i, line in enumerate(lines, 1):
        if "R2ObjectBody" in line or ".arrayBuffer" in line or ".bytes" in line:
            print(f"\n--- {f}:{i} ---")
            for j in range(max(1, i-4), min(len(lines), i+6)):
                print(f"{j}: {lines[j-1].rstrip()}")
PY

Repository: mani1261790/PHOTO-TEXTE

Length of output: 692


Read the R2 object with arrayBuffer().

R2ObjectBody defines arrayBuffer(), not bytes(), so this call fails the Cloudflare Workers R2 API type contract.

Proposed fix
-  return object.bytes();
+  return new Uint8Array(await object.arrayBuffer());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function loadJapanesePdfFont(): Promise<Uint8Array> {
const env = await getAppEnv();
const object = await env.CONTENT_BUCKET.get("system/fonts/NotoSansJP-Regular.ttf");
if (!object) {
badRequest("PDF_FONT_MISSING", "PDF Japanese font is not installed");
}
return object.bytes();
async function loadJapanesePdfFont(): Promise<Uint8Array> {
const env = await getAppEnv();
const object = await env.CONTENT_BUCKET.get("system/fonts/NotoSansJP-Regular.ttf");
if (!object) {
badRequest("PDF_FONT_MISSING", "PDF Japanese font is not installed");
}
return new Uint8Array(await object.arrayBuffer());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/workflows/export.ts` around lines 49 - 55, Update loadJapanesePdfFont to
read the R2ObjectBody using its supported arrayBuffer() method instead of
bytes(), while preserving the existing missing-font validation and return type
behavior.

Comment thread package.json
Comment on lines 17 to +26
"lint": "next lint",
"test": "vitest run"
},
"dependencies": {
"@supabase/supabase-js": "^2.49.1",
"diff": "^7.0.0",
"@opennextjs/cloudflare": "^1.20.2",
"@pdf-lib/fontkit": "^1.1.1",
"better-auth": "^1.6.26",
"diff": "8.0.3",
"jszip": "^3.10.1",
"next": "^16.1.6",
"next": "16.3.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the official Next.js 16.3.0 documentation, confirm that next lint is removed and identify the supported replacement command.

💡 Result:

The next lint command has been removed in Next.js 16 [1][2]. There is no direct "replacement" command provided by Next.js; instead, you should use the official CLI for your chosen linter directly [1][2]. For most projects, this means invoking the ESLint CLI (e.g., eslint .) or the Biome CLI directly [1][2]. Additionally, automatic linting during the next build process has been removed, providing developers with full control over when and how linting is executed [3][4][2]. To migrate existing projects, Next.js provides a codemod: npx @next/codemod@canary next-lint-to-eslint-cli [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json scripts and lint deps =="
python3 - <<'PY'
import json
from pathlib import Path
p=Path('package.json')
data=json.loads(p.read_text())
for section in ['scripts','devDependencies','dependencies']:
    vals=data.get(section,{})
    if section=='scripts':
        for k,v in vals.items():
            print(f"{section}.{k}={v}")
    else:
        print(f"## {section}")
        for k in sorted(vals):
            print(f"{k}={vals[k]}")
PY

echo
echo "== lint config files =="
git ls-files | grep -E '(^|/)(eslint\.|\.eslintrc(\..*)?|biome|biome\.)|package-lock\.json|pnpm-lock\.yaml|yarn\.lock$' | sort

echo
echo "== references to next lint / lint command / lint config =="
rg -n '"next lint"|next lint|eslint|biome|`@next/codemod`' package.json .eslintrc* biome.json biome.* package-lock.json 2>/dev/null || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 1420


Replace the removed next lint command.

scripts.lint still calls next lint, but Next.js 16.3.0 no longer provides that command. Update the script to run ESLint/Biome directly and add the matching config, since the repo currently has no ESLint/Biome dependencies or config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 17 - 26, Update the package.json scripts.lint
entry to invoke the repository’s chosen linter directly instead of next lint,
and add the corresponding linter dependency and configuration required for it to
run. Keep the existing lint script interface intact while ensuring it targets
the project source files.

Comment thread README.md
3. 成功時にBetter Auth形式でパスワードをハッシュし、D1へ保存
4. 以後はCloudflareだけでログイン

移行期間中はCloudflareへ `LEGACY_SUPABASE_URL` と `LEGACY_SUPABASE_ANON_KEY` を登録します。9利用者全員にCredentialが作成されたことを確認した後、この2値とSupabaseプロジェクトを削除できます。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the Japanese wording.

Replace 9利用者全員 with 9人の利用者全員 if the count is fixed, or 全利用者 if the procedure applies to all users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 145, Update the README migration instructions by replacing
`9利用者全員` with the appropriate Japanese phrasing: use `9人の利用者全員` for a fixed
count, or `全利用者` when the procedure applies to all users.

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '');
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const r2Bucket = process.env.CLOUDFLARE_R2_BUCKET ?? 'photo-texte-content';
const cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? '2ea670c2a6ff28e248ef084adf095e8b';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the account id appears elsewhere and whether it is documented.
rg -n "2ea670c2a6ff28e248ef084adf095e8b|CLOUDFLARE_ACCOUNT_ID" -g '!node_modules'

Repository: mani1261790/PHOTO-TEXTE

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -u

echo "== repo files matching migrate script =="
git ls-files | rg 'migrate-supabase-to-cloudflare\.mjs|scripts/migrate|CONTL\.*(md|txt)|\.env|example' || true

echo
echo "== inspect candidate script if present =="
if [ -f scripts/migrate-supabase-to-cloudflare.mjs ]; then
  nl -ba scripts/migrate-supabase-to-cloudflare.mjs | sed -n '1,180p'
fi

echo
echo "== search for hardcoded cloudflare account-ish strings =="
rg -n "2ea670c2a6ff28e248ef084adf095e8b|account.*(id|identifier)|cloudflare|CLOUDFLARE|SUPABASE_SERVICE_ROLE|NEXT_PUBLIC_SUPABASE" -g '!node_modules' . || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 10345


🏁 Script executed:

#!/bin/bash
set -u

echo "== git status and diff stat =="
git status --short
git diff --stat || true

echo
echo "== show exact diff around environment handling =="
git diff -- scripts/migrate-supabase-to-cloudflare.mjs | sed -n '1,220p' || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -u

echo "== candidate script with line numbers =="
awk '{printf "%6d\t%s\n", NR, $0}' scripts/migrate-supabase-to-cloudflare.mjs | sed -n '1,180p'

echo
echo "== .env.example =="
awk '{printf "%6d\t%s\n", NR, $0}' .env.example | sed -n '1,180p'

echo
echo "== wrangler account_id =="
awk '{printf "%6d\t%s\n", NR, $0}' wrangler.jsonc | sed -n '1,40p'

echo
echo "== env documentation around env variables =="
awk '{printf "%6d\t%s\n", NR, $0}' README.md | sed -n '45,60p;105,140p'

Repository: mani1261790/PHOTO-TEXTE

Length of output: 11021


Security Misconfiguration (CWE-1188): Insecure Default Initialization of Resource

Reachability: Internal

Remove the hardcoded Cloudflare account id default.

CLOUDFLARE_ACCOUNT_ID falls back to a literal account id when unset, then passes that value into the wrangler child process. Require the variable, like NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY, so an omitted migration environment variable fails before wrangler runs.

🛡️ Proposed fix
-const cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? '2ea670c2a6ff28e248ef084adf095e8b';
-if (!supabaseUrl || !serviceKey) throw new Error('Supabase migration credentials are missing');
+const cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID;
+if (!supabaseUrl || !serviceKey) throw new Error('Supabase migration credentials are missing');
+if (!cloudflareAccountId) throw new Error('CLOUDFLARE_ACCOUNT_ID is required');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-supabase-to-cloudflare.mjs` at line 22, Remove the literal
fallback from the cloudflareAccountId initialization and require
CLOUDFLARE_ACCOUNT_ID to be present, matching the validation behavior used for
NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY. Ensure the migration
exits or throws before launching the wrangler child process when the variable is
unset.

Comment on lines +54 to +80
async function fetchAuthUsers() {
const users = [];
for (let page = 1; ; page += 1) {
const body = await fetchJson(`${supabaseUrl}/auth/v1/admin/users?page=${page}&per_page=1000`, {
headers: supabaseHeaders
});
const batch = body.users ?? [];
users.push(...batch);
if (batch.length < 1000) return users;
}
}

async function fetchTable(table) {
const rows = [];
for (let offset = 0; ; offset += 1000) {
const response = await fetch(`${supabaseUrl}/rest/v1/${table}?select=*`, {
headers: {
...supabaseHeaders,
range: `${offset}-${offset + 999}`
}
});
if (!response.ok) throw new Error(`Unable to export ${table} (${response.status})`);
const batch = await response.json();
rows.push(...batch);
if (batch.length < 1000) return rows;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Supabase Admin API listUsers per_page maximum limit

💡 Result:

The Supabase Admin API listUsers method allows you to paginate results using the per_page parameter [1][2]. While the default value for per_page is 50 [3][4], the maximum limit allowed for per_page is 1000 [5][1][2]. When using the Supabase client libraries (such as supabase-js), you can pass this parameter within the options object [1][2][6]: // Example using JavaScript/TypeScript const { data: { users }, error } = await supabase.auth.admin.listUsers({ page: 1, perPage: 1000 // Sets the per_page parameter to the maximum limit }); If you are calling the Auth Admin API directly via HTTP, use the per_page query parameter [7]: GET /auth/v1/admin/users?per_page=1000 Authorization: Bearer <SERVICE_ROLE_KEY> apikey: <SUPABASE_ANON_KEY>

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the migration script and relevant symbols without executing repo code.
if [ -f scripts/migrate-supabase-to-cloudflare.mjs ]; then
  echo "== file size =="
  wc -l scripts/migrate-supabase-to-cloudflare.mjs
  echo "== outline =="
  ast-grep outline scripts/migrate-supabase-to-cloudflare.mjs || true
  echo "== relevant lines 1-220 =="
  sed -n '1,220p' scripts/migrate-supabase-to-cloudflare.mjs | nl -ba
else
  echo "scripts/migrate-supabase-to-cloudflare.mjs not found"
  fd -a 'migrate-supabase-to-cloudflare\.mjs$' .
fi

echo "== search for migration and table order logic =="
rg -n "fetchAuthUsers|fetchTable|insertSql|tableColumns|order=|per_page|range:" . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 1605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant lines 1-120 =="
awk '{printf "%6d  %s\n", NR, $0}' scripts/migrate-supabase-to-cloudflare.mjs | sed -n '1,120p'

echo "== relevant lines 121-260 =="
awk '{printf "%6d  %s\n", NR, $0}' scripts/migrate-supabase-to-cloudflare.mjs | sed -n '121,260p'

echo "== exact matches =="
rg -n "fetchAuthUsers|fetchTable|insertSql|tableColumns|order=|per_page|range:" scripts/migrate-supabase-to-cloudflare.mjs || true

Repository: mani1261790/PHOTO-TEXTE

Length of output: 11722


🌐 Web query:

Supabase PostgREST range header order default duplicate rows documentation

💡 Result:

PostgREST does not have a default sort order when you use the Range header for pagination [1][2][3]. PostgreSQL does not guarantee any specific order for rows returned from a query unless an explicit ORDER BY clause is provided [1][3]. If you omit the order parameter while using pagination (e.g.,.range in Supabase or Range headers in PostgREST), the order of the results is effectively undefined [1][4]. This leads to two significant issues: 1. Unpredictable Results: Since the database does not guarantee an order, the result set can be different every time you execute the same request [1][4]. 2. Duplicate or Missing Rows: When you paginate through a dataset without a stable, unique ordering, rows may shift between pages. A row might appear on the first page, and then appear again on the second page (or be skipped entirely) because the database's internal ordering changed between the two requests [1][2][4]. To ensure reliable, consistent, and duplicate-free pagination, you must always include an explicit order clause on a unique column (such as a primary key or a unique timestamp) [5][4][3]. For example, in Supabase, you should chain an.order call before your.range call: // Example of stable pagination const { data, error } = await supabase.from('your_table').select('*').order('id', { ascending: true }) // Stable ordering by a unique column.range(0, 9); If you do not specify an order, the system may exhibit behavior that appears to be ordered (such as falling back to the physical disk order, often referred to as ctid), but this is not reliable and should never be relied upon for production applications [4][6].

Citations:


🌐 Web query:

Supabase admin api listUsers returns truncated short batch empty source count

💡 Result:

The Supabase admin API listUsers() method returns a limited, paginated set of users by default—typically 50—rather than the entire user list [1][2][3]. If you are encountering truncated results, empty data, or incorrect pagination counts, it is likely due to the following factors: 1. Default Pagination: listUsers() defaults to returning a single page of results (50 users) [1][2][3]. To retrieve more users, you must explicitly use the page and perPage parameters [4][3]. 2. Pagination Metadata Issues: Older versions of the Supabase JavaScript library contained bugs where the nextPage and lastPage metadata were incorrectly parsed if they exceeded a single digit (e.g., page 10 was read as page 1) [5][6]. If your application relies on these values to fetch subsequent pages, ensure you are using an up-to-date version of supabase-js, as these issues were resolved in later patches [5][6]. 3. Correct Implementation: You can control the batch size and page number by passing an options object to the method [4]: const { data: { users, nextPage, total }, error } = await supabase.auth.admin.listUsers({ page: 1, perPage: 1000 // Adjust as needed, usually up to 1000 }); If you need to retrieve all users and are seeing unexpected behavior, verify that you are not relying on truncated pagination links and consider fetching in a loop using the explicit page parameter rather than relying solely on the returned nextPage helper if you are on an older version [1][5]. Always ensure this code runs in a secure, server-side environment using your service_role key [4].

Citations:


Use stable pagination for the migration dataset.

fetchTable pages with only range, so PostgREST row order is not guaranteed between requests. That can produce duplicate or skipped rows; INSERT OR IGNORE then hides the duplicates while skipped rows are lost. Add &order=id.asc to every table query before the range and stop on an empty batch.

INSERT OR IGNORE does not replace the need to compare the migrated database/user counts against the source count before migration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/migrate-supabase-to-cloudflare.mjs` around lines 54 - 80, Update
fetchTable to request a stable ascending id order on every table query before
applying the range, and stop pagination when a batch is empty rather than only
when it is short. Also add pre-migration count comparisons between the source
Supabase users/table data and the destination database, failing or reporting a
mismatch before inserts proceed; do not rely on INSERT OR IGNORE to validate
completeness.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wrangler.jsonc`:
- Around line 62-66: Add the production BETTER_AUTH_URL to the top-level vars in
wrangler.jsonc, using the deployed production URL and preserving the existing
env.dev value. Ensure lib/auth/better-auth.ts receives this variable unchanged
for baseURL, secure cookies, and host allowlist behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ffad4fca-4c58-42e5-b164-d7614541c3ce

📥 Commits

Reviewing files that changed from the base of the PR and between 37195c7 and eda6063.

📒 Files selected for processing (5)
  • README.md
  • components/TopNav.tsx
  • package.json
  • scripts/migrate-supabase-to-cloudflare.mjs
  • wrangler.jsonc
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/TopNav.tsx
  • scripts/migrate-supabase-to-cloudflare.mjs
  • README.md

Comment thread wrangler.jsonc

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/globals.css (1)

184-184: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align .brand small with the minimum font-size rule.

The French override sets --minimum-font-size to 12px, while .brand small still uses hard-coded 11px and is not scoped inside the media-query exception. Use font-size: max(11px, var(--minimum-font-size)); or add an explicit comment that this element is intentionally exempt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/globals.css` at line 184, Update the `.brand small` rule to use
`font-size: max(11px, var(--minimum-font-size));`, ensuring it respects the
French minimum font-size override while retaining the 11px floor.
🧹 Nitpick comments (1)
components/LogoMark.tsx (1)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace deprecated priority with preload.

This repository uses Next.js 16.3.0, where Image priority is deprecated in favor of preload.

Proposed fix
-      priority
+      preload
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/LogoMark.tsx` at line 11, In the Image props within LogoMark,
replace the deprecated priority prop with preload while preserving the existing
eager-loading behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy-cloudflare-dev.yml:
- Around line 48-49: Add connection and transfer timeouts to both curl health
checks assigning login_status and auth_status, using curl’s --connect-timeout
and --max-time options with bounded values so unresponsive endpoints fail
promptly while preserving the existing HTTP status capture.
- Around line 43-44: Update the “Deploy dev Worker” workflow step to run the
shared D1 migration command before npm run cf:deploy:dev, targeting the dev D1
binding named photo-texte from wrangler.jsonc. Preserve the existing deployment
command and ordering so migrations complete before the Worker is deployed.
- Around line 20-22: Remove the job-level CLOUDFLARE_API_TOKEN from the workflow
and define it only in the Deploy dev Worker step’s environment, leaving the
account ID appropriately scoped. Update actions/checkout@v4 with
persist-credentials: false so the GitHub token is not retained for later steps.

In `@app/globals.css`:
- Line 1: Update the Google Fonts `@import` declaration in the global stylesheet
to use the configured quoted import-string notation instead of url(...), while
preserving the existing font URL and parameters.
- Around line 354-357: Update the focus-visible outline in the .topnav-links
a:focus-visible and .topnav-links button:focus-visible rules to use an opaque
theme teal color instead of the current 25%-opacity color, while preserving the
existing outline width and offset.

In `@lib/ai/client.ts`:
- Around line 66-80: Add a bounded AbortController timeout around the shared
OpenAI fetch in createOpenAIResponse, passing its signal to fetch so translate,
rewrite, and generateLearningNotes cannot hang indefinitely. Clear the timeout
in a finally block after the request completes or fails.

---

Outside diff comments:
In `@app/globals.css`:
- Line 184: Update the `.brand small` rule to use `font-size: max(11px,
var(--minimum-font-size));`, ensuring it respects the French minimum font-size
override while retaining the 11px floor.

---

Nitpick comments:
In `@components/LogoMark.tsx`:
- Line 11: In the Image props within LogoMark, replace the deprecated priority
prop with preload while preserving the existing eager-loading behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c611575-849c-41fa-afa1-74d35c8d5f2d

📥 Commits

Reviewing files that changed from the base of the PR and between eda6063 and 8112af8.

⛔ Files ignored due to path filters (4)
  • app/apple-icon.png is excluded by !**/*.png
  • app/icon.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • public/icon-192.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • .github/workflows/deploy-cloudflare-dev.yml
  • README.md
  • app/globals.css
  • components/LogoMark.tsx
  • lib/ai/client.ts
  • package.json
  • wrangler.jsonc
🚧 Files skipped from review as they are similar to previous changes (2)
  • wrangler.jsonc
  • README.md

Comment thread .github/workflows/deploy-cloudflare-dev.yml Outdated
Comment on lines +43 to +44
- name: Deploy dev Worker
run: npm run cf:deploy:dev

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
scripts = json.load(open("package.json"))["scripts"]
for name in ("cf:deploy:dev", "db:migrate:dev", "db:migrate:shared"):
    print(f"{name}: {scripts.get(name, '<missing>')}")
PY

rg -n -C 3 '"d1_databases"|"migrations_dir"|"dev"' wrangler.jsonc

Repository: mani1261790/PHOTO-TEXTE

Length of output: 1148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- workflow excerpt ---\n'
sed -n '1,90p' .github/workflows/deploy-cloudflare-dev.yml

printf '\n--- package scripts excerpt ---\n'
python3 - <<'PY'
import json
with open("package.json") as f:
    scripts = json.load(f)["scripts"]
for name in sorted(scripts):
    if "dev" in name.lower() or "cf:" in name or "db" in name.lower() or "migrate" in name.lower():
        print(f"{name}: {scripts[name]}")
PY

printf '\n--- cloudflare/migrations directory ---\n'
if [ -d cloudflare/migrations ]; then
  find cloudflare/migrations -maxdepth 2 -type f -print | sort
else
  echo '<missing>'
fi

printf '\n--- deploy scripts references ---\n'
rg -n "cf:deploy:dev|opennextjs-cloudflare|wrangler.*d1|migrations apply|DB --remote|env.dev|env=\"\"" .

Repository: mani1261790/PHOTO-TEXTE

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow excerpt ---'
sed -n '1,90p' .github/workflows/deploy-cloudflare-dev.yml

printf '%s\n' ''
printf '%s\n' '--- package scripts excerpt ---'
python3 - <<'PY'
import json
with open("package.json") as f:
    scripts = json.load(f)["scripts"]
for name in sorted(scripts):
    if "dev" in name.lower() or "cf:" in name or "db" in name.lower() or "migrate" in name.lower():
        print(f"{name}: {scripts[name]}")
PY

printf '%s\n' ''
printf '%s\n' '--- cloudflare/migrations directory ---'
if [ -d cloudflare/migrations ]; then
  find cloudflare/migrations -maxdepth 2 -type f -print | sort
else
  echo '<missing>'
fi

printf '%s\n' ''
printf '%s\n' '--- deploy scripts references ---'
rg -n "cf:deploy:dev|opennextjs-cloudflare|wrangler.*d1|migrations apply|DB --remote|env.dev|env=\"\"" .

Repository: mani1261790/PHOTO-TEXTE

Length of output: 3352


Run D1 migrations before cf:deploy:dev.

npm run cf:deploy:dev only builds and deploys the dev Worker, and there is no separate cf:migrate:dev script. Run the shared dev D1 migration command before deployment so new Worker code does not execute against an older photo-texte schema. Use the dev D1 binding from wrangler.jsonc when applying migrations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy-cloudflare-dev.yml around lines 43 - 44, Update the
“Deploy dev Worker” workflow step to run the shared D1 migration command before
npm run cf:deploy:dev, targeting the dev D1 binding named photo-texte from
wrangler.jsonc. Preserve the existing deployment command and ordering so
migrations complete before the Worker is deployed.

Comment thread .github/workflows/deploy-cloudflare-dev.yml Outdated
Comment thread app/globals.css Outdated
Comment thread app/globals.css
Comment thread lib/ai/client.ts Outdated
@mani1261790 mani1261790 changed the title Revamp bilingual editor and add PDF export Move PHOTO-TEXTE to Cloudflare and refine bilingual workflow Aug 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/entries/new/page.tsx (1)

69-69: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve compliant PNG and WebP files.

Line 69 sends every accepted image through canvas.toBlob(..., "image/jpeg"). This converts compliant PNG and WebP files to JPEG. It removes PNG transparency and drops animated WebP frames before upload.

Return the original file when it already meets the dimension and size limits.

Proposed fix
   const scale = Math.min(
     1,
     MAX_IMAGE_DIMENSION / Math.max(image.naturalWidth, image.naturalHeight),
   );
+  if (scale === 1 && file.size <= MAX_UPLOAD_BYTES) {
+    return file;
+  }
   const width = Math.max(1, Math.round(image.naturalWidth * scale));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/entries/new/page.tsx` at line 69, Update the image-processing flow around
the file type check and canvas conversion so files that already satisfy the
dimension and size limits are returned unchanged, preserving compliant PNG and
WebP formats. Only invoke the existing JPEG conversion for images that require
resizing or size reduction.
components/EntryWizard.tsx (2)

533-539: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep annotation state stable during export.

exportFile saves dirty annotations with silent=true. This path does not set annotationSavingId, so CorrectionAnnotationEditor can remain editable while the export save is in flight. If the user changes an annotation during that request, Line 463 clears its dirty state from the stale save. The export can then use older annotations and the new edit can appear saved without persistence.

Disable annotation editing during export, or use a revision check that clears dirty state only when the saved snapshot still matches the current annotations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/EntryWizard.tsx` around lines 533 - 539, Update exportFile’s
dirty-annotation save flow around saveAnnotationsForPhoto so annotation editing
is blocked for the entire in-flight export save, or add a revision/snapshot
guard that preserves dirty state when annotations change during the request.
Ensure stale saves cannot clear newer edits, and the export uses the latest
stable annotation values.

487-510: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Serialize self-note saves.

saveSelfNote can run again before an earlier request resolves. Each call uses its captured memos value. If no self-note exists, overlapping calls can both issue POST requests. If an earlier request completes after a newer edit, it clears memoPendingSave for the newer draft. The debounce then cancels the newer save, and the UI can show an unsaved note as saved.

Queue note mutations, or track a revision per draft. Clear memoPendingSave only when the completed request matches the latest revision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/EntryWizard.tsx` around lines 487 - 510, Serialize mutations in
saveSelfNote so overlapping saves cannot use stale memos or issue duplicate POST
requests; chain each save behind the previous request or otherwise track the
latest draft revision. Ensure memoPendingSave is cleared and memoSavedAt is
updated only when the completed save corresponds to the latest revision, leaving
newer edits pending.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/EntryWizard.tsx`:
- Around line 225-231: Update the initialization around
setGeneratedFinalByPhotoId in EntryWizard so it uses the API’s separate
generated-final-text field instead of photo.final_fr. Ensure the corresponding
API persistence returns and exposes that generated field, allowing
activeGeneratedFinal and the restore action to retain the original generated
text after manual edits and reloads.

---

Outside diff comments:
In `@app/entries/new/page.tsx`:
- Line 69: Update the image-processing flow around the file type check and
canvas conversion so files that already satisfy the dimension and size limits
are returned unchanged, preserving compliant PNG and WebP formats. Only invoke
the existing JPEG conversion for images that require resizing or size reduction.

In `@components/EntryWizard.tsx`:
- Around line 533-539: Update exportFile’s dirty-annotation save flow around
saveAnnotationsForPhoto so annotation editing is blocked for the entire
in-flight export save, or add a revision/snapshot guard that preserves dirty
state when annotations change during the request. Ensure stale saves cannot
clear newer edits, and the export uses the latest stable annotation values.
- Around line 487-510: Serialize mutations in saveSelfNote so overlapping saves
cannot use stale memos or issue duplicate POST requests; chain each save behind
the previous request or otherwise track the latest draft revision. Ensure
memoPendingSave is cleared and memoSavedAt is updated only when the completed
save corresponds to the latest revision, leaving newer edits pending.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bdda9146-7c55-47ec-80b8-9c3f4b1234c2

📥 Commits

Reviewing files that changed from the base of the PR and between 8112af8 and a3d2577.

📒 Files selected for processing (6)
  • .github/workflows/deploy-cloudflare-dev.yml
  • app/entries/new/page.tsx
  • app/globals.css
  • components/EntryWizard.tsx
  • lib/ai/client.ts
  • wrangler.jsonc
🚧 Files skipped from review as they are similar to previous changes (3)
  • wrangler.jsonc
  • lib/ai/client.ts
  • .github/workflows/deploy-cloudflare-dev.yml

Comment on lines +225 to +231
setGeneratedFinalByPhotoId((current) => {
const next = { ...current };
orderedPhotos.forEach((photo) => {
if (next[photo.id] === undefined) next[photo.id] = photo.final_fr ?? "";
});
return next;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Persist a separate generated-text baseline.

Lines 225-231 initialize generatedFinalByPhotoId from photo.final_fr. saveFinalText persists manual edits in final_fr. After a reload, activeGeneratedFinal contains the saved manual text, not the generated text. The restore action then cannot restore the generated version.

Store the generated final text separately in the API and initialize this state from that field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/EntryWizard.tsx` around lines 225 - 231, Update the initialization
around setGeneratedFinalByPhotoId in EntryWizard so it uses the API’s separate
generated-final-text field instead of photo.final_fr. Ensure the corresponding
API persistence returns and exposes that generated field, allowing
activeGeneratedFinal and the restore action to retain the original generated
text after manual edits and reloads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant