Skip to content

feat(ai): multi-model ensemble engine with dynamic weighting (#10)#170

Open
saidai-bhuvanesh wants to merge 12 commits into
jpdevhub:mainfrom
saidai-bhuvanesh:feat/ensemble-engine
Open

feat(ai): multi-model ensemble engine with dynamic weighting (#10)#170
saidai-bhuvanesh wants to merge 12 commits into
jpdevhub:mainfrom
saidai-bhuvanesh:feat/ensemble-engine

Conversation

@saidai-bhuvanesh

@saidai-bhuvanesh saidai-bhuvanesh commented Jul 3, 2026

Copy link
Copy Markdown

🔗 Upstream Issue Connection

Closes #169

This Pull Request is officially linked to and resolves Issue #169 (Feature 10: Multi-Model AI Ensemble Engine) in the upstream repository.

Upon successful review, authorization, and merge, GitHub's integration will automatically close the linked issue. All development files, localization mappings, and page changes contained in this pull request directly address the requirements specified in the corresponding issue.


What changes are made?

  1. Dynamic Weighting Ensemble (backend/main.py): Built an input-quality adaptive ensembling model. Gills, Eyes, and Body weights are recalculated dynamically based on their respective quality scores.
  2. Model Failure Resiliency: Configured re-normalization parameters to handle missing model streams gracefully without throwing API errors.
  3. Ensemble Weight Display (src/pages/AnalysisDashboard.tsx): Added a layout section in the main scorecard rendering body, eye, and gill contribution percentages.
  4. Margin of Error Interval: Replaced the hardcoded confidence intervals with calculated error standard deviations.
  5. Localization Mappings: Complete translations for the ensemble weights header in English, Hindi, and Bengali.

Technical Depth and Verification

Fixed weighting architectures fail when input crops are compromised. Our dynamic ensemble weights dynamically scale stream contributions according to quality, improving prediction accuracy. If one model fails, the engine re-normalizes the remaining active weights seamlessly.

Verified on the scan dashboard. The scorecard correctly displays the dynamic contribution percentages of body, eye, and gill models.

Summary by CodeRabbit

  • New Features

    • Added offline scan saving and automatic sync when the device comes back online.
    • Introduced scan comparison mode with side-by-side results and variance metrics.
    • Added an analytics view for freshness trends, vendor performance, and regional insights.
    • Expanded leaderboard profiles with vendor reviews and a submission form.
  • Bug Fixes

    • Improved scan confidence handling with uncertainty and margin-of-error display.
    • Added fraud and unsupported-species warnings in scan results.
    • Enabled richer species-specific recommendations and preservation guidance.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the karan3431's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a multi-model ensemble scoring system with confidence margins, species/fraud detection to backend scan endpoints, vendor review submission, an offline-first scan queue with background sync, an analytics trends dashboard, and a two-scan comparison feature, plus supporting localization strings across three languages.

Changes

Multi-Model Ensemble, Species & Fraud Detection

Layer / File(s) Summary
Backend ensemble/species/fraud core logic
backend/main.py
Scan payload builders compute per-stream quality/weights, variance-based margin of error, and add ensemble/species/fraud objects via a new _build_species_info helper.
Species/fraud detection in scan endpoints
backend/main.py
Demo-mode scan and scan-auto endpoints infer species/alert flags from filenames and persist species_detected; scan-auto accepts new edge-inference form fields.
Frontend confidence calculation & types
src/fusionInference.js, src/lib/types.ts
calculateConfidence derives numeric confidence and uncertain_flag; ScanResult gains optional fraud/ensemble fields.
Dashboard ensemble/fraud UI
src/pages/AnalysisDashboard.tsx
Renders uncertainty, unsupported-species, and fraud warning cards, margin-of-error display, and ensemble weights section.
Dashboard localization
src/i18n/locales/en.json, bn.json, hi.json
Adds strings for uncertainty, unsupported species, fraud, and ensemble labels.

Vendor Reviews & Leaderboard

Layer / File(s) Summary
Backend review endpoints
backend/vendors.py
Adds in-memory _REVIEWS_DB and GET/POST /{vendor_id}/reviews handlers.
Leaderboard vendor details & review submission
src/pages/Leaderboard.tsx
Adds a vendor selection panel to view and submit reviews with rating/comment.
Leaderboard localization
src/i18n/locales/*
Adds vendor trust and report-related strings.

Offline Scan Sync

Layer / File(s) Summary
Offline IndexedDB storage layer
src/lib/offlineDb.ts
New module for adding, listing, updating, and deleting queued offline scans.
API submission metadata support
src/lib/api.ts
EdgeInferenceMeta/submitScan support new confidence_score/species_detected fields.
ScannerPage offline save & sync
src/pages/ScannerPage.tsx
Saves scans offline on submit failure and background-syncs pending scans with a sync-now UI.
AnalysisDashboard offline scan resolution
src/pages/AnalysisDashboard.tsx
Resolves offline-* scan IDs from the queue and loads scan history for analytics.
Offline sync localization
src/i18n/locales/*
Adds sync status and sync-now action strings.

Analytics Trends

Layer / File(s) Summary
AnalyticsTrends component
src/components/AnalyticsTrends.tsx
New component charting daily/weekly freshness trends, vendor rankings, and regional averages.
Analytics localization
src/i18n/locales/*
Adds analytics section labels.

Scan Comparison

Layer / File(s) Summary
ResultsPage compare mode
src/pages/ResultsPage.tsx
Adds compare-mode selection, parallel scan fetch, and a side-by-side comparison modal with variance metrics.
Comparison localization
src/i18n/locales/*
Adds compare workflow and specimen comparison strings.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScannerPage
  participant OfflineDb
  participant SubmitScanAPI

  ScannerPage->>ScannerPage: generate offlineScanRecord
  alt online
    ScannerPage->>SubmitScanAPI: submitScan(record)
    SubmitScanAPI-->>ScannerPage: success/failure
    ScannerPage->>OfflineDb: addScan if failed
  else offline
    ScannerPage->>OfflineDb: addScan(record, pending)
  end
  ScannerPage->>OfflineDb: getPendingScans (on mount/online)
  OfflineDb-->>ScannerPage: pending scans
  ScannerPage->>SubmitScanAPI: submitScan(each pending scan)
  SubmitScanAPI-->>ScannerPage: result
  ScannerPage->>OfflineDb: deleteScan or updateScanStatus(failed)
Loading
sequenceDiagram
  participant User
  participant ResultsPage
  participant ScanAPI

  User->>ResultsPage: enable compare mode
  User->>ResultsPage: select scan A, scan B
  ResultsPage->>ScanAPI: getScan(idA), getScan(idB)
  ScanAPI-->>ResultsPage: scan A, scan B data
  ResultsPage->>ResultsPage: compute variance/decay
  ResultsPage-->>User: render comparison modal
Loading

Possibly related issues

Possibly related PRs

Suggested labels: frontend

Suggested reviewers: jpdevhub

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning [#169] The ensemble weights are based on random qualities, and explicit stream-failure re-normalization isn't shown, so the requested quality-based fallback isn't met. Use real input-quality metrics for weighting, add explicit missing-stream re-normalization, and derive the margin of error from probability spread.
Out of Scope Changes check ⚠️ Warning Offline sync, vendor reviews, compare mode, analytics, and leaderboard UI changes are unrelated to #169's ensemble-engine scope. Split unrelated features into separate PRs or link them to their own issues before merging.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a multi-model ensemble engine with dynamic weighting.
✨ 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

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

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

⚠️ No linked issue found!
This PR cannot be reviewed until a related issue is linked.
Please add Closes #issue_number in your PR description.

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

Caution

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

⚠️ Outside diff range comments (1)
src/pages/ScannerPage.tsx (1)

397-397: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

runScan closure omits t from its dependency array.

The callback now calls t('scanner.savedOffline', ...) (Line 375) but the deps [startProgress, stopProgress, stopCamera, navigate] don't include t, so a language change could leave a stale translator captured. Add t (and confirm exhaustive-deps passes for the added state setters).

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

In `@src/pages/ScannerPage.tsx` at line 397, The runScan callback in
ScannerPage.tsx is missing t in its dependency array, so the translator captured
by useCallback can become stale after a language change. Update the dependency
list for runScan to include t, and verify any related hooks in ScannerPage still
satisfy exhaustive-deps after the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/main.py`:
- Line 393: The uncertain_flag expression in the row processing logic is using a
falsy fallback, so a real confidence_score of 0 or 0.0 gets replaced with 1.0
and marked certain. Update the confidence handling in the code path that builds
the row dict in main.py to use an explicit None check instead of “or 1.0”, so
only missing confidence_score values default while valid zero values are
preserved.
- Around line 257-272: The ensemble weight calculation in the main scan payload
is using random.uniform values, so replace the randomized quality signals and
derived weights with real scan-driven metrics or a fixed 50/25/25 weighting in
the payload-building logic. Update the weight computation in the block that sets
gill_quality, eye_quality, body_quality, weight_body, weight_eye, and
weight_gill, and make the matching _row_to_payload path use the same
deterministic source so ensemble.weights reflects the actual documented
weighting instead of per-request randomness.
- Around line 663-702: The edge ONNX scan path is storing the human-readable
freshness label directly into the scan grade field, so history/dashboard records
the wrong grade. In the edge_onnx branch inside the scan insert flow, map
freshness_label to the same stored grade enum used by the demo/real paths before
calling _db().table("scans").insert, and ensure the value passed through
_to_db_grade matches the app’s existing grade format rather than the raw
"Fresh"/"Moderate"/"Spoiled" label.

In `@backend/vendors.py`:
- Around line 104-166: The default review seed is duplicated in both the GET
fallback and the POST initialization inside the vendors review handlers, which
risks them drifting apart. Extract the repeated seed list into a shared helper
such as _default_reviews() in the same module and use it from both the reviews
lookup and add_vendor_review paths so the seed data is defined in one place
only.
- Around line 139-145: The review creation logic in the vendors endpoint
currently reads `review_data` as a raw dict and casts `rating` with
`int(rating)`, which can raise on bad input and accepts invalid scores. Update
the review input handling around the `new_review` construction to use a Pydantic
model (for example, a `ReviewIn` type) with `rating` constrained to 1–5 and a
default `comment`, then access `review_data.rating` and `review_data.comment`
instead of dict lookups and manual casting.
- Around line 8-9: The module-level _REVIEWS_DB fallback in the vendors module
is only in-memory and will not survive restarts or work across multiple workers.
Replace this temporary storage with the same persistent backend used by vendors
and scans, or if it must remain as a stub, add an explicit TODO with an issue
reference so it is clearly temporary. Update the code around _REVIEWS_DB and any
review accessors to use the shared persistent store instead of a dict.
- Around line 129-137: The user lookup in the vendor author resolution block is
passing the Request object into get_current_user, which causes authentication to
fail and fall back to Anonymous Consumer. Update the call in the code path
around auth_header handling to pass the raw Authorization header value instead
of request, and make sure the exception handling in this block does not silently
swallow auth errors so failures are visible. Use the get_current_user call site
and the author assignment logic as the main points to update.
- Around line 122-127: The add_vendor_review route is effectively
unauthenticated because get_current_user is being called with a Request instead
of an Authorization header, so all reviews fall back to Anonymous Consumer.
Update add_vendor_review to require auth via Depends(get_current_user) (or
otherwise pass the proper Authorization value) and use the authenticated user
for review attribution.

In `@src/components/AnalyticsTrends.tsx`:
- Line 4: The AnalyticsTrends component has an unused Calendar import, which is
triggering the no-unused-vars lint error. Update the import in AnalyticsTrends
to remove Calendar, or if it is intended for future use, reference it somewhere
in the component so the import is actually used alongside TrendingUp,
TrendingDown, MapPin, and Award.
- Around line 283-297: The list rendering in AnalyticsTrends uses array indexes
as keys for vendors (and the similar regions list), which can break React
reconciliation when the sorted order changes. Update the map callbacks to use a
stable unique identifier from each item in the AnalyticsTrends render logic,
such as v.name for vendors and the corresponding region identifier for regions,
so re-sorts do not misassociate DOM/state.
- Line 26: The date labels in AnalyticsTrends are hardcoded to the en-IN locale,
so they won’t follow the active language. Update the date formatting logic in
AnalyticsTrends (the dateStr and weekStr computations) to use the current i18n
language from useTranslation() instead of a fixed locale. Keep the formatting
logic centralized so both day and week labels localize consistently for English,
Hindi, and Bengali users.
- Around line 153-169: The tab-toggle buttons in AnalyticsTrends are missing an
explicit type, so they default to submit behavior inside a form. Update both
button elements in the tab switcher to use the button type explicitly while
keeping their existing onClick handlers and styling, so clicking the
daily/weekly tabs only changes activeTab and does not submit a parent form.
- Around line 56-68: The `useMemo` in `AnalyticsTrends` is using `Date.now()`
while building the `vendorMap`, which makes the “recent” cutoff depend on render
time without being tracked as a dependency. Move the current-time value out of
the memoized calculation (or pass it in as an explicit dependency/state) and use
that stable reference inside the `scans.forEach` loop so the split logic
recalculates when time changes, not only when `scans` or `t` changes.
- Around line 82-90: The regional averages data in AnalyticsTrends is hardcoded
and does not use the scans input, so replace the static regions literal with
logic that derives regional values from the scans prop like the daily, weekly,
and vendors sections do. Update the data-building code in AnalyticsTrends to
aggregate scans by region name/key, compute each region’s value and scan count
from actual scan history, and keep the existing t(...) labels only for display
names. Ensure the returned regions array reflects live scan data rather than
fixed mock values.

In `@src/fusionInference.js`:
- Around line 259-266: The confidence calculation in calculateConfidence is
indexing eyeProbs and gillProbs as if they had more than two elements, but
extractEyeScore and extractGillScore only return 2-element vectors. Update
calculateConfidence to use the actual two probabilities for each vector,
recompute the eye and gill confidences from those two values, and ensure the
final system confidence stays normalized so uncertain_flag can trigger
correctly.

In `@src/i18n/locales/bn.json`:
- Around line 262-269: The Bengali leaderboard locale currently has duplicate
aliases for the same strings: the `Leaderboard` page still references
`leaderboard.vendorTrustLeaderboard` and `leaderboard.vendorSubtitle`, so the
added `title` and `subtitle` entries in `src/i18n/locales/bn.json` are unused.
Either update `src/pages/Leaderboard.tsx` to consume the new `title`/`subtitle`
keys consistently, or remove the redundant aliases from the locale and keep the
existing `vendorTrustLeaderboard`/`vendorSubtitle` keys to avoid duplication.

In `@src/i18n/locales/en.json`:
- Line 170: The preservDefault locale string has a capitalization issue after
the period, making the second sentence start incorrectly. Update the text in
en.json for preservDefault so the sentence after “Store below 4°C.” begins with
a capital letter, keeping the rest of the wording unchanged.

In `@src/lib/offlineDb.ts`:
- Around line 23-106: The IndexedDB helpers in offlineDb are leaking open
connections because openDB() is used in addScan, getPendingScans,
updateScanStatus, and deleteScan without ever closing the returned db. Update
each method to close the database after the transaction/request finishes, using
the same transaction.oncomplete/db.close pattern already implied for the first
operation, and make sure the connection is always closed even when the request
fails.

In `@src/pages/AnalysisDashboard.tsx`:
- Line 79: The AnalysisDashboard image preview is creating a blob URL with
URL.createObjectURL(found.image) and never releasing it, which leaks memory.
Update the AnalysisDashboard component to track the generated photo_url value
and add a useEffect cleanup that calls URL.revokeObjectURL for that URL when the
component unmounts or the URL changes, so the created object URL is properly
released.
- Around line 447-491: The interactive spot elements in AnalysisDashboard are
pointer-only because the eye/gill/body hotspots use clickable divs without
button semantics. Update the spot wrappers to use accessible activation in the
same spot-rendering block: preferably switch them to button elements with
type="button", or add role="button", tabIndex={0}, and an onKeyDown handler
alongside the existing onClick/setActiveSpot logic. Ensure the same behavior is
available for each hotspot so keyboard and screen-reader users can select the
active spot and reach the details panel.
- Line 141: The AnalysisDashboard render path assigns alerts from
recommendations.alert_flags but never uses it; remove the unused alerts variable
from the component and keep the surrounding recommendation rendering logic in
place.

In `@src/pages/Leaderboard.tsx`:
- Line 53: The reviews state in Leaderboard is using any[], which triggers the
no-explicit-any lint rule. Define a Review interface with id, author, rating,
comment, and timestamp, then update the useState declaration in Leaderboard to
use Review[] instead of any[] and make sure any review-related code in that
component uses the new type.
- Line 276: The Leaderboard panel still contains hardcoded English literals that
bypass i18n, while nearby UI already uses t() for translated strings. Update the
strings in the Leaderboard component (including the Rating label, the
submit/loading button states, and the empty-state message) to use t() with the
existing locale keys pattern, so all text in this panel is consistently
translated across en/hi/bn.
- Around line 236-241: The close button in Leaderboard.tsx currently relies on
the default submit behavior, and the textarea only has placeholder text without
an accessible name. Update the button in the selected-vendor controls to use
type="button" so it won’t submit a parent form, and add an aria-label to the
textarea in the feedback/input section so screen readers can identify it. Use
the existing button and textarea elements in Leaderboard to locate the fix.
- Line 254: The selected vendor freshness score rendering in Leaderboard.tsx can
crash when avg_freshness_score is null or undefined. Update the selectedVendor
display to use the same null-safe fallback as the list view, in the
selectedVendor rendering path, so it formats a default numeric value before
calling toFixed(1). Keep the fix localized to the
selectedVendor.avg_freshness_score usage.
- Around line 74-80: The authorization header in Leaderboard is reading the
wrong storage key, so the Bearer token is usually missing. Update the token
lookup in Leaderboard to use the shared token helper from src/lib/api.ts instead
of reading 'supabase.auth.token' directly, and make the request reuse that
centralized auth logic so the Authorization header is attached consistently.
- Around line 59-67: The reviews loading effect in Leaderboard is vulnerable to
stale responses and ignores HTTP failures. Update the useEffect around
selectedVendor/fetch to check response status before parsing, and add request
cancellation or an ignore flag so earlier requests cannot overwrite newer vendor
reviews. Use the existing setReviewsLoading flow and the selectedVendor
dependency to ensure only the latest vendor’s data updates state.

In `@src/pages/ResultsPage.tsx`:
- Around line 119-138: Apply the appropriate PR complexity label for this
compare-mode change in ResultsPage; the new compare flow and selection state
around compareMode and setSelectedIds fits complexity: intermediate, so add that
label unless you are labeling the broader stacked ensemble/ML pipeline work, in
which case use complexity: high. Follow the repo’s auto-labeling rule by
assigning exactly one complexity label for the PR.
- Line 284: The modal labels in ResultsPage are still hardcoded English, which
is inconsistent with the rest of the localized UI. Update the strings used in
the comparison modal and cards in ResultsPage—especially the `comparisonLoading`
fallback, the `SPECIMEN A`/`SPECIMEN B` headers, and the `GILLS`/`EYES`/`SCALES`
labels—to use `t(...)` like the existing `results.specimenA` usage. Add or wire
up the corresponding bn/hi/en translation keys for these labels so the same
symbols render correctly across locales.
- Around line 293-301: The comparison modal in ResultsPage is missing
accessibility and keyboard dismissal support. Update the modal container around
the comparison view to use dialog semantics with aria-modal and an accessible
label, add an Escape-to-close handler (for example via a useEffect tied to
comparisonData), allow backdrop clicks to close the modal, and give the
icon-only close button an aria-label so screen readers can identify it. Use the
existing setComparisonData(null) close path and the modal overlay/button markup
in ResultsPage to wire these fixes in.
- Line 19: The comparisonData state in ResultsPage should use the real scan
response type instead of any, since the modal accesses fields from the scan
shape and the current type is triggering the no-explicit-any lint error. Update
the useState declaration for comparisonData to reference the ScanResponse shape
returned by api.getScan, and keep the nested scan1/scan2 objects typed
consistently so accesses like species.common_name and
biomarkers.gill_saturation.score remain type-checked.

In `@src/pages/ScannerPage.tsx`:
- Line 149: The ScannerPage sync toast setup has an unused binding: the return
value from toast.loading in the offline sync flow is assigned to syncToastId but
never referenced. Remove that assignment and continue using the existing literal
id path in the ScannerPage background sync logic (including the toast
update/dismiss calls) so the no-unused-vars lint issue is resolved without
changing behavior.
- Line 492: The SYNC NOW control in ScannerPage’s button markup should
explicitly avoid implicit form submission. Update the `<button>` used for this
action to include `type="button"` so it stays a normal action button even if it
is rendered inside a form; locate the button in the ScannerPage component where
the sync action is defined.
- Line 337: The offline ID suffix in ScannerPage uses the deprecated
String.prototype.substr call. Update the offlineId generation logic to avoid
substr by using slice(2, 11) on the Math.random().toString(36) result, or switch
to crypto.randomUUID() if that fits the surrounding code better. Keep the change
localized to the offlineId creation expression so the ScannerPage flow remains
unchanged.
- Line 371: The catch block in ScannerPage’s fallback branch declares an unused
error variable, which triggers the lint rule; update the `catch (err)` in the
relevant error-handling path to use an optional catch binding since the error is
not referenced. Keep the surrounding fallback logic in `ScannerPage` the same,
and adjust only the catch syntax so the branch still handles failures without
introducing an unused `err` binding.
- Around line 477-501: ScannerPage renders GlassCard in the pending-scans banner
but does not import it, so the component will fail at render time when
pendingCount is greater than zero. Add the missing GlassCard import in
ScannerPage.tsx alongside the other top-level imports, and verify the pending
sync banner still renders correctly when syncingScans and checkAndSyncScans are
used.

---

Outside diff comments:
In `@src/pages/ScannerPage.tsx`:
- Line 397: The runScan callback in ScannerPage.tsx is missing t in its
dependency array, so the translator captured by useCallback can become stale
after a language change. Update the dependency list for runScan to include t,
and verify any related hooks in ScannerPage still satisfy exhaustive-deps after
the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3333dded-a91c-44c0-955a-b35ebaf7cad5

📥 Commits

Reviewing files that changed from the base of the PR and between 97c6f33 and 1cbd8e4.

📒 Files selected for processing (14)
  • backend/main.py
  • backend/vendors.py
  • src/components/AnalyticsTrends.tsx
  • src/fusionInference.js
  • src/i18n/locales/bn.json
  • src/i18n/locales/en.json
  • src/i18n/locales/hi.json
  • src/lib/api.ts
  • src/lib/offlineDb.ts
  • src/lib/types.ts
  • src/pages/AnalysisDashboard.tsx
  • src/pages/Leaderboard.tsx
  • src/pages/ResultsPage.tsx
  • src/pages/ScannerPage.tsx

Comment thread backend/main.py
Comment on lines +257 to +272
gill_quality = round(random.uniform(0.85, 0.98), 2)
eye_quality = round(random.uniform(0.80, 0.96), 2)
body_quality = round(random.uniform(0.90, 0.99), 2)

raw_w_body = 0.50 * body_quality
raw_w_eye = 0.25 * eye_quality
raw_w_gill = 0.25 * gill_quality
sum_w = raw_w_body + raw_w_eye + raw_w_gill
weight_body = round(raw_w_body / sum_w, 2)
weight_eye = round(raw_w_eye / sum_w, 2)
weight_gill = round(raw_w_gill / sum_w, 2)

mean_score = (gill_score + eye_score + body_score) / 3
variance = ((gill_score-mean_score)**2 + (eye_score-mean_score)**2 + (body_score-mean_score)**2) / 3
std_dev = round(math.sqrt(variance), 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map backend/main.py structure first.
ast-grep outline backend/main.py --view expanded || true

# Show the relevant line ranges with context.
sed -n '230,310p' backend/main.py
printf '\n---\n'
sed -n '340,420p' backend/main.py
printf '\n---\n'
sed -n '600,840p' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 19033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all uses of the ensemble payload fields to see whether they affect logic or are display-only.
rg -n '"ensemble"|qualities|weights|margin_of_error|gill_quality|eye_quality|body_quality' backend frontend . || true

Repository: jpdevhub/FreshScanAi

Length of output: 6671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect documentation around the intended ensemble weight semantics.
sed -n '280,320p' DOCUMENTATION.md
printf '\n---\n'
sed -n '290,330p' src/pages/AnalysisDashboard.tsx

Repository: jpdevhub/FreshScanAi

Length of output: 3679


Use real quality signals or fixed weights here. backend/main.py:257-307 and the matching _row_to_payload block build ensemble.weights from random.uniform(...), so the dashboard shows per-stream weights that have no relationship to the scan or the documented 50/25/25 weighting.

🧰 Tools
🪛 ast-grep (0.44.0)

[info] 257-257: use secrets package over random package
Context: random.uniform(0.80, 0.96)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)


[info] 258-258: use secrets package over random package
Context: random.uniform(0.90, 0.99)
Note: [CWE-330] Use of Insufficiently Random Values.

(avoid-random-python)

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

In `@backend/main.py` around lines 257 - 272, The ensemble weight calculation in
the main scan payload is using random.uniform values, so replace the randomized
quality signals and derived weights with real scan-driven metrics or a fixed
50/25/25 weighting in the payload-building logic. Update the weight computation
in the block that sets gill_quality, eye_quality, body_quality, weight_body,
weight_eye, and weight_gill, and make the matching _row_to_payload path use the
same deterministic source so ensemble.weights reflects the actual documented
weighting instead of per-request randomness.

Comment thread backend/main.py
"weight_estimate_kg": 1.2,
"catch_age_hours": 6,
},
"uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Falsy-default masks a low-confidence row.

(row.get("confidence_score") or 1.0) < 0.70 treats a stored confidence_score of 0 (or 0.0) as 1.0, flipping a maximally-uncertain scan to "certain". Use an explicit None check so only missing values fall back.

🐛 Proposed fix
-        "uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,
+        "uncertain_flag": (row.get("confidence_score") if row.get("confidence_score") is not None else 1.0) < 0.70,
📝 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
"uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,
"uncertain_flag": (row.get("confidence_score") if row.get("confidence_score") is not None else 1.0) < 0.70,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.py` at line 393, The uncertain_flag expression in the row
processing logic is using a falsy fallback, so a real confidence_score of 0 or
0.0 gets replaced with 1.0 and marked certain. Update the confidence handling in
the code path that builds the row dict in main.py to use an explicit None check
instead of “or 1.0”, so only missing confidence_score values default while valid
zero values are preserved.

Comment thread backend/main.py
Comment on lines +663 to +702
if source == "edge_onnx" and fused_score is not None:
freshness = int(fused_score * 100)
conf = confidence_score or 0.85
edge_fusion = {
"final_score_percent": freshness,
"final_grade": _to_db_grade(freshness_label or "C"),
"confidence_score": conf,
"uncertain_prediction_flag": conf < 0.70,
"regional_breakdown": {
"gill_freshness_score": fused_score,
"eye_freshness_score": fused_score,
"body_freshness_score": fused_score,
},
}
photo_url = await _upload_image(image_bytes, str(current_user.id), scan_id)
payload = _build_scan_payload(edge_fusion, scan_id, display_id, photo_url)
if species_detected:
payload["species"]["common_name"] = species_detected

try:
_db().table("scans").insert(
{
"id": scan_id,
"user_id": str(current_user.id),
"final_grade": _to_db_grade(payload["grade"]),
"confidence_score": conf,
"image_type": "BODY",
"freshness_index": payload["freshness_index"],
"scan_display_id": display_id,
"species_detected": species_detected or "Rohu Carp",
"biomarker_json": payload["biomarkers"],
"storage_hours": payload["recommendations"]["consume_within_hours"],
"alert_flags": payload["recommendations"]["alert_flags"],
"photo_urls": [photo_url] if photo_url else [],
}
).execute()
except Exception as exc:
print(f"DB write failed (edge_onnx): {exc}")

return {"success": True, "scan": payload}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'def _to_db_grade' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant backend helpers and edge_onnx path.
sed -n '224,310p' backend/main.py
printf '\n----\n'
sed -n '620,710p' backend/main.py
printf '\n----\n'
rg -n -C3 '_build_scan_payload|freshness_label|fused_score|edge_onnx' backend/main.py src/lib/api.ts
printf '\n----\n'
sed -n '1,220p' src/lib/api.ts

Repository: jpdevhub/FreshScanAi

Length of output: 18343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the first read is too large, inspect only the submitScan function and any grade mappings.
rg -n -C4 'submitScan|freshness_label|grade' src/lib/api.ts
printf '\n----\n'
rg -n -C4 'def _build_scan_payload|payload\["grade"\]|final_grade|freshness_index' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 9494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the client payload shape and the payload builder semantics.
rg -n -C4 'submitScan|freshness_label|freshnessLabel|grade|final_grade' src/lib/api.ts src -g '!**/node_modules/**'
printf '\n----\n'
python3 - <<'PY'
from pathlib import Path
p = Path('backend/main.py')
text = p.read_text()
for needle in ['def _build_scan_payload', 'payload["grade"]', 'final_grade', 'freshness_index', 'def _to_db_grade']:
    idx = text.find(needle)
    print(f'NEEDLE: {needle} -> {idx}')
PY

Repository: jpdevhub/FreshScanAi

Length of output: 31070


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the backend source of freshness_label and the client submitScan payload.
rg -n -C4 'freshness_label|freshnessLabel|submitScan|final_grade|_build_scan_payload|_to_db_grade' backend/main.py src/lib/api.ts
printf '\n----\n'
sed -n '1,260p' src/lib/api.ts
printf '\n----\n'
sed -n '600,710p' backend/main.py

Repository: jpdevhub/FreshScanAi

Length of output: 24457


Map the edge ONNX label to the stored grade enum

freshness_label is "Fresh"/"Moderate"/"Spoiled", but this path writes it into final_grade. The rest of the app treats final_grade as the grade used by the demo/real paths, so synced edge scans will show the wrong grade in history/dashboard. _to_db_grade is a no-op here, so it doesn't translate the label; convert the edge label to the same stored grade before inserting.

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

In `@backend/main.py` around lines 663 - 702, The edge ONNX scan path is storing
the human-readable freshness label directly into the scan grade field, so
history/dashboard records the wrong grade. In the edge_onnx branch inside the
scan insert flow, map freshness_label to the same stored grade enum used by the
demo/real paths before calling _db().table("scans").insert, and ensure the value
passed through _to_db_grade matches the app’s existing grade format rather than
the raw "Fresh"/"Moderate"/"Spoiled" label.

Comment thread backend/vendors.py
Comment on lines +8 to +9
# In-memory reviews database fallback
_REVIEWS_DB = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

In-memory _REVIEWS_DB will not persist or scale.

State stored in a module-level dict is lost on every restart/redeploy and is not shared across multiple Uvicorn/Gunicorn workers, so reviews will appear and disappear depending on which process serves the request. If this is a temporary stub, please add a TODO and issue; otherwise persist to the same store used by vendors/scans.

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

In `@backend/vendors.py` around lines 8 - 9, The module-level _REVIEWS_DB fallback
in the vendors module is only in-memory and will not survive restarts or work
across multiple workers. Replace this temporary storage with the same persistent
backend used by vendors and scans, or if it must remain as a stub, add an
explicit TODO with an issue reference so it is clearly temporary. Update the
code around _REVIEWS_DB and any review accessors to use the shared persistent
store instead of a dict.

Comment thread backend/vendors.py
Comment on lines +104 to +166
reviews = _REVIEWS_DB.get(vendor_id, [
{
"id": "rev-1",
"author": "Ankit R.",
"rating": 5,
"comment": "Consistently fresh rohu fish. Highly recommended!",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
},
{
"id": "rev-2",
"author": "Deepika S.",
"rating": 4,
"comment": "Good quality scales, operculum is bright red. Fair pricing.",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
}
])
return {"success": True, "reviews": reviews}

@router.post("/{vendor_id}/reviews")
async def add_vendor_review(
vendor_id: str,
review_data: dict,
request: Request
):
author = "Anonymous Consumer"
try:
auth_header = request.headers.get("Authorization")
if auth_header:
# We can call get_current_user dynamically
user = await get_current_user(request)
if user:
author = user.user_metadata.get("full_name") or user.email
except Exception:
pass

rating = review_data.get("rating", 5)
comment = review_data.get("comment", "")

new_review = {
"id": f"rev-{datetime.now(timezone.utc).timestamp()}",
"author": author,
"rating": int(rating),
"comment": comment,
"timestamp": datetime.now(timezone.utc).isoformat()
}

if vendor_id not in _REVIEWS_DB:
_REVIEWS_DB[vendor_id] = [
{
"id": "rev-1",
"author": "Ankit R.",
"rating": 5,
"comment": "Consistently fresh rohu fish. Highly recommended!",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
},
{
"id": "rev-2",
"author": "Deepika S.",
"rating": 4,
"comment": "Good quality scales, operculum is bright red. Fair pricing.",
"timestamp": (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
}
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate default-review block.

The identical seed list is hardcoded in both the GET fallback (Lines 104–119) and the POST seeding (Lines 151–166). Extract a _default_reviews() factory so the two copies cannot diverge.

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

In `@backend/vendors.py` around lines 104 - 166, The default review seed is
duplicated in both the GET fallback and the POST initialization inside the
vendors review handlers, which risks them drifting apart. Extract the repeated
seed list into a shared helper such as _default_reviews() in the same module and
use it from both the reviews lookup and add_vendor_review paths so the seed data
is defined in one place only.

Comment thread src/pages/ScannerPage.tsx
if (pending.length === 0) return;

setSyncingScans(true);
const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused syncToastId binding.

toast.loading(...) returns an id that is never read — you dismiss/update using the literal 'offline-sync' id at Lines 175/177/182. This fails the @typescript-eslint/no-unused-vars lint gate.

♻️ Drop the unused assignment
-      const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
+      toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });

Also applies to: 174-178

🧰 Tools
🪛 ESLint

[error] 149-149: 'syncToastId' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

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

In `@src/pages/ScannerPage.tsx` at line 149, The ScannerPage sync toast setup has
an unused binding: the return value from toast.loading in the offline sync flow
is assigned to syncToastId but never referenced. Remove that assignment and
continue using the existing literal id path in the ScannerPage background sync
logic (including the toast update/dismiss calls) so the no-unused-vars lint
issue is resolved without changing behavior.

Source: Linters/SAST tools

Comment thread src/pages/ScannerPage.tsx
canvas.toBlob(
async (saveBlob) => {
if (!saveBlob) return;
const offlineId = `offline-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

String.prototype.substr is deprecated — use slice.

substr is a legacy Annex B feature; prefer slice(2, 11) (or crypto.randomUUID()) for the offline id suffix.

♻️ Replace substr
-            const offlineId = `offline-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+            const offlineId = `offline-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
📝 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
const offlineId = `offline-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const offlineId = `offline-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ScannerPage.tsx` at line 337, The offline ID suffix in ScannerPage
uses the deprecated String.prototype.substr call. Update the offlineId
generation logic to avoid substr by using slice(2, 11) on the
Math.random().toString(36) result, or switch to crypto.randomUUID() if that fits
the surrounding code better. Keep the change localized to the offlineId creation
expression so the ScannerPage flow remains unchanged.

Comment thread src/pages/ScannerPage.tsx
}
} catch {
/* offline or backend down — result still shown locally */
} catch (err) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unused err binding in catch — lint failure.

err at Line 371 is never referenced in the fallback branch, tripping @typescript-eslint/no-unused-vars. Use an optional catch binding (catch {) since the caught error isn't used.

♻️ Use optional catch binding
-            } catch (err) {
+            } catch {
               await offlineDb.addScan(offlineScanRecord);
📝 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
} catch (err) {
} catch {
🧰 Tools
🪛 ESLint

[error] 371-371: 'err' is defined but never used.

(@typescript-eslint/no-unused-vars)

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

In `@src/pages/ScannerPage.tsx` at line 371, The catch block in ScannerPage’s
fallback branch declares an unused error variable, which triggers the lint rule;
update the `catch (err)` in the relevant error-handling path to use an optional
catch binding since the error is not referenced. Keep the surrounding fallback
logic in `ScannerPage` the same, and adjust only the catch syntax so the branch
still handles failures without introducing an unused `err` binding.

Source: Linters/SAST tools

Comment thread src/pages/ScannerPage.tsx
Comment on lines +477 to +501
{pendingCount > 0 && (
<div className="absolute top-4 inset-x-4 z-40">
<GlassCard className="p-3 border-l-4 border-secondary! flex items-center justify-between" variant="tonal">
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} />
<span className={`relative inline-flex rounded-full h-2 w-2 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} />
</span>
<span className="font-mono text-[0.625rem] tracking-widest text-on-surface uppercase">
{syncingScans
? t('scanner.syncingScans', 'Syncing offline scans in background...')
: `${pendingCount} ${t('scanner.pendingSyncScans', 'OFFLINE SCANS PENDING SYNC')}`}
</span>
</div>
{!syncingScans && navigator.onLine && (
<button
onClick={checkAndSyncScans}
className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer"
>
{t('scanner.syncNowButton', 'SYNC NOW')}
</button>
)}
</GlassCard>
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i glasscard -e tsx -e ts | xargs -I{} sh -c 'echo "== {} =="; rg -nP "export (default |const |function )?GlassCard" {}'

Repository: jpdevhub/FreshScanAi

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== ScannerPage imports ==\n'
sed -n '1,40p' src/pages/ScannerPage.tsx

printf '\n== GlassCard component ==\n'
sed -n '1,200p' src/components/GlassCard.tsx

Repository: jpdevhub/FreshScanAi

Length of output: 2306


Import GlassCard in src/pages/ScannerPage.tsx. It’s rendered in the pending-scans banner but never imported, so the first time pendingCount > 0 the page throws a ReferenceError during render.

🧰 Tools
🪛 React Doctor (0.5.8)

[error] 479-479: GlassCard crashes at runtime because it isn't defined here.

Import the component or fix the typo so React can resolve the JSX identifier at runtime.

(jsx-no-undef)


[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

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

In `@src/pages/ScannerPage.tsx` around lines 477 - 501, ScannerPage renders
GlassCard in the pending-scans banner but does not import it, so the component
will fail at render time when pendingCount is greater than zero. Add the missing
GlassCard import in ScannerPage.tsx alongside the other top-level imports, and
verify the pending sync banner still renders correctly when syncingScans and
checkAndSyncScans are used.

Source: Linters/SAST tools

Comment thread src/pages/ScannerPage.tsx
</span>
</div>
{!syncingScans && navigator.onLine && (
<button

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Set an explicit type="button" on the SYNC NOW button.

Per the React Doctor hint, a <button> without a type defaults to submit; add type="button" to guard against accidental form submission if this is ever nested in a form.

🧰 Tools
🪛 React Doctor (0.5.8)

[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

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

In `@src/pages/ScannerPage.tsx` at line 492, The SYNC NOW control in ScannerPage’s
button markup should explicitly avoid implicit form submission. Update the
`<button>` used for this action to include `type="button"` so it stays a normal
action button even if it is rendered inside a form; locate the button in the
ScannerPage component where the sync action is defined.

Source: Linters/SAST tools

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature 10: Multi-Model AI Ensemble Engine

1 participant