Skip to content

feat: initial release - #4

Open
XiaokiYolo wants to merge 1 commit into
code-with-antonio:masterfrom
XiaokiYolo:main
Open

feat: initial release#4
XiaokiYolo wants to merge 1 commit into
code-with-antonio:masterfrom
XiaokiYolo:main

Conversation

@XiaokiYolo

@XiaokiYolo XiaokiYolo commented Apr 29, 2026

Copy link
Copy Markdown

add some offline support,and fixx some fixssssss

Summary by CodeRabbit

Release Notes

  • New Features

    • Offline-first capability—continue learning without internet; all changes sync automatically when reconnected
    • Native mobile apps for Android and iOS via Capacitor
    • Real-time network connectivity indicator
    • Progressive Web App (PWA) support for web app installation
  • Documentation

    • Comprehensive mobile setup and offline feature documentation
    • Updated README with deployment guides and architecture overview
  • Chores

    • Docker support for containerized deployments

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request transforms the project into an offline-first language learning app ("Lingo") with full Capacitor-based mobile support, complete Android native configuration, SQLite offline database with sync engine, Docker containerization, and comprehensive architectural documentation.

Changes

Cohort / File(s) Summary
Docker & Container Setup
.dockerignore, Dockerfile
Introduces Docker build context exclusions and Alpine Node.js 18 containerization with npm dependency installation, Next.js build, and port 3000 exposure.
Mobile Architecture Documentation
MOBILE_ARCHITECTURE.md, TEST_REPORT.md
Adds detailed offline-first architecture guide with Capacitor/SQLite integration, development commands, sync flow specifications, and testing report with environment setup/blocking issues.
Project README & Metadata
README.md, app/manifest.ts
Expands README to cover offline capabilities, mobile setup, and architecture; introduces PWA manifest with app configuration and icon definitions.
Android Build & Gradle Configuration
android/build.gradle, android/variables.gradle, android/settings.gradle, android/gradle.properties, android/gradle/wrapper/gradle-wrapper.properties, android/gradlew, android/gradlew.bat
Establishes complete Android Gradle buildscript, version variables, wrapper scripts (POSIX/Windows), and JVM/repository configuration.
Android App Module Configuration
android/app/build.gradle, android/app/proguard-rules.pro, android/.gitignore, android/app/.gitignore
Configures app-level Gradle build with AndroidX/Capacitor dependencies, ProGuard template, and platform-specific gitignore patterns.
Android Manifest & Resources
android/app/src/main/AndroidManifest.xml, android/app/src/main/res/...
Declares MainActivity as launcher entry, configures FileProvider, registers INTERNET permission, and adds adaptive launcher icons, themes, layouts, and string resources.
Android App Code
android/app/src/main/java/com/lingo/app/MainActivity.java
Introduces MainActivity extending BridgeActivity to enable Capacitor activity bridge.
Android Tests
android/app/src/androidTest/java/.../ExampleInstrumentedTest.java, android/app/src/test/java/.../ExampleUnitTest.java
Adds instrumented and unit test examples validating app context and basic arithmetic.
Offline Database & Query Layer
db/offline-db.ts, db/offline-queries.ts
Implements SQLite-based offline database service with table initialization, import/export, and query functions; adds offline-aware read/write operations with sync queue management.
Sync Engine & Mobile Utilities
hooks/use-sync-engine.ts, lib/mobile-app.ts, scripts/seed-offline.ts
Adds network-aware sync hook to persist queued changes via API, mobile app lifecycle listeners, and offline database seeding with initial course/lesson data.
React Providers & Components
providers/database-provider.tsx, providers/offline-provider.tsx, components/network-status.tsx
Introduces database initialization provider with error handling, offline state context with network polling, and network status toast notification component.
App Configuration & Layout
app/layout.tsx, app/globals.css, capacitor.config.ts, next.config.mjs, package.json, vercel.json
Updates root layout with database/offline providers, adds default font class, configures Capacitor splash screen and app identification, adds build scripts and dependencies (Capacitor/sql.js), and injects Vercel/Clerk environment variables.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~85 minutes

Poem

🐰 Whiskers twitch with digital delight,
SQLite dances in offline night,
Capacitor bridges build mobile dreams,
Docker containers flow like streams,
Lingo learns while the network's gone—
Sync when morning brings the dawn! 🌅

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'feat: initial release' is vague and does not accurately reflect the substantial changes in this PR, which focus on implementing offline-first mobile architecture with SQLite, Capacitor integration, Android setup, and PWA support rather than an 'initial release'. Consider a more descriptive title such as 'feat: add offline-first mobile architecture with SQLite and Capacitor' or 'feat: implement offline support and Android mobile integration' to better communicate the primary changes to reviewers.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@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: 10

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
next.config.mjs (2)

21-23: ⚠️ Potential issue | 🔴 Critical

Fix malformed Content-Range header.

The Content-Range header value "bytes : 0-9/*" has incorrect formatting. The space after bytes makes it non-standard. The correct format should be either "bytes 0-9/*" or "bytes: 0-9/*".

This malformed header may cause parsing errors in HTTP clients.

🐛 Proposed fix for the Content-Range header
           {
             key: "Content-Range",
-            value: "bytes : 0-9/*",
+            value: "bytes 0-9/*",
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.mjs` around lines 21 - 23, The Content-Range header value is
malformed in the headers config (key "Content-Range" with value "bytes :
0-9/*"); update the value to a valid format such as "bytes 0-9/*" (preferred
standard) or "bytes: 0-9/*" so the header is parseable by HTTP clients and
replace the current value in the headers configuration where the "Content-Range"
key is set.

9-11: ⚠️ Potential issue | 🟠 Major

Security: Overly permissive CORS configuration.

Setting Access-Control-Allow-Origin to "*" allows any domain to access your API endpoints. This creates a security risk by exposing your API to cross-origin requests from untrusted sources.

Consider restricting this to specific trusted domains (e.g., your production domain) or implement dynamic origin validation based on an allowlist.

🔒 Proposed fix to restrict CORS to specific origins
         headers: [
           {
             key: "Access-Control-Allow-Origin",
-            value: "*",
+            value: process.env.ALLOWED_ORIGIN || "https://yourdomain.com",
           },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.mjs` around lines 9 - 11, The CORS header currently sets
"Access-Control-Allow-Origin" to "*" which is too permissive; update the code
that sets the "Access-Control-Allow-Origin" header to validate the request
origin against an allowlist (or use a configured trustedOrigin value) and only
return that origin when it matches, otherwise deny or return a safe default;
locate the place that sets the "Access-Control-Allow-Origin" header (the key
"Access-Control-Allow-Origin") and replace the wildcard with dynamic origin
validation using an allowlist of trusted domains (e.g., production and dev
domains) or a configured single trusted origin.
🟠 Major comments (13)
.dockerignore-4-4 (1)

4-4: ⚠️ Potential issue | 🟠 Major

Broaden env-file ignores to prevent secret leakage into Docker build context.

Ignoring only .env is incomplete; files like .env.local, .env.production, and .env.development.local can still be sent to the daemon.

🔒 Suggested hardening
-.env
+.env
+.env.*
+!.env.example
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.dockerignore at line 4, Update the .dockerignore to broaden ignored
env-file patterns so secrets don't get sent to the Docker build context: add
wildcard patterns such as .env*, .env.* and specific variants like .env.local,
.env.production, .env.development.local (or a single .env* pattern) to ensure
all environment files are excluded; modify the existing ".env" entry in
.dockerignore to include these patterns so Docker will ignore any env variants
during builds.
Dockerfile-1-9 (1)

1-9: ⚠️ Potential issue | 🟠 Major

Container runs as root; drop privileges before runtime.

No USER is defined, so the app runs as root in production. Add a non-root runtime user.

🛡️ Suggested hardening
 FROM node:18-alpine
 WORKDIR /app
 ENV NEXT_TELEMETRY_DISABLED=1
 COPY package*.json ./
 RUN npm ci
 COPY . .
 RUN npm run build
+RUN chown -R node:node /app
+USER node
 EXPOSE 3000
 CMD ["npm", "start"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 1 - 9, The Dockerfile currently runs the app as root
(no USER set); create a non-root runtime user and switch to it before CMD: add a
dedicated unprivileged user/group (or use the existing node user), ensure
ownership/permissions for WORKDIR /app are changed (chown -R) so the new user
can access build artifacts created by RUN npm run build, then set USER to that
unprivileged account before CMD ["npm","start"]; keep the build steps as-is but
perform chown after COPY . . / after build if needed so runtime files are
readable by the non-root user.
app/manifest.ts-1-27 (1)

1-27: ⚠️ Potential issue | 🟠 Major

Update app/manifest.ts to use the Next.js metadata-route handler signature.

This file must export a typed default function returning MetadataRoute.Manifest, not a plain object, to comply with Next.js App Router metadata conventions.

✅ Correct structure
+import type { MetadataRoute } from 'next';
+
-export const manifest = {
-  name: 'Lingo - Language Learning',
-  short_name: 'Lingo',
-  description: 'Offline-capable language learning app',
-  start_url: '/',
-  display: 'standalone',
-  background_color: '#ffffff',
-  theme_color: '#4CAF50',
-  orientation: 'portrait',
-  icons: [
-    {
-      src: '/mascot.svg',
-      sizes: '192x192',
-      type: 'image/svg+xml',
-      purpose: 'any maskable',
-    },
-    {
-      src: '/mascot.svg',
-      sizes: '512x512',
-      type: 'image/svg+xml',
-      purpose: 'any maskable',
-    },
-  ],
-  categories: ['education', 'productivity'],
-  lang: 'en',
-};
-export default manifest;
+export default function manifest(): MetadataRoute.Manifest {
+  return {
+    name: 'Lingo - Language Learning',
+    short_name: 'Lingo',
+    description: 'Offline-capable language learning app',
+    start_url: '/',
+    display: 'standalone',
+    background_color: '#ffffff',
+    theme_color: '#4CAF50',
+    orientation: 'portrait',
+    icons: [
+      {
+        src: '/mascot.svg',
+        sizes: '192x192',
+        type: 'image/svg+xml',
+        purpose: 'any maskable',
+      },
+      {
+        src: '/mascot.svg',
+        sizes: '512x512',
+        type: 'image/svg+xml',
+        purpose: 'any maskable',
+      },
+    ],
+    categories: ['education', 'productivity'],
+    lang: 'en',
+  };
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/manifest.ts` around lines 1 - 27, Replace the plain exported object named
manifest and the default export with a typed metadata-route handler: export a
default function (typed to return MetadataRoute.Manifest) that returns the
manifest object; keep the same manifest structure (name, short_name, icons,
etc.) inside the function and ensure the function signature uses the
MetadataRoute.Manifest return type (import the type if needed) so Next.js App
Router recognizes it as a metadata route handler instead of a plain object.
android/app/src/main/res/xml/file_paths.xml-3-4 (1)

3-4: ⚠️ Potential issue | 🟠 Major

Restrict FileProvider path scope to app-owned directories.

Line 3 uses <external-path path="." />, which exposes the entire external storage root to URI access. This is a major security risk that enables attackers to access sensitive files and directories beyond the app's scope. Replace it with <external-files-path> for app-scoped storage.

Line 4's <cache-path path="." /> should also specify a subdirectory instead of the root path.

🔐 Suggested path narrowing
 <paths xmlns:android="http://schemas.android.com/apk/res/android">
-    <external-path name="my_images" path="." />
-    <cache-path name="my_cache_images" path="." />
+    <external-files-path name="shared_images" path="Pictures/" />
+    <cache-path name="shared_cache_images" path="images/" />
 </paths>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/res/xml/file_paths.xml` around lines 3 - 4, Replace the
broad external-path and root cache-path entries with app-scoped, narrowed paths:
change the <external-path name="my_images" path="." /> entry to
<external-files-path name="my_images" path="Pictures/your_subdir" /> (use
external-files-path to limit URIs to the app's external files directory and pick
a specific subdirectory), and update the <cache-path name="my_cache_images"
path="." /> entry to specify a subfolder like <cache-path name="my_cache_images"
path="cache_images" /> so the FileProvider only exposes app-owned subdirectories
(refer to the external-path/external-files-path and cache-path tag names and the
my_images/my_cache_images identifiers to locate the entries).
android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java-1-1 (1)

1-1: ⚠️ Potential issue | 🟠 Major

Package name mismatch - test file is in wrong package.

The test is declared in package com.getcapacitor.myapp, but the application uses com.lingo.app (per android/app/build.gradle). The test file should be moved to the correct package directory.

Expected path: android/app/src/androidTest/java/com/lingo/app/ExampleInstrumentedTest.java

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`
at line 1, The test's package declaration is incorrect:
ExampleInstrumentedTest.java currently declares package com.getcapacitor.myapp
but the app package is com.lingo.app; update the package line to "package
com.lingo.app;" and move the file into the matching directory
(android/app/src/androidTest/java/com/lingo/app/ExampleInstrumentedTest.java) so
the class ExampleInstrumentedTest is in the correct package for instrumentation
tests. Ensure the package declaration and filesystem path match exactly.
android/app/src/main/AndroidManifest.xml-38-40 (1)

38-40: 🛠️ Refactor suggestion | 🟠 Major

Add required ACCESS_NETWORK_STATE permission for Capacitor Network plugin.

The app uses @capacitor/network for offline sync functionality. The Capacitor Network plugin requires the ACCESS_NETWORK_STATE permission on Android to query network status and listen for connectivity changes via the ConnectivityManager API. This permission is install-time only (no runtime request needed) and is classified as a normal permission.

Required addition
 <!-- Permissions -->

 <uses-permission android:name="android.permission.INTERNET" />
+<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/AndroidManifest.xml` around lines 38 - 40, Add the
Android manifest permission for ACCESS_NETWORK_STATE so the Capacitor Network
plugin can query connectivity; update the AndroidManifest.xml (near the existing
uses-permission for android.permission.INTERNET) to include a uses-permission
element for android.permission.ACCESS_NETWORK_STATE and ensure it is placed in
the manifest's manifest-level permissions block so ConnectivityManager-based
network checks in the Network plugin work correctly.
hooks/use-sync-engine.ts-63-85 (1)

63-85: ⚠️ Potential issue | 🟠 Major

Fetch requests lack timeout, risking indefinite hangs.

The sync functions don't set a timeout on fetch calls. On unreliable networks, requests could hang indefinitely, blocking the sync queue.

🔧 Proposed fix using AbortController
 const syncChallengeProgress = async (data: any) => {
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 30000);
+
-  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/challenge-progress`, {
+  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/challenge-progress`, {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(data),
+    signal: controller.signal,
   });
+  clearTimeout(timeoutId);
 
   if (!response.ok) {
     throw new Error('Failed to sync challenge progress');
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-sync-engine.ts` around lines 63 - 85, The fetch calls in
syncChallengeProgress and syncUserProgress can hang because they lack a timeout;
wrap each request with an AbortController, start a timer (e.g., setTimeout) that
calls controller.abort() after a reasonable timeout (e.g., 5–10s), pass
controller.signal to fetch, and clear the timer when the response arrives;
ensure you catch AbortError separately (or rethrow a descriptive Error like
"Request timed out") so the sync queue can handle it deterministically.
db/offline-db.ts-150-154 (1)

150-154: ⚠️ Potential issue | 🟠 Major

importData lacks validation, risking data corruption.

The importData method parses and imports arbitrary JSON without validating its structure. Malformed or malicious data could corrupt the database or cause unexpected behavior.

🛡️ Proposed fix
   async importData(jsonData: string) {
     if (!this.db) throw new Error('Database not initialized');
-    const data = JSON.parse(jsonData);
-    await this.db.importFromJson('full', data);
+    let data: JsonSQLite;
+    try {
+      data = JSON.parse(jsonData);
+    } catch (e) {
+      throw new Error('Invalid JSON format for import');
+    }
+    
+    // Basic structure validation
+    if (!data || typeof data !== 'object' || !data.database) {
+      throw new Error('Invalid database export format');
+    }
+    
+    await this.db.importFromJson('full', data);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-db.ts` around lines 150 - 154, The importData method currently
parses arbitrary JSON and forwards it to this.db.importFromJson('full', ...); to
prevent corruption, validate the parsed data before calling importFromJson: wrap
JSON.parse in try/catch, verify the result is the expected shape (e.g., an
object with the expected top-level keys/arrays your DB expects) or run it
through a JSON Schema validator, and throw a descriptive error if validation
fails; reference the importData function and this.db.importFromJson('full', ...)
and ensure all errors are logged/propagated rather than importing malformed
data.
hooks/use-sync-engine.ts-27-61 (1)

27-61: ⚠️ Potential issue | 🟠 Major

Stale closure issue: isSyncing check uses captured value.

syncPendingChanges is called from the network listener but captures the initial isSyncing value (false). The guard on line 28 will always see false, allowing concurrent syncs if multiple network changes occur rapidly.

🔧 Proposed fix using a ref
+import { useEffect, useState, useRef, useCallback } from 'react';
 
 export function useSyncEngine() {
   const [isOnline, setIsOnline] = useState(true);
   const [isSyncing, setIsSyncing] = useState(false);
   const [lastSyncTime, setLastSyncTime] = useState<Date | null>(null);
+  const isSyncingRef = useRef(false);
 
-  const syncPendingChanges = async () => {
-    if (isSyncing || !isOnline) return;
+  const syncPendingChanges = useCallback(async () => {
+    if (isSyncingRef.current) return;
 
+    isSyncingRef.current = true;
     setIsSyncing(true);
     try {
       // ... existing sync logic
     } finally {
+      isSyncingRef.current = false;
       setIsSyncing(false);
     }
-  };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-sync-engine.ts` around lines 27 - 61, syncPendingChanges currently
reads the stale isSyncing boolean from closure which allows concurrent runs;
change to use a mutable ref (e.g., isSyncingRef) that you check and set inside
syncPendingChanges instead of the captured isSyncing state: create const
isSyncingRef = useRef(false), update isSyncingRef.current whenever you call
setIsSyncing (or directly set it in syncPendingChanges), replace the initial
guard (if (isSyncing || !isOnline)) with checking isSyncingRef.current, and
ensure you set isSyncingRef.current = true at start and false in finally so
concurrent invocations are prevented while still keeping setIsSyncing for UI
state.
hooks/use-sync-engine.ts-48-51 (1)

48-51: ⚠️ Potential issue | 🟠 Major

Failed sync items are never retried, causing potential data loss.

In use-sync-engine.ts (line 50), failed syncs call markSyncFailed, which sets status = 'failed' and increments retry_count. However, getSyncQueue (line 126 of offline-queries.ts) only retrieves items with status = 'pending', so failed items remain in the database indefinitely. The retry_count field exists but is never checked for a limit or reset.

Implement:

  1. A retry mechanism that resets status to 'pending' after a backoff period
  2. A maximum retry limit before marking items as permanently failed
  3. User notification for failed syncs requiring manual intervention
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-sync-engine.ts` around lines 48 - 51, The catch in
use-sync-engine.ts currently calls offlineQueries.markSyncFailed(item.id) but
failed items are never retried; update offline-queries.ts: change markSyncFailed
to increment retry_count, set a next_retry_at timestamp using exponential
backoff (e.g., now + baseDelay * 2^retry_count) and keep status='failed' until
next_retry_at; modify getSyncQueue to return items where (status='pending') OR
(status='failed' AND next_retry_at <= now AND retry_count < MAX_RETRIES); add a
MAX_RETRIES constant and if markSyncFailed would exceed it, set
status='permanent_failed' and trigger a user notification via the existing
notification helper (or add notifyUser/queueUserAlert) so users can intervene;
also ensure successful sync path (function handling success in
use-sync-engine.ts) resets retry_count and clears next_retry_at.
db/offline-queries.ts-124-127 (1)

124-127: ⚠️ Potential issue | 🟠 Major

Failed sync items are never retried with current queue filters.

getSyncQueue reads only pending, but markSyncFailed sets failed. After one failure, items stop being processed.

🛠️ Proposed fix
   getSyncQueue: async () => {
     const result = await dbService.executeSelectQuery(
-      `SELECT * FROM sync_queue WHERE status = 'pending' ORDER BY created_at ASC`,
+      `SELECT * FROM sync_queue
+       WHERE status = 'pending' OR (status = 'failed' AND retry_count < 3)
+       ORDER BY created_at ASC`,
       []
     );
@@
   markSyncFailed: async (syncId: number) => {
     await dbService.executeQuery(
-      `UPDATE sync_queue SET status = 'failed', retry_count = retry_count + 1 WHERE id = ?`,
+      `UPDATE sync_queue
+       SET retry_count = retry_count + 1,
+           status = CASE WHEN retry_count + 1 >= 3 THEN 'failed' ELSE 'pending' END
+       WHERE id = ?`,
       [syncId]
     );
   },

Also applies to: 139-142

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-queries.ts` around lines 124 - 127, getSyncQueue currently filters
only status = 'pending' while markSyncFailed sets status = 'failed', causing
failed items never to be retried; update getSyncQueue (and the similar query
around the other occurrence) to include both pending and failed items (e.g.,
WHERE status IN ('pending','failed') ORDER BY created_at ASC) so failed items
are picked up for retry. Ensure you update the SQL in the getSyncQueue function
and the duplicate query at the other occurrence (lines referenced in the review)
to use the IN clause.
db/offline-queries.ts-108-114 (1)

108-114: ⚠️ Potential issue | 🟠 Major

Guard and sanitize dynamic update fields before SQL construction.

Line 109 interpolates keys directly into SQL and fails when updates is empty (SET , sync_status...).

🛠️ Proposed fix
   updateUserProgress: async (userId: string, updates: Record<string, any>) => {
-    const fields = Object.keys(updates).map(key => `${key} = ?`).join(', ');
-    const values = [...Object.values(updates), userId];
+    const allowed = new Set([
+      'user_name',
+      'user_image_src',
+      'active_course_id',
+      'hearts',
+      'points',
+    ]);
+    const entries = Object.entries(updates).filter(([key]) => allowed.has(key));
+    if (entries.length === 0) return;
+
+    const fields = entries.map(([key]) => `${key} = ?`).join(', ');
+    const values = [...entries.map(([, value]) => value), userId];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-queries.ts` around lines 108 - 114, In updateUserProgress ensure
dynamic field names are validated and handle empty updates: validate keys from
the updates object against an allowlist of permitted column names (e.g.,
acceptedColumns) and build the fields/placeholders only from those validated
keys; if no valid keys remain, omit the interpolated SET fragment and execute a
query that only updates sync_status = 'pending' for the given userId; always use
parameterized values (as you already do) and never interpolate raw keys without
validation so the function (updateUserProgress) cannot produce malformed SQL
like "SET , sync_status...".
db/offline-queries.ts-92-99 (1)

92-99: ⚠️ Potential issue | 🟠 Major

Add a UNIQUE constraint on (user_id, challenge_id) to the challenge_progress table.

The ON CONFLICT(user_id, challenge_id) clause in the upsert query requires a matching UNIQUE or PRIMARY KEY constraint. The current table definition has only a PRIMARY KEY on id, so this upsert will fail at runtime. Add either a UNIQUE constraint in the table definition or a unique index to support the conflict resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-queries.ts` around lines 92 - 99, The upsert in
upsertChallengeProgress uses ON CONFLICT(user_id, challenge_id) but the
challenge_progress table only has id as PRIMARY KEY, so add a UNIQUE
constraint/index on (user_id, challenge_id) in the DB schema/migration to
satisfy the ON CONFLICT target; update the table creation or add a migration
that creates a UNIQUE constraint or a unique index (e.g., unique index on
user_id, challenge_id) so the upsertConflict target exists and the
upsertChallengeProgress query will succeed.
🟡 Minor comments (9)
TEST_REPORT.md-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

Update the report date to the actual test run date.

The report currently says 2024-04-24, but this PR was opened on April 29, 2026. Keeping this accurate improves trust in the test evidence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@TEST_REPORT.md` at line 4, Update the test report date string in
TEST_REPORT.md from "2024-04-24" to the actual test run date (e.g.,
"2026-04-29"); locate the dated line containing 2024-04-24 and replace it with
the correct ISO-style date so the report reflects the PR's test run date.
TEST_REPORT.md-77-79 (1)

77-79: ⚠️ Potential issue | 🟡 Minor

Add a language to the fenced error block.

This currently violates markdownlint MD040 and should be typed (for example text).

🛠️ Proposed fix
-```
+```text
 Error: Missing Clerk Secret Key or API Key
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @TEST_REPORT.md around lines 77 - 79, The fenced code block containing the
string "Error: Missing Clerk Secret Key or API Key" should include a language
specifier to satisfy markdownlint MD040; update the triple-backtick fence to
start with ```text so the block reads as a text code fence and preserves the
existing error message unchanged.


</details>

</blockquote></details>
<details>
<summary>android/gradlew.bat-1-94 (1)</summary><blockquote>

`1-94`: _⚠️ Potential issue_ | _🟡 Minor_

**Windows batch file uses Unix line endings (LF) which may cause script failures.**

This batch file uses Unix-style line endings (LF only), but Windows batch scripts require CRLF line endings. The Windows batch parser has documented issues with LF-only files that can cause label parsing failures and script malfunctions.

Configure Git to handle line endings automatically or convert the file:

```shell
# Option 1: Configure Git for this file type
echo "*.bat text eol=crlf" >> .gitattributes

# Option 2: Convert existing file (if unix2dos is available)
unix2dos android/gradlew.bat
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@android/gradlew.bat` around lines 1 - 94, The gradlew.bat script contains
Unix LF-only line endings which can break Windows batch parsing (labels like
:execute, :fail, :mainEnd); fix by converting this file to CRLF line endings and
ensuring future commits keep CRLF for .bat files—add a .gitattributes rule for
"*.bat text eol=crlf" and convert the existing gradlew.bat to CRLF (using your
preferred tool or git settings) so labels and batch parsing behave correctly.
```

</details>

</blockquote></details>
<details>
<summary>providers/database-provider.tsx-27-43 (1)</summary><blockquote>

`27-43`: _⚠️ Potential issue_ | _🟡 Minor_

**Cleanup may close database prematurely in React Strict Mode.**

In React 18 Strict Mode (development), effects run twice: mount → unmount → mount. The cleanup function calling `dbService.close()` during the first unmount can close the database before the second mount's `init()` completes or runs, causing initialization failures.

Consider adding a ref to track whether initialization completed successfully before allowing close:

<details>
<summary>Suggested approach</summary>

```diff
+import { useRef } from 'react';
+
 export const DatabaseProvider = ({ children }: DatabaseProviderProps) => {
   const [isReady, setIsReady] = useState(false);
   const [error, setError] = useState<Error | null>(null);
+  const initSucceeded = useRef(false);

   useEffect(() => {
     const initDb = async () => {
       try {
         await dbService.init();
+        initSucceeded.current = true;
         setIsReady(true);
       } catch (err) {
         setError(err as Error);
         console.error('Failed to initialize database:', err);
       }
     };

     initDb();

     return () => {
-      dbService.close().catch(console.error);
+      if (initSucceeded.current) {
+        dbService.close().catch(console.error);
+      }
     };
   }, []);
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@providers/database-provider.tsx` around lines 27 - 43, The cleanup may close
the DB prematurely in React Strict Mode because the effect unmounts before the
second mount’s init completes; modify the effect around initDb/useEffect to use
a ref (e.g., isInitializedRef) or a mountedRef to track successful
initialization: set isInitializedRef.current = true only after await
dbService.init() and setIsReady(true), and in the cleanup call dbService.close()
only if isInitializedRef.current is true (or set a flag to ignore the first
unmount), also ensure errors still call setError(err) and console.error(err) as
before; update symbols: useEffect, initDb, dbService.init, dbService.close,
setIsReady, setError.
```

</details>

</blockquote></details>
<details>
<summary>scripts/seed-offline.ts-9-17 (1)</summary><blockquote>

`9-17`: _⚠️ Potential issue_ | _🟡 Minor_

**Add null check before accessing query results.**

If the `courses` table doesn't exist yet or the query fails silently, `courseCount.values` could be undefined or empty, causing a runtime error on line 14.


<details>
<summary>🛡️ Proposed fix</summary>

```diff
     const courseCount = await dbService.executeSelectQuery(
       'SELECT COUNT(*) as count FROM courses',
       []
     );
 
-    if (courseCount.values[0].count > 0) {
+    if (courseCount.values?.[0]?.count > 0) {
       console.log('Database already seeded, skipping...');
       return;
     }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@scripts/seed-offline.ts` around lines 9 - 17, Add a null/shape check before
reading courseCount.values[0].count: after calling dbService.executeSelectQuery
(symbol: courseCount and dbService.executeSelectQuery), verify courseCount is
defined, courseCount.values is an array, and courseCount.values[0] exists and
has a numeric count; if any check fails treat the count as 0 (or log an error)
so the code safely continues or skips seeding only when the verified count > 0.
```

</details>

</blockquote></details>
<details>
<summary>README.md-103-103 (1)</summary><blockquote>

`103-103`: _⚠️ Potential issue_ | _🟡 Minor_

**Incorrect Capacitor version in documentation.**

The README states "Capacitor 6" but the project uses Capacitor 8.x (per `@capacitor/core` version 8.3.1 in the library context).


<details>
<summary>📝 Proposed fix</summary>

```diff
 ### Mobile Stack
-- **Bridge**: Capacitor 6
+- **Bridge**: Capacitor 8
 - **Local DB**: SQLite (via `@capacitor-community/sqlite`)
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 103, The README entry showing "Bridge: Capacitor 6" is
incorrect; update the documentation to reflect the actual Capacitor version used
by the project (per `@capacitor/core` 8.3.1). Locate the "Bridge" line in
README.md (the "Bridge: Capacitor 6" text) and replace it with "Bridge:
Capacitor 8" or "Bridge: Capacitor 8.x" to match the library's versioning,
ensuring the README accurately reflects the project's dependency.
```

</details>

</blockquote></details>
<details>
<summary>db/offline-db.ts-45-53 (1)</summary><blockquote>

`45-53`: _⚠️ Potential issue_ | _🟡 Minor_

**`order` is a SQL reserved keyword.**

The column name `order` (lines 45, 53, 62) is a reserved keyword in SQL. While SQLite may allow it unquoted in `CREATE TABLE`, queries against these columns may fail or behave unexpectedly. The seed script (line 29, 34, 42) correctly quotes it as `"order"`, but this should be consistent everywhere.


<details>
<summary>💡 Recommendation</summary>

Either consistently quote `"order"` in the schema definition, or rename the column to a non-reserved name like `sort_order` or `display_order` to avoid potential issues.

```diff
-        order INTEGER NOT NULL,
+        "order" INTEGER NOT NULL,
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@db/offline-db.ts` around lines 45 - 53, The schema uses the reserved word
column name order in the CREATE TABLE statements (e.g., in tables courses,
lessons, activities); rename this column to a non-reserved name like sort_order
(or display_order) across the schema instead of quoting it: update the CREATE
TABLE definitions for courses, lessons, activities to use sort_order INTEGER NOT
NULL, update the seed script entries that currently quote "order" to use
sort_order, and search/replace any SQL queries or code that reference order to
use the new column name (or alternatively consistently quote "order" everywhere
if you prefer quoting).
```

</details>

</blockquote></details>
<details>
<summary>lib/mobile-app.ts-5-31 (1)</summary><blockquote>

`5-31`: _⚠️ Potential issue_ | _🟡 Minor_

**Listeners are never cleaned up.**

The `App` and `Network` listeners added in `setupMobileApp` are never removed. If this function is called multiple times (e.g., during hot reload in development), listeners will accumulate. Consider returning a cleanup function or storing listener handles for later removal.


<details>
<summary>💡 Suggested approach</summary>

```diff
 export async function setupMobileApp() {
+  const listeners: Array<{ remove: () => Promise<void> }> = [];
+
   try {
-    await App.addListener('appUrlOpen', (data) => {
+    listeners.push(await App.addListener('appUrlOpen', (data) => {
       console.log('App opened with URL:', data.url);
-    });
+    }));
 
-    await App.addListener('appStateChange', (state) => {
+    listeners.push(await App.addListener('appStateChange', (state) => {
       // ...
-    });
+    }));
 
-    await Network.addListener('networkStatusChange', (status) => {
+    listeners.push(await Network.addListener('networkStatusChange', (status) => {
       console.log('Network status changed:', status);
-    });
+    }));
 
     console.log('Mobile app setup completed successfully');
+    
+    return () => {
+      listeners.forEach(l => l.remove());
+    };
   } catch (error) {
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@lib/mobile-app.ts` around lines 5 - 31, setupMobileApp currently registers
App.addListener and Network.addListener but never removes them; capture the
returned listener handles (e.g., appUrlHandle, appStateHandle, networkHandle)
when calling App.addListener and Network.addListener inside setupMobileApp and
return a cleanup function that calls .remove() on each handle (guarding if a
handle is undefined). Ensure the function signature of setupMobileApp returns
the cleanup callback (or an object with a dispose method) so callers can call it
to unregister the listeners.
```

</details>

</blockquote></details>
<details>
<summary>MOBILE_ARCHITECTURE.md-9-26 (1)</summary><blockquote>

`9-26`: _⚠️ Potential issue_ | _🟡 Minor_

**Add languages to fenced code blocks (markdownlint MD040).**

Line 9 and Line 30 use unlabeled fenced blocks, which will keep lint warnings active.


<details>
<summary>🛠️ Proposed fix</summary>

```diff
-```
+```text
 ┌─────────────────────────────────────┐
 │         Next.js 14 App              │
 │  (React Server + Client Components) │
 └──────────────┬──────────────────────┘
@@
 └─────────────────────────────────────┘
-```
+```

-```
+```text
 nextjs-duolingo-clone/
 ├── capacitor.config.ts          # Capacitor 配置文件
 ├── db/
 │   ├── offline-db.ts            # SQLite 数据库核心服务
@@
 └── components/
     └── network-status.tsx       # 网络状态提示组件
-```
+```
```
</details>


Also applies to: 30-47

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @MOBILE_ARCHITECTURE.md around lines 9 - 26, The markdown has unlabeled
fenced code blocks causing markdownlint MD040 warnings; update each unlabeled
triple-backtick block that contains the ASCII diagram (the box drawing starting
with "┌─────────────────────────────────────┐" and the Capacitor/Next.js
diagram) and the project tree block (starting with "nextjs-duolingo-clone/") to
include a language label (use "text") after the opening ``` so they become

and the file-tree block) to silence MD040.
🧹 Nitpick comments (15)
next.config.mjs (1)

30-30: Consider adding a trailing newline.

The file no longer ends with a newline character. Most coding standards (POSIX, style guides) and linters expect files to end with a newline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.mjs` at line 30, Add a trailing newline at the end of the file
containing the export statement so the file ends with a newline character; open
the file that contains "export default nextConfig;" and ensure there is a final
newline after that line (save the file so the EOF ends with a newline).
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (1)

3-4: Use one launcher asset strategy to avoid duplicated/unused icon resources.

This adaptive icon points to @color + @mipmap, while this PR also adds android/app/src/main/res/drawable/ic_launcher_background.xml and android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml. Right now those drawable assets are effectively sidelined, which makes icon maintenance error-prone.

♻️ Suggested cleanup
-    <background android:drawable="@color/ic_launcher_background"/>
-    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+    <background android:drawable="@drawable/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>

If you keep the mipmap PNG path instead, consider removing the new drawable/vector files to avoid drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml` around lines 3 -
4, The adaptive launcher XML currently mixes mipmap and drawable assets
(ic_launcher.xml uses background
android:drawable="@color/ic_launcher_background" and foreground
android:drawable="@mipmap/ic_launcher_foreground") while you also added
drawable/ic_launcher_background.xml and drawable-v24/ic_launcher_foreground.xml,
causing duplicated/unused assets; choose one strategy: either (A) switch
ic_launcher.xml to reference the drawable assets
(foreground="@drawable/ic_launcher_foreground" and
background="@drawable/ic_launcher_background") so the new vector/drawable files
are used, or (B) keep the mipmap PNG path and remove the new drawable files
(drawable/ic_launcher_background.xml and
drawable-v24/ic_launcher_foreground.xml) to avoid drift—apply the change by
updating ic_launcher.xml references or deleting the unused drawable files
accordingly.
android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java (1)

1-1: Align test package namespace with the app namespace.

Line 1 still uses com.getcapacitor.myapp while the app uses com.lingo.app. Consider renaming this test package (and folder path) to reduce scaffold drift/confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java` at
line 1, The test class ExampleUnitTest.java currently declares the package as
"com.getcapacitor.myapp"; update the package declaration to match the app
namespace "com.lingo.app" and move/rename the test file's directory to mirror
the package path (e.g., change folder structure from com/getcapacitor/myapp to
com/lingo/app) so the package name and file path align with the application
namespace.
app/manifest.ts (1)

12-21: Replace SVG maskable icons with PNG files.

The current manifest uses SVG for maskable icons, which lacks proper support for masking and safe zone application across platforms. Use dedicated PNG files (192x192 and 512x512) with purpose: 'maskable' instead to ensure consistent PWA install UI on Android and other platforms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/manifest.ts` around lines 12 - 21, Update the manifest's icon entries for
sizes '192x192' and '512x512' in the exported manifest object: replace the SVG
src values with PNG variants (e.g. '/mascot-192.png' and '/mascot-512.png'), and
change purpose from 'any maskable' to just 'maskable' so each entry reads
purpose: 'maskable'; ensure type reflects PNG (e.g. 'image/png') to guarantee
proper maskable support across platforms.
package.json (2)

29-29: Move @capacitor/cli to devDependencies.

@capacitor/cli is a build-time tool and should be in devDependencies, not dependencies. This reduces the production bundle size.

 "dependencies": {
   "@capacitor-community/sqlite": "^8.1.0",
   "@capacitor/android": "^8.3.1",
   "@capacitor/app": "^8.1.0",
-  "@capacitor/cli": "^8.3.1",
   "@capacitor/core": "^8.3.1",
 "devDependencies": {
+  "@capacitor/cli": "^8.3.1",
   "@types/node": "^20",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 29, The package.json currently lists "@capacitor/cli":
"^8.3.1" under dependencies; move that entry into devDependencies instead by
removing the "@capacitor/cli" line from dependencies and adding the same
"@capacitor/cli": "^8.3.1" entry under devDependencies, then reinstall/update
the lockfile (npm install or yarn install) to persist changes; reference the
"@capacitor/cli" dependency in package.json when making this change.

44-44: Move @types/sql.js to devDependencies.

Type definition packages are only needed at compile time and should be in devDependencies.

 "dependencies": {
   ...
-  "@types/sql.js": "^1.4.11",
   ...
 },
 "devDependencies": {
+  "@types/sql.js": "^1.4.11",
   "@types/node": "^20",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 44, The package "@types/sql.js" is a type-only
dependency and should be moved from dependencies to devDependencies in
package.json; edit package.json to remove the "@types/sql.js" entry from the
top-level "dependencies" object and add the same entry (preserving the version
"^1.4.11") under "devDependencies" so type definitions are installed for
development/compilation only.
android/variables.gradle (1)

15-15: Remove unused cordovaAndroidVersion variable.

This variable is defined in android/variables.gradle but is not referenced anywhere in the build configuration. Since this is a Capacitor project (indicated by capacitor.config.ts), the unused Cordova-related variable should be removed to reduce confusion and keep the configuration clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/variables.gradle` at line 15, Remove the unused Cordova variable by
deleting the cordovaAndroidVersion declaration (the symbol
cordovaAndroidVersion) from android/variables.gradle and ensure there are no
remaining references to cordovaAndroidVersion elsewhere; if any references
exist, either remove them or replace them with the appropriate Capacitor/Android
configuration value to keep the build config consistent and avoid confusion.
android/app/build.gradle (1)

47-54: Exception handling for missing google-services.json relies on file read.

The check servicesJSON.text will throw an exception if the file doesn't exist, which is caught and logged at info level. This works but may be clearer with an explicit existence check.

💡 Alternative approach
 try {
     def servicesJSON = file('google-services.json')
-    if (servicesJSON.text) {
+    if (servicesJSON.exists()) {
         apply plugin: 'com.google.gms.google-services'
     }
 } catch(Exception e) {
-    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+    logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
 }

Using warn instead of info makes the message more visible during builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/build.gradle` around lines 47 - 54, The current try/catch uses
servicesJSON.text which throws if the file is missing; change the logic to
explicitly check file existence using the servicesJSON.exists() (or new
File('google-services.json').exists()) before reading/applying the plugin
(symbols: servicesJSON, apply plugin: 'com.google.gms.google-services'), and
replace logger.info with logger.warn so missing google-services.json is more
visible during builds; remove the try/catch around the file read once the
existence check is in place.
README.md (1)

111-134: Add language specifier to fenced code block.

The project structure code block lacks a language identifier. Use text or plaintext for directory trees.

📝 Proposed fix
-```
+```text
 ├── app/                    # Next.js App Router
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 111 - 134, The README's project structure fenced code
block (the block starting with the directory tree line containing "├── app/") is
missing a language specifier; update the opening fence to include a plaintext
specifier (e.g., change ``` to ```text or ```plaintext) so the tree renders
correctly as plain text in the README's code block.
db/offline-db.ts (1)

4-11: Consider adding proper typing for the database instance.

The db property is typed as any, which loses type safety benefits. The @capacitor-community/sqlite package provides types that could be used here.

💡 Suggestion
+import { SQLiteConnection, CapacitorSQLite, JsonSQLite, SQLiteDBConnection } from '@capacitor-community/sqlite';
 
 class DatabaseService {
   private sqlite: SQLiteConnection;
-  private db: any = null;
+  private db: SQLiteDBConnection | null = null;
   private isInitialized = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-db.ts` around lines 4 - 11, The db field is currently typed as any
on class DatabaseService which loses type safety; import and use the appropriate
connection/type provided by `@capacitor-community/sqlite` (e.g.,
SQLiteDBConnection or the package's DB type) and change the declaration from
"private db: any = null" to a typed nullable union like "private db:
SQLiteDBConnection | null = null"; update any methods that access db
(connect/open/close/execute) to account for the nullable type (add null checks
or non-null assertions where safe) and ensure SQLiteConnection and
CapacitorSQLite usages remain unchanged.
lib/mobile-app.ts (2)

14-18: Inconsistent async pattern: Use await instead of .then().

The code mixes await (line 7, 11, 22) with .then() (line 14). For consistency and better error handling, prefer async/await throughout.

🧹 Proposed fix
     await App.addListener('appStateChange', (state) => {
       console.log('App state changed:', state.isActive);
       if (state.isActive) {
-        Network.getStatus().then((status) => {
-          if (status.connected) {
-            console.log('App came to foreground with network, can sync');
-          }
-        });
+        const status = await Network.getStatus();
+        if (status.connected) {
+          console.log('App came to foreground with network, can sync');
+        }
       }
     });

Note: The callback would need to be marked as async for this to work:

-    await App.addListener('appStateChange', (state) => {
+    await App.addListener('appStateChange', async (state) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mobile-app.ts` around lines 14 - 18, Replace the .then() usage of
Network.getStatus() with async/await for consistency: mark the surrounding
callback/handler as async, call const status = await Network.getStatus(), then
check status.connected and run the sync logic; update error handling to use
try/catch around the await to mirror the other await usages in this file
(reference: Network.getStatus and status.connected).

13-19: Foreground detection logs but doesn't trigger sync.

The comment on line 16 says "can sync" but no sync is actually triggered when the app comes to foreground with network. Consider integrating with the sync engine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mobile-app.ts` around lines 13 - 19, The foreground/network check in the
state.isActive block only logs "can sync" but doesn't call the sync engine;
update the handler inside Network.getStatus().then(...) so that when
status.connected is true it invokes the app's sync entry (e.g.,
SyncEngine.triggerSync or SyncManager.syncNow) and awaits or handles its promise
and errors, replacing or augmenting the console.log; reference the
state.isActive check, Network.getStatus(), and the app sync API
(SyncEngine.triggerSync/SyncManager.syncNow) to locate where to add the call.
providers/offline-provider.tsx (1)

1-55: Architectural concern: Duplicate network monitoring.

Both OfflineProvider and useSyncEngine (from hooks/use-sync-engine.ts) independently monitor network status via Network.addListener. This creates redundant listeners and potential state inconsistencies.

Consider consolidating network monitoring into the OfflineProvider and exposing a callback or event for sync triggering, or have useSyncEngine consume the isOnline state from useOffline() instead of maintaining its own listener.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@providers/offline-provider.tsx` around lines 1 - 55, OfflineProvider
currently creates Network.addListener and maintains isOnline; remove duplicate
network monitoring from useSyncEngine and have it consume the shared state from
useOffline() (or accept a callback from OfflineProvider) instead: keep
Network.addListener and periodic checks only in OfflineProvider (symbol:
OfflineProvider, useOffline, Network.addListener) and update useSyncEngine
(symbol: useSyncEngine) to read isOnline from useOffline() or receive an
onOnline/onStatusChange callback from OfflineProvider so sync triggering is
centralized and duplicate listeners are eliminated.
hooks/use-sync-engine.ts (1)

3-3: Unused import: dbService.

The dbService import is not used in this file.

🧹 Proposed fix
-import { dbService } from '@/db/offline-db';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-sync-engine.ts` at line 3, Remove the unused import dbService from
the top of hooks/use-sync-engine.ts; locate the import line "import { dbService
} from '@/db/offline-db';" and delete it (or remove dbService from the named
imports) so the module no longer imports an unused symbol.
components/network-status.tsx (1)

4-4: Remove unused import RefreshCw.

The RefreshCw icon is imported but never used in this component.

🧹 Proposed fix
-import { WifiOff, Wifi, RefreshCw } from 'lucide-react';
+import { WifiOff, Wifi } from 'lucide-react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/network-status.tsx` at line 4, Remove the unused icon import by
deleting RefreshCw from the import statement that currently reads "import {
WifiOff, Wifi, RefreshCw } from 'lucide-react';" in the network status
component; leave only the used symbols (WifiOff, Wifi) so the import becomes
"import { WifiOff, Wifi } from 'lucide-react';".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d47678be-a0ee-460d-95b5-34898174022f

📥 Commits

Reviewing files that changed from the base of the PR and between 91f488c and c7b72b1.

⛔ Files ignored due to path filters (28)
  • android/app/src/main/res/drawable-land-hdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-land-mdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-land-xhdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-land-xxhdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-land-xxxhdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-port-hdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-port-mdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-port-xhdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-port-xxhdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable-port-xxxhdpi/splash.png is excluded by !**/*.png
  • android/app/src/main/res/drawable/splash.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (44)
  • .dockerignore
  • Dockerfile
  • MOBILE_ARCHITECTURE.md
  • README.md
  • TEST_REPORT.md
  • android/.gitignore
  • android/app/.gitignore
  • android/app/build.gradle
  • android/app/proguard-rules.pro
  • android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/com/lingo/app/MainActivity.java
  • android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  • android/app/src/main/res/drawable/ic_launcher_background.xml
  • android/app/src/main/res/layout/activity_main.xml
  • android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • android/app/src/main/res/values/ic_launcher_background.xml
  • android/app/src/main/res/values/strings.xml
  • android/app/src/main/res/values/styles.xml
  • android/app/src/main/res/xml/file_paths.xml
  • android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  • android/build.gradle
  • android/gradle.properties
  • android/gradle/wrapper/gradle-wrapper.properties
  • android/gradlew
  • android/gradlew.bat
  • android/settings.gradle
  • android/variables.gradle
  • app/globals.css
  • app/layout.tsx
  • app/manifest.ts
  • capacitor.config.ts
  • components/network-status.tsx
  • db/offline-db.ts
  • db/offline-queries.ts
  • hooks/use-sync-engine.ts
  • lib/mobile-app.ts
  • next.config.mjs
  • package.json
  • providers/database-provider.tsx
  • providers/offline-provider.tsx
  • scripts/seed-offline.ts
  • vercel.json

// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

assertEquals("com.getcapacitor.app", appContext.getPackageName());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Test asserts wrong package name - will always fail.

The test expects "com.getcapacitor.app" but the actual application ID is "com.lingo.app" as defined in android/app/build.gradle.

Proposed fix
-        assertEquals("com.getcapacitor.app", appContext.getPackageName());
+        assertEquals("com.lingo.app", appContext.getPackageName());
📝 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
assertEquals("com.getcapacitor.app", appContext.getPackageName());
assertEquals("com.lingo.app", appContext.getPackageName());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`
at line 24, The test ExampleInstrumentedTest uses assertEquals with the wrong
expected package string; update the expected value in the assertion (the
assertEquals call in ExampleInstrumentedTest) from "com.getcapacitor.app" to the
actual applicationId "com.lingo.app" so the test compares against the real
package name defined in build.gradle.

Comment thread db/offline-queries.ts
Comment on lines +15 to +38
getUnits: async (courseId: number) => {
const units = await dbService.executeSelectQuery(
`SELECT * FROM units WHERE course_id = ? ORDER BY "order"`,
[courseId]
);

const unitsWithLessons = await Promise.all(
(units.values || []).map(async (unit: any) => {
const lessons = await dbService.executeSelectQuery(
`SELECT * FROM lessons WHERE unit_id = ? ORDER BY "order"`,
[unit.id]
);

const lessonsWithChallenges = await Promise.all(
(lessons.values || []).map(async (lesson: any) => {
const challenges = await dbService.executeSelectQuery(
`SELECT c.*,
(SELECT COUNT(*) > 0 AND SUM(CASE WHEN cp.completed = 1 THEN 1 ELSE 0 END) = COUNT(*)
FROM challenge_progress cp
WHERE cp.challenge_id = c.id AND cp.user_id = ?) as completed
FROM challenges c
WHERE c.lesson_id = ?
ORDER BY c."order"`,
[userId, lesson.id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

getUnits is bound to a hardcoded user instead of the caller’s user.

Line 38 uses userId, but this function doesn’t accept it; Line 147 hardcodes 'current_user'. This will compute completion state for the wrong user.

🛠️ Proposed fix
-  getUnits: async (courseId: number) => {
+  getUnits: async (courseId: number, userId: string) => {
@@
-              [userId, lesson.id]
+              [userId, lesson.id]
             );
@@
-const userId = 'current_user';

Also applies to: 147-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-queries.ts` around lines 15 - 38, getUnits is computing per-user
completion using an undefined/hardcoded userId; update getUnits to accept a
userId parameter and pass that userId into the nested DB queries (the challenges
query that uses userId and any other queries relying on completion), and replace
the hardcoded 'current_user' occurrence with the passed userId variable; also
update all callers of getUnits to supply the correct userId so completion is
computed for the caller's user (refer to function name getUnits and the
challenges query that currently references userId and the hardcoded
'current_user').

Comment thread db/offline-queries.ts
Comment on lines +61 to +69
`SELECT c.*, co.* as options,
(SELECT COUNT(*) > 0 AND SUM(CASE WHEN cp.completed = 1 THEN 1 ELSE 0 END) = COUNT(*)
FROM challenge_progress cp
WHERE cp.challenge_id = c.id AND cp.user_id = ?) as completed
FROM challenges c
LEFT JOIN challenge_options co ON co.challenge_id = c.id
WHERE c.lesson_id = ?
ORDER BY c."order"`,
[userId, lessonId]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import sqlite3
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.executescript("""
CREATE TABLE challenges(id INTEGER, lesson_id INTEGER, "order" INTEGER);
CREATE TABLE challenge_options(id INTEGER, challenge_id INTEGER);
CREATE TABLE challenge_progress(challenge_id INTEGER, user_id TEXT, completed INTEGER);
""")
sql = """
SELECT c.*, co.* as options,
 (SELECT COUNT(*) > 0 AND SUM(CASE WHEN cp.completed = 1 THEN 1 ELSE 0 END) = COUNT(*)
  FROM challenge_progress cp
  WHERE cp.challenge_id = c.id AND cp.user_id = ?) as completed
FROM challenges c
LEFT JOIN challenge_options co ON co.challenge_id = c.id
WHERE c.lesson_id = ?
ORDER BY c."order"
"""
try:
    cur.execute(sql, ("u1", 1))
    print("query_ok")
except Exception as e:
    print("query_error:", e)
PY

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 120


Fix invalid SQL projection: co.* as options is not valid SQLite syntax.

The query at lines 61-69 uses co.* as options, which is not supported in SQLite and will fail at runtime with a syntax error. Wildcard projections cannot be aliased in SQLite. Consider selecting specific columns explicitly instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/offline-queries.ts` around lines 61 - 69, The SQL projection uses the
invalid SQLite expression "co.* as options" (in the SQL string with parameters
[userId, lessonId]); replace that wildcard-alias with either an explicit list of
challenge_options columns (e.g., co.id AS option_id, co.name AS option_name,
...) or construct a single JSON column via SQLite json_object(...) listing the
option fields and alias it as options; update any code that deserializes the
result to match the chosen column names/JSON shape so the callers that expect an
"options" value continue to work.

Comment thread Dockerfile
@@ -0,0 +1,9 @@
FROM node:18-alpine

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Current Node.js release schedule and end-of-life date for Node 18, plus recommended active LTS version for production Docker images

💡 Result:

Current Node.js release schedule (as of 2026-04-29): - Node.js 25.x (Current): Initial release Oct 15, 2025; Maintenance starts Apr 1, 2026; EOL Jun 1, 2026. - Node.js 24.x (Active LTS, Krypton): Initial release May 6, 2025; Active LTS start Oct 28, 2025; Maintenance starts Oct 20, 2026; EOL Apr 30, 2028. - Node.js 22.x (Maintenance LTS, Jod): Initial release Apr 24, 2024; Active LTS start Oct 29, 2024; Maintenance starts Oct 21, 2025; EOL Apr 30, 2027. - Node.js 20.x (Maintenance LTS, Iron): Initial release Apr 18, 2023; Active LTS start Oct 24, 2023; Maintenance starts Oct 22, 2024; EOL Apr 30, 2026. Starting with Node.js 27.x (Apr 2027), the schedule evolves to one major release per year in April, with LTS promotion in October, and every release becoming LTS (no odd/even distinction). Node.js 18 (Hydrogen) end-of-life: April 30, 2025. It is now EOL and receives no updates or security patches. Recommended active LTS version for production Docker images: Node.js 24.x (Active LTS). Official Node.js Docker images support versions under active support per the release schedule. Use tags like node:24-bookworm-slim for production (slim variants recommended for smaller size). Production applications should use Active LTS or Maintenance LTS releases; avoid Current or EOL versions.

Citations:


Node.js 18 is end-of-life; upgrade to an active LTS version immediately.

Line 1 pins node:18-alpine, which reached end-of-life on April 30, 2025 and no longer receives security patches. Use node:22-alpine (Maintenance LTS) or node:24-alpine (Active LTS) instead, and update package.json engines to match.

🧰 Tools
🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 1, Update the base image in the Dockerfile by replacing
the FROM node:18-alpine reference with a supported LTS tag (e.g., FROM
node:24-alpine or FROM node:22-alpine) and ensure package.json's "engines" field
matches the chosen Node major version; also run your dependency install/build
locally in the updated image to verify there are no breaking changes from the
Node upgrade and adjust any Node-specific scripts if they fail.

Comment thread hooks/use-sync-engine.ts
Comment on lines +11 to +25
useEffect(() => {
const initNetworkListener = async () => {
const status = await Network.getStatus();
setIsOnline(status.connected);

Network.addListener('networkStatusChange', status => {
setIsOnline(status.connected);
if (status.connected) {
syncPendingChanges();
}
});
};

initNetworkListener();
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Network listener is never removed, causing memory leak.

The Network.addListener call returns a PluginListenerHandle that must be removed on cleanup. Without cleanup, multiple listeners accumulate on component remount, and syncPendingChanges will be called multiple times per network change.

🔧 Proposed fix
   useEffect(() => {
+    let listenerHandle: { remove: () => Promise<void> } | null = null;
+
     const initNetworkListener = async () => {
       const status = await Network.getStatus();
       setIsOnline(status.connected);
 
-      Network.addListener('networkStatusChange', status => {
+      listenerHandle = await Network.addListener('networkStatusChange', status => {
         setIsOnline(status.connected);
         if (status.connected) {
           syncPendingChanges();
         }
       });
     };
 
     initNetworkListener();
+
+    return () => {
+      listenerHandle?.remove();
+    };
   }, []);
📝 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
useEffect(() => {
const initNetworkListener = async () => {
const status = await Network.getStatus();
setIsOnline(status.connected);
Network.addListener('networkStatusChange', status => {
setIsOnline(status.connected);
if (status.connected) {
syncPendingChanges();
}
});
};
initNetworkListener();
}, []);
useEffect(() => {
let listenerHandle: { remove: () => Promise<void> } | null = null;
const initNetworkListener = async () => {
const status = await Network.getStatus();
setIsOnline(status.connected);
listenerHandle = await Network.addListener('networkStatusChange', status => {
setIsOnline(status.connected);
if (status.connected) {
syncPendingChanges();
}
});
};
initNetworkListener();
return () => {
listenerHandle?.remove();
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-sync-engine.ts` around lines 11 - 25, The Network.addListener
created in initNetworkListener is never removed, causing leaked listeners;
capture its returned PluginListenerHandle (from Network.addListener) in a
variable inside useEffect (or initNetworkListener), then return a cleanup
function from useEffect that calls handle.remove() to unregister the listener;
ensure you still call setIsOnline and syncPendingChanges inside the listener and
that the cleanup runs on unmount/re-mount to avoid duplicate calls to
syncPendingChanges.

Comment thread hooks/use-sync-engine.ts
Comment on lines +64 to +84
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/challenge-progress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error('Failed to sync challenge progress');
}
};

const syncUserProgress = async (data: any) => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/user-progress`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error('Failed to sync user progress');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the API routes exist
fd -t f -p 'api/challenge-progress' 
fd -t f -p 'api/user-progress'
# Also search for route handlers
rg -l "challenge-progress" --type ts -g '*/api/*'
rg -l "user-progress" --type ts -g '*/api/*'

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 381


🏁 Script executed:

# First, let's see the overall project structure
ls -la

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 2418


🏁 Script executed:

# Look for API routes - check common Next.js locations
find . -type d -name "api" | head -20

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 414


🏁 Script executed:

# Search for challenge-progress and user-progress in the entire codebase without filters
rg "challenge-progress" --type-list | head -20

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 473


🏁 Script executed:

# Simpler approach - just search for these strings anywhere
rg "challenge-progress"
rg "user-progress"

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 1006


🏁 Script executed:

# List contents of app/api directory
find ./app/api -type f -name "*.ts" -o -name "*.js" | head -20

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 473


🏁 Script executed:

# Show the full directory structure of app/api
tree -L 3 ./app/api

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 533


🏁 Script executed:

# Check the full use-sync-engine.ts file
cat -n ./hooks/use-sync-engine.ts

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 3438


🏁 Script executed:

# Check if there are server actions for challenge-progress and user-progress
ls -la ./actions/

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 407


🏁 Script executed:

# Check the content of the actions
cat -n ./actions/challenge-progress.ts
cat -n ./actions/user-progress.ts

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 7412


Create missing API endpoints or refactor to use server actions.

The sync functions in use-sync-engine.ts make fetch requests to /api/challenge-progress and /api/user-progress endpoints (lines 64 and 76), but these endpoints do not exist in the codebase. When offline changes need to sync, these calls will fail with 404 errors. Either create the missing API route handlers in app/api/challenge-progress/route.ts and app/api/user-progress/route.ts, or refactor syncChallengeProgress and syncUserProgress to call the existing server actions (upsertChallengeProgress from @/actions/challenge-progress and upsertUserProgress from @/actions/user-progress) instead of making HTTP requests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-sync-engine.ts` around lines 64 - 84, The sync functions in
hooks/use-sync-engine.ts (syncChallengeProgress and syncUserProgress) are
calling non-existent API endpoints; either add route handlers at
app/api/challenge-progress/route.ts and app/api/user-progress/route.ts to accept
POST/PUT and perform the upsert logic, or refactor these two functions to import
and call the existing server actions upsertChallengeProgress (from
`@/actions/challenge-progress`) and upsertUserProgress (from
`@/actions/user-progress`) directly instead of fetch: replace the fetch +
response.ok check with an await call to the appropriate upsert... function,
propagate errors, and remove the HTTP-specific headers/body serialization so the
client-side hook invokes the server action correctly (ensure proper types and
async/await usage in syncChallengeProgress and syncUserProgress).

Comment thread package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"build:static": "next build && next export",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Next.js 14 static export configuration output export

💡 Result:

In Next.js 14, static export is configured by setting output: 'export' in next.config.js. This replaces the deprecated next export command, which was removed in v14.0.0. Example configuration: const nextConfig = { output: 'export', // Optional: trailingSlash: true, // Optional: distDir: 'dist', } module.exports = nextConfig; Running next build generates static HTML/CSS/JS assets in the out directory (or custom distDir), suitable for deployment on any static web server like Nginx, GitHub Pages, or S3. Key points: - Supports App Router (React Server Components, Route Handlers with GET only) and Pages Router (getStaticProps/getStaticPaths). - Use custom image loader for next/image. - Only static patterns: no dynamic server features like cookies or headers. Official docs confirm this for both App and Pages Routers as of 2026.

Citations:


🏁 Script executed:

# Check if next.config.js exists and its contents
if [ -f "next.config.js" ]; then
  echo "=== next.config.js exists ==="
  cat -n next.config.js
elif [ -f "next.config.mjs" ]; then
  echo "=== next.config.mjs exists ==="
  cat -n next.config.mjs
elif [ -f "next.config.ts" ]; then
  echo "=== next.config.ts exists ==="
  cat -n next.config.ts
else
  echo "No next.config file found"
fi

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 981


next export command will fail in Next.js 14.1.1 — it was removed in v14.0.0.

The build:static script uses a deprecated command that no longer exists. Configure static export in next.config.mjs instead:

Required fix

In next.config.mjs, add the output property:

const nextConfig = {
  output: 'export',
  async headers() {
    // ... existing config
  },
};

Then update the script:

-"build:static": "next build && next export",
+"build:static": "next build",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 11, The package.json "build:static" script uses the
removed "next export" command—remove "next export" from the "build:static" npm
script (leave it as "next build" or an equivalent) and instead add output:
'export' to your Next.js configuration in next.config.mjs (keep any existing
async headers() and other config intact); update the package.json "build:static"
entry and the next.config.mjs export/default object (refer to the "build:static"
script and the next.config.mjs output property) so static export is configured
via Next config rather than the removed CLI command.

Comment on lines +28 to +48
useEffect(() => {
const checkNetwork = async () => {
const status = await Network.getStatus();
setIsOnline(status.connected);

Network.addListener('networkStatusChange', (status) => {
setIsOnline(status.connected);
});
};

checkNetwork();

const interval = setInterval(async () => {
const status = await Network.getStatus();
setIsOnline(status.connected);
}, 5000);

return () => {
clearInterval(interval);
};
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Network listener is never removed, causing memory leak.

Similar to use-sync-engine.ts, the Network.addListener call on line 33 is never cleaned up. This causes listener accumulation on component remount.

Additionally, the 5-second polling interval is redundant when the event listener is already active—the listener provides real-time updates.

🔧 Proposed fix
   useEffect(() => {
+    let listenerHandle: { remove: () => Promise<void> } | null = null;
+
     const checkNetwork = async () => {
       const status = await Network.getStatus();
       setIsOnline(status.connected);
 
-      Network.addListener('networkStatusChange', (status) => {
+      listenerHandle = await Network.addListener('networkStatusChange', (status) => {
         setIsOnline(status.connected);
       });
     };
 
     checkNetwork();
 
-    const interval = setInterval(async () => {
-      const status = await Network.getStatus();
-      setIsOnline(status.connected);
-    }, 5000);
-
     return () => {
-      clearInterval(interval);
+      listenerHandle?.remove();
     };
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@providers/offline-provider.tsx` around lines 28 - 48, The effect creates a
persistent Network.addListener without cleanup and also uses redundant
setInterval polling; modify the useEffect so checkNetwork registers the listener
and captures the listener subscription returned by Network.addListener (from
checkNetwork or directly in the effect), then remove that listener in the
cleanup function (call subscription.remove() or subscription.removeListener as
appropriate) and remove the 5-second setInterval polling to avoid duplicate
updates; update references to checkNetwork, Network.addListener, setIsOnline,
interval and clearInterval to ensure only the event listener drives online state
and is properly disposed on unmount.

Comment thread scripts/seed-offline.ts
Comment on lines +21 to +66
const seedData = `
INSERT INTO courses (id, title, image_src) VALUES
(1, 'Spanish', '/es.svg'),
(2, 'French', '/fr.svg'),
(3, 'German', '/de.svg'),
(4, 'Japanese', '/jp.svg'),
(5, 'Italian', '/it.svg');

INSERT INTO units (id, title, description, course_id, "order") VALUES
(1, 'Unit 1', 'Learn the basics of Spanish', 1, 1),
(2, 'Unit 2', 'Common phrases and expressions', 1, 2),
(3, 'Unit 3', 'Food and drink vocabulary', 1, 3);

INSERT INTO lessons (id, title, unit_id, "order") VALUES
(1, 'Basics 1', 1, 1),
(2, 'Greetings', 1, 2),
(3, 'Basics 2', 1, 3),
(4, 'Travel phrases', 2, 1),
(5, 'Restaurant', 2, 2),
(6, 'Food items', 3, 1);

INSERT INTO challenges (id, lesson_id, type, question, "order") VALUES
(1, 1, 'SELECT', 'What does "hola" mean?', 1),
(2, 1, 'SELECT', 'Translate "goodbye"', 2),
(3, 1, 'ASSIST', 'hello', 3),
(4, 2, 'SELECT', 'How do you say "good morning"?', 1),
(5, 2, 'SELECT', 'What is "please" in Spanish?', 2);

INSERT INTO challenge_options (id, challenge_id, text, correct, image_src, audio_src) VALUES
(1, 1, 'hello', 1, NULL, NULL),
(2, 1, 'goodbye', 0, NULL, NULL),
(3, 1, 'thank you', 0, NULL, NULL),
(4, 2, 'adiós', 1, NULL, NULL),
(5, 2, 'por favor', 0, NULL, NULL),
(6, 2, 'buenos días', 0, NULL, NULL),
(7, 3, 'hello', 1, NULL, NULL),
(8, 3, 'goodbye', 0, NULL, NULL),
(9, 4, 'buenos días', 1, NULL, NULL),
(10, 4, 'buenas noches', 0, NULL, NULL),
(11, 4, 'adiós', 0, NULL, NULL),
(12, 5, 'por favor', 1, NULL, NULL),
(13, 5, 'gracias', 0, NULL, NULL),
(14, 5, 'de nada', 0, NULL, NULL);
`;

await dbService.executeQuery(seedData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how executeQuery is implemented and if it supports multi-statement
ast-grep --pattern 'executeQuery($_, $_) {
  $$$
}'
# Check if there's transaction support in offline-db
rg -n "transaction|BEGIN|COMMIT|ROLLBACK" --type ts

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 65


🌐 Web query:

Does @capacitor-community/sqlite executeQuery support multiple SQL statements separated by semicolons?

💡 Result:

No, the @capacitor-community/sqlite executeQuery method (likely referring to the 'query' method on SQLiteDBConnection) does not support multiple SQL statements separated by semicolons. The 'query' method is designed for single SELECT statements and takes a single 'statement' parameter of type string with optional bind values. Official docs describe it as "Execute SQLite DB Connection Query" for a single statement. Multiple statements are supported by: - 'execute(statements: string)': Executes a batch of raw SQL statements (DDL like CREATE, DML like INSERT/UPDATE/DELETE, transaction commands). - 'executeSet(set: capSQLiteSet[])': Executes multiple statements as an array of {statement, values}. Evidence from docs and issues shows 'query' expects one statement; attempting multiple in 'query' is not documented and would fail like other single-statement methods. Issues (#393, #386, #521) discuss multi-statement support/bugs only in 'execute'/'executeSet', confirming 'query' is single-statement. Newer forks like capawesome sqlite explicitly state 'query' for SELECT statements.

Citations:


🏁 Script executed:

# Find the db service definition
fd -t f -i "db" . | grep -E "\.(ts|js)$" | head -20

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 102


🏁 Script executed:

# Search for dbService definition and executeQuery implementation
rg -n "dbService|executeQuery" --type ts | head -40

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 2106


🏁 Script executed:

# Look for the offline-db or database service files
fd -t f -i "offline" . | grep -E "\.(ts|js)$"

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 152


🏁 Script executed:

# Read the offline-db.ts file to understand executeQuery implementation
cat -n db/offline-db.ts

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 6584


🌐 Web query:

@capacitor-community/sqlite run() method single statement or multiple statements documentation

💡 Result:

The run method in @capacitor-community/sqlite is designed for executing a single statement with optional bind values, particularly for Data Manipulation Language (DML) commands like INSERT, UPDATE, DELETE, REPLACE when using bind values. Key evidence from official documentation: - API.md explicitly states: "Execute a Single Statement" for run(options: capSQLiteRunOptions), where options include a singular "statement" string and "values" array for that one statement. - Recommended usage: DML commands with bind values use run; without bind values, either execute or run can be used. - DBConnection API shows run(statement: string, values?: any[]) → Promise, taking one statement. For multiple statements: - Use executeSet(capSQLiteSet[]) for a set of statements, each with their own statement and values array (e.g., multiple INSERTs). - Or execute(statements: string) for a batch of raw SQL statements as a single string (may have platform issues with multi-statement parsing, as noted in issues). Examples: Single statement with run: await db.run('INSERT INTO table (col) VALUES (?)', [value]); Multiple via executeSet: await db.executeSet([ { statement: 'INSERT INTO table (col) VALUES (?)', values: [val1] }, { statement: 'INSERT INTO table (col) VALUES (?)', values: [val2] } ]); Do not pass multiple value sets to run; it expects one statement and one values array, as misuse leads to errors (e.g., issue #264 confirms run is "for single datasets only").

Citations:


🌐 Web query:

@capacitor-community/sqlite execute() batch multiple statements transaction support BEGIN COMMIT

💡 Result:

The @capacitor-community/sqlite plugin supports executing batch multiple statements with transaction control through several methods on the SQLiteDBConnection interface. Key methods: 1. execute(statements: string, transaction?: boolean, isSQL92?: boolean): Executes a batch of raw SQL statements as a single string (semicolon-separated). The optional 'transaction' parameter (defaults to true) wraps the statements in BEGIN TRANSACTION / COMMIT automatically if true. Set to false to disable implicit transaction or when using explicit transaction control. 2. executeSet(set: capSQLiteExecuteStatement[]): Executes an array of statements, each potentially with values. Also supports a transaction parameter (defaults to true) that wraps the entire batch in a transaction. 3. executeTransaction(txn: capTask[], isSQL92: boolean): Dedicated method (since 3.4.0) for executing a batch of tasks/statements within a transaction. 4. Explicit transaction methods (since 5.0.7): beginTransaction, commitTransaction, rollbackTransaction, isTransactionActive. Use these for manual control: - await db.beginTransaction; - await db.execute("multi; statements; here", false); // pass false to avoid nested transaction - await db.commitTransaction; For batch operations spanning multiple execute calls, use explicit transactions and set transaction: false on execute calls within the transaction block to avoid conflicts. The plugin opens/closes the DB per command but maintains transaction state across calls when using explicit methods. Examples: // Implicit transaction with execute batch await db.execute("CREATE TABLE test(id); INSERT INTO test VALUES(1); INSERT INTO test VALUES(2);"); // Explicit multi-call transaction await db.beginTransaction; try { await db.execute("INSERT INTO test VALUES(3);", false); await db.execute("INSERT INTO test VALUES(4);", false); await db.commitTransaction; } catch(e) { await db.rollbackTransaction; throw e; } // executeSet batch await db.executeSet([ {statement: 'INSERT INTO test VALUES(?)', values: [5]}, {statement: 'INSERT INTO test VALUES(?)', values: [6]} ]);

Citations:


Use execute() instead of run() for batch SQL statements; run() only supports single statements.

The executeQuery() method uses this.db.run(), which only accepts a single SQL statement. Passing multiple INSERT statements (14+ in the seed data) will fail because run(statement: string, values?: any[]) expects a single statement, not semicolon-separated batch operations.

Change dbService.executeQuery(seedData) to use this.db.execute(seedData) instead, which supports batch statements. As a bonus, execute() wraps operations in a transaction by default (transaction: true), preventing partial failures. The codebase already demonstrates this pattern correctly in createTables() at line 124.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/seed-offline.ts` around lines 21 - 66, The seed script calls
dbService.executeQuery(seedData) but executeQuery uses this.db.run which only
accepts a single SQL statement; replace that call to use the batch-capable API
by invoking the method that uses this.db.execute (e.g., call
dbService.execute(seedData) or change executeQuery to delegate to execute),
ensuring the multi-statement seed string is executed inside a transaction (same
pattern used by createTables()) and avoid using run for semicolon-separated
statements.

Comment thread vercel.json
Comment on lines +6 to +14
"env": {
"DATABASE_URL": "<NEON_DATABASE_URL>",
"NEXT_PUBLIC_CLERK_FRONTEND_API": "<your-clerk-frontend-api>",
"CLERK_API_KEY": "<your-clerk-api-key>",
"NEXT_PUBLIC_CLERK_SIGN_IN_URL": "/sign-in",
"NEXT_PUBLIC_CLERK_SIGN_UP_URL": "/sign-up",
"NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL": "/learn",
"NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL": "/learn"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Env keys currently in vercel.json:"
jq -r '.env | keys[]' vercel.json

echo
echo "Clerk env variable names referenced across the repo:"
rg -n --glob '!**/node_modules/**' 'NEXT_PUBLIC_CLERK_(PUBLISHABLE_KEY|FRONTEND_API)|CLERK_(SECRET_KEY|API_KEY)'

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 794


🌐 Web query:

What are the current recommended environment variable names for @clerk/nextjs in Next.js, and which older names are deprecated?

💡 Result:

The current recommended environment variables for @clerk/nextjs in Next.js are: Required core keys: - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (client-side publishable key, prefixed pk_test_ or pk_live_) - CLERK_SECRET_KEY (server-side secret key, prefixed sk_test_ or sk_live_; do not expose publicly) Common redirect and path variables (all NEXT_PUBLIC_ prefixed for client exposure): - NEXT_PUBLIC_CLERK_SIGN_IN_URL - NEXT_PUBLIC_CLERK_SIGN_UP_URL - NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL - NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL - NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL - NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL Advanced/configuration: - NEXT_PUBLIC_CLERK_JS_URL - NEXT_PUBLIC_CLERK_JS_VERSION - NEXT_PUBLIC_CLERK_API_URL - NEXT_PUBLIC_CLERK_API_VERSION - NEXT_PUBLIC_CLERK_JWT_KEY - NEXT_PUBLIC_CLERK_FAPI (Frontend API URL) - NEXT_PUBLIC_CLERK_PROXY_URL - CLERK_ENCRYPTION_KEY (Next.js specific, for clerkMiddleware dynamic keys) - NEXT_PUBLIC_CLERK_TELEMETRY_DISABLED (or similar for telemetry) Deprecated older names (still supported in some cases but do not use in new projects; migrate immediately): - NEXT_PUBLIC_CLERK_FRONTEND_API (use NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) - CLERK_API_KEY (use CLERK_SECRET_KEY) - CLERK_JS_VERSION (use NEXT_PUBLIC_CLERK_JS_VERSION) - CLERK_JS (use NEXT_PUBLIC_CLERK_JS_URL) - NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL and NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL (use the _FORCE_REDIRECT_URL and FALLBACK_REDIRECT_URL variants instead) These are sourced from Clerk's official documentation and quickstart examples as of 2026. Client-side vars must be prefixed NEXT_PUBLIC to be bundled by Next.js. For production, use live keys from Clerk Dashboard.

Citations:


🏁 Script executed:

#!/bin/bash

# Search for actual usage of the deprecated Clerk env vars in app code
echo "=== Searching for NEXT_PUBLIC_CLERK_FRONTEND_API usage in code ==="
rg -n 'NEXT_PUBLIC_CLERK_FRONTEND_API' --glob '!**/node_modules/**' --glob '!*.md' --glob '!vercel.json' || echo "(not found)"

echo
echo "=== Searching for CLERK_API_KEY usage in code ==="
rg -n 'CLERK_API_KEY' --glob '!**/node_modules/**' --glob '!*.md' --glob '!vercel.json' || echo "(not found)"

echo
echo "=== Checking for any Clerk middleware or config files ==="
fd -type f \( -name '*clerk*' -o -name 'middleware*' \) --exclude node_modules | head -20

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 473


🏁 Script executed:

#!/bin/bash

# Check for Clerk provider, middleware, or actual auth initialization
echo "=== Searching for ClerkProvider or Clerk initialization ==="
rg -n 'ClerkProvider|useClerk|auth|middleware' --glob '!**/node_modules/**' --type tsx --type ts --type jsx --type js | head -30

echo
echo "=== Checking .env.local or .env files ==="
find . -maxdepth 2 -name '.env*' -type f | grep -v node_modules

echo
echo "=== Looking for process.env usage in app code ==="
rg -n 'process\.env\.NEXT_PUBLIC_CLERK|process\.env\.CLERK' --glob '!**/node_modules/**' --type tsx --type ts | head -20

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 311


🏁 Script executed:

#!/bin/bash

# Fix: search without type constraints
echo "=== Searching for Clerk-related code (any extension) ==="
rg -n 'ClerkProvider|useClerk|auth' --glob '!**/node_modules/**' | head -20

echo
echo "=== Check if .env.example exists ==="
ls -la | grep -E '\.env|vercel'

echo
echo "=== Scan for actual package.json to confirm Clerk is listed ==="
cat package.json | grep -i clerk || echo "(Clerk not in dependencies)"

Repository: code-with-antonio/nextjs-duolingo-clone

Length of output: 1556


Remove the env section from vercel.json and configure environment variables via the Vercel dashboard.

The environment block contains deprecated Clerk variable names (NEXT_PUBLIC_CLERK_FRONTEND_API and CLERK_API_KEY) that are incompatible with the installed version of @clerk/nextjs (^4.29.9). These should be replaced with NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY respectively. However, runtime environment values must never be committed as placeholders in config files. Remove the entire env section and set these variables through the Vercel project dashboard instead. Deploying with these outdated placeholder values will cause Clerk authentication to fail.

Also, the deprecated redirect URL variables (NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL and NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL) should be replaced with NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL and NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL if still needed.

♻️ Proposed change
 {
   "version": 2,
   "builds": [
     { "src": "package.json", "use": "@vercel/next" }
-  ],
-  "env": {
-    "DATABASE_URL": "<NEON_DATABASE_URL>",
-    "NEXT_PUBLIC_CLERK_FRONTEND_API": "<your-clerk-frontend-api>",
-    "CLERK_API_KEY": "<your-clerk-api-key>",
-    "NEXT_PUBLIC_CLERK_SIGN_IN_URL": "/sign-in",
-    "NEXT_PUBLIC_CLERK_SIGN_UP_URL": "/sign-up",
-    "NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL": "/learn",
-    "NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL": "/learn"
-  }
+  ]
 }
📝 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
"env": {
"DATABASE_URL": "<NEON_DATABASE_URL>",
"NEXT_PUBLIC_CLERK_FRONTEND_API": "<your-clerk-frontend-api>",
"CLERK_API_KEY": "<your-clerk-api-key>",
"NEXT_PUBLIC_CLERK_SIGN_IN_URL": "/sign-in",
"NEXT_PUBLIC_CLERK_SIGN_UP_URL": "/sign-up",
"NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL": "/learn",
"NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL": "/learn"
}
{
"version": 2,
"builds": [
{ "src": "package.json", "use": "@vercel/next" }
]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vercel.json` around lines 6 - 14, Remove the entire "env" section from
vercel.json and do not commit runtime secrets or placeholder values; instead set
all Clerk and database variables in the Vercel project dashboard. Replace usages
of deprecated variable names in your app code if present: change
NEXT_PUBLIC_CLERK_FRONTEND_API -> NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and
CLERK_API_KEY -> CLERK_SECRET_KEY, and if you still need redirect variables
change NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL / NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL
-> NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL /
NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL; verify code references to these
symbols and update them accordingly. Ensure DATABASE_URL and other secrets are
only set via the Vercel dashboard and removed from vercel.json.

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