- useDetection.analyze() now awaits a requestAnimationFrame before
calling runInference. Without it React commits isAnalyzing=true and
immediately hits the synchronous TFLite runSync that blocks the JS
thread for 500-1500ms — the analyzing skeleton overlay appears AFTER
the inference, defeating its purpose.
- Same hook enforces a minimum 600ms total before resolving so a
cached/fast inference doesn't show a skeleton flicker that reads as
a glitch.
- ScannerScreen.handleCapture is split: capture stays inline,
processImage(uri) is now its own async function. Cleaner control
flow when a take succeeds but analysis is delegated.
- The previously dead "image gallery" icon next to the shutter is now
a real TouchableOpacity that fires a "coming soon" toast (we'll wire
it to expo-image-picker once we add the lib + native rebuild).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related UX improvements when the device has no network:
- OfflineNoticeModal pops once per offline session on the Home tab
with a translated "Tu peux continuer en local" message. Dismissed
state lives in a ref keyed on connectivity transitions, so coming
back online and going offline again will re-show it.
- useDiseaseDetail and useGuideDetail now check NetworkContext before
attempting the API call. Without this, an offline disease detail
screen would trigger a 10s fetch timeout and look frozen; we now
fall back to the cached or bundled local data immediately and set
isLoading=false on the same tick.
- ToastContext (NetworkToastWatcher) i18n-ifies the offline/online
toast strings via the new network.* keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The detection pipeline already returned result === 'not_vine' but the
app rendered it the same as a low-confidence positive, which was
confusing (a coffee cup classified at 35% would show up as "uncertain
vine"). Surface non-vine results explicitly across the app:
- New ScanStatus 'not_vine' branch in types/detection.getScanStatus()
- StatusTag, ScanListItem fill, MapBottomSheet row icon (HelpCircle)
and MapView marker color get a neutral grey palette for not_vine
- ResultScreen short-circuits to a centered "Aucune vigne détectée"
layout with a single CTA "Reprendre une photo" (instead of pretending
the model has a meaningful prediction to show)
- MapBottomSheet learns an isLoading prop and renders 4 row skeletons
while useHistory rehydrates, instead of flashing the "no plants" empty
state. MapScreen plumbs historyLoading through
Bundles the i18n additions (FR + EN) for this commit and the next
two: result.notVineTitle/Message, myPlants.status.notVine, plus the
network.* and scanner.galleryComingSoon* keys used by follow-up
commits — splitting JSON hunks would have been more churn than
signal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: a guest scanning a plant fired POST /api/mobile/scans without
a Bearer token, the backend rightfully replied 401, and the apiPost
emitter dispatched 'unauthorized' which AuthContext interpreted as
"session lost" and wiped the local guest, kicking the user back to
Onboarding.
Two fixes:
1. apiGet/apiPost now track whether a Bearer was actually attached
to the request and only emit the 'unauthorized' event when one was
sent. An anonymous 401 stays a plain SERVER error.
2. pushScan() short-circuits if getToken() returns null, so guests
never even hit the network for scan persistence.
Combined effect: guests stay guests, registered users still get
session-revocation feedback when their token is rejected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .tflite reports inputs[0].shape == [1, 224, 224, 3] but the
preprocessing was producing 256x256x3 buffers, so 196608 floats were
written into a tensor sized for 150528. Inference still completed
(no allocator check on the JS side) but ran on shifted, decadred
data — predictions were effectively random.
Also: the v3 react-native-fast-tflite binding rejects raw TypedArray
views with "TfliteModel.runSync(...): Object \"<element dump>\"" and
only accepts the underlying ArrayBuffer. We now pass `input.buffer`
as the primary path and keep the TypedArray as a fallback.
Bonus:
- Read input dtype from the model and dispatch preprocess accordingly
(float32, uint8, int8) instead of hard-coding float32. Future-proof
for a quantized re-export.
- Dequantize uint8/int8 outputs to floats so argmax stays consistent.
- Log model.inputs and model.outputs at load time — invaluable when
re-exporting the .tflite and discovering shape mismatches.
Validated on device (Samsung S23): preprocess 700ms + inference 39ms,
no fallback chain. Still ~25% accuracy because the model itself is
overfit (see docs/audit_report.md), but the inference plumbing is
finally honest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the React Native app to the vineye-admin backend so user accounts
and scans flow into the admin panel, and so a ban applied via the panel
takes effect on the device on the next app boot (or sooner on any
authenticated request).
Core
- Install expo-secure-store for storing the better-auth session token.
Falls back to AsyncStorage on web/Expo Go where SecureStore is unavailable.
- New tokenStorage service with saveToken/getToken/removeToken and a
stable per-install getDeviceId() (used to derive the deterministic
password the backend signs sign-up/sign-in with).
- Extend the API client with apiPost(), automatic Bearer header attach,
and a tiny pub/sub (authEvents) that emits 'unauthorized' on 401 and
'banned' on 403 with banned: true. Handlers are global so any request
can trigger logout or open the BannedModal.
Auth
- New services/api/auth.ts: syncUser (POST /auth/sync), fetchMe (GET
/auth/me), signOutServer (POST /auth/sign-out, best-effort).
- types/auth.User now carries optional banned/bannedReason/role/xp/level
hydrated from the backend.
- AuthContext.login is now async vs the backend; the server-side id
replaces any locally-generated UUID so mobile and admin agree on the
same User row. Hydration is optimistic from AsyncStorage (never blocks
the splash on the network) and a background fetchMe picks up server
ban changes. logout/resetAccount best-effort revoke the server session.
- AuthChoiceScreen surfaces sign-up failures through a toast instead of
silently dropping the user into the app with no account.
Ban UX
- BannedModal: non-dismissible Tailwind modal with the bannedReason
interpolated and a CTA that calls resetAccount. Mounted globally in
RootNavigator and toggled by isBanned from AuthContext.
- Banned state is persisted alongside the User in AsyncStorage so the
modal stays visible across restarts even when /auth/me is unreachable.
Scan sync
- New services/api/scans.pushScan() that maps the mobile ScanRecord to
the backend body: confidence /100 (0-100 → 0-1 backend), diseaseSlug
passthrough (server resolves to diseaseId), latitude/longitude direct,
imageUrl always null (V1 keeps photos local-only), customName dropped
(no column server-side, stays in AsyncStorage).
- useHistory.addScan now fires pushScan after the local save and ignores
failures so the app stays usable offline.
i18n
- New keys auth.errors.network/signupFailed and auth.banned.{title,
description,descriptionNoReason,cta}. The fr/en files also include
scanner gallery placeholder keys from an adjacent feature WIP — not
part of this commit's scope but bundled here to avoid splitting a
small JSON.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Textarea below the ban Switch that lets the admin write the reason
shown to the mobile user in the BannedModal. The reason is persisted on
blur via PATCH /api/users/[id] (existing route), and only rendered when
the user is currently banned to keep the UI tight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connect the React Native app to the admin backend so users and scans flow
into the panel and bans take effect on the device.
- Activate the bearer() plugin in lib/auth.ts so mobile clients can pass
the better-auth session token via Authorization: Bearer header
- Add requireMobileAuth() helper in lib/auth-guard.ts that resolves the
session, re-fetches the user from DB (banned flag is on User, not in
the Session payload) and returns 403 with banned/bannedReason for
banned accounts
- Extend CORS in middleware.ts to allow POST + Authorization header on
/api/mobile/* (preflight was failing before)
- New routes:
POST /api/mobile/auth/sync — passwordless mobile auth via
deterministic password derived from sha256(email + deviceId + pepper).
Tries signIn first, falls back to signUp on USER_NOT_FOUND. Returns
409 when the email exists with a different deviceId.
GET /api/mobile/auth/me — current user enriched with
banned/bannedReason/role/xp/level
POST /api/mobile/auth/sign-out — best-effort session revocation
POST /api/mobile/scans — create a scan, resolves diseaseSlug
to diseaseId, never accepts an imageUrl from the device (V1 keeps
photos local-only)
GET /api/mobile/scans — own scans, 50 most recent
Validated end-to-end via curl: signUp → me → repeat sync (idempotent) →
post scan → ban via DB → me reflects banned: true → POST scans returns
403 + banned/bannedReason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Touch targets on the bottom tabs were too small near the top edge of the
bar, making it easy to miss the tap when reaching for an icon. Add
hitSlop on the Pressable and pointerEvents="none" on the active-state
badge so taps always land on the parent button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ML
- Reinstall react-native-fast-tflite + react-native-nitro-modules and
register the fast-tflite Expo plugin in app.json
- Wire model.ts to the real native module: dynamic require + lazy
loadTensorflowModel (cached), softmax/argmax on output, build Detection
with the project 0-100 confidence convention. Falls back to mockDetection
on any load/inference failure so the app never breaks.
- Align preprocessing input size to 256x256 to match the Python
MobileNetV2 export.
Scanner UX
- Preload the TFLite model on Scanner mount to avoid the ~1-2s decode hit
on first capture
- Add a flip-front/back camera control with a toast warning that the rear
camera gives better results
- Show a full-screen analyzing skeleton overlay while inference runs
- Memoize ConfidenceMeter color into a single computed value
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The withCmakeFix plugin now also modifies the root android/build.gradle
via withProjectBuildGradle, iterating over subprojects with
plugins.withId('com.android.library') / plugins.withId('com.android.application').
Using plugins.withId (vs subprojects { afterEvaluate {} }) avoids the
"Cannot run Project.afterEvaluate when the project is already evaluated"
error caused by gradle-plugins (kotlin, expo-gradle-plugin, ...) being
evaluated before the closure runs.
This unblocks the native CMake build of react-native-fast-tflite,
react-native-nitro-modules, react-native-screens, expo-modules-core, etc.
on Windows where the path-too-long issue affected subproject .o files.
Build verified: BUILD SUCCESSFUL in 15m 17s, 842 tasks, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SettingsScreen :
- Section "Compte" : ligne user (avatar + nom + email si non-guest +
badge "Invité" orange si isGuest) + ligne "Recommencer avec un nouveau
compte" (icône RefreshCw rouge)
- Reset account : remplace Alert.alert natif par ConfirmDialog stylé
(variant destructive). Au confirm, resetAccount() puis
navigation.reset({ index: 0, routes: [{ name: 'Onboarding' }] }) après
un setTimeout(50) pour laisser RootNavigator re-render avec le screen
Onboarding monté
- Language picker : remplace le toggle inline (clic = swap FR/EN) par
l'ouverture d'un LanguagePickerModal stylé
LanguagePickerModal (composant ui réutilisable) :
- Tailwind only : Modal RN + backdrop noir 50% + card rounded-3xl + shadow
- Header icône Globe verte + titre + subtitle
- 2 options Francais/English avec drapeau emoji 28px + label 16px
font-semibold ; option active : bg vert pâle + border verte + cercle
vert avec checkmark
- Bouton Annuler ghost grisé en bas
Messages i18n explicites :
- 'Vous serez redirigé vers l'écran de connexion pour créer un nouveau
compte ou continuer en invité'
- CTA destructive : 'Oui, me déconnecter'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useScanDetail :
- Nouvelle méthode setLocation(coords) qui persiste latitude / longitude
(top-level pour MapScreen.hasLocation), locationCapturedAt et
l'objet location.{latitude, longitude} dans AsyncStorage + state local
ScanDetailScreen :
- handleAddLocation appelle requestAndGetLocation() (useScanLocation hook
existant) puis setLocation()
- État addingLocation : bouton désactivé + opacity 0.6 + ActivityIndicator
spinner à la place de l'icône MapPin + label 'Localisation en cours...'
- Toast success / error + hapticSuccess en cas de succès
- La carte location bascule automatiquement vers l'affichage des coords
une fois persisté → la plante apparaît aussi sur MapScreen et
SearchScreen mode Map
i18n :
- myPlants.toasts.locationAdded
- myPlants.detail.locating
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConfirmDialog (nouveau composant ui réutilisable) :
- Modal RN avec backdrop noir 50%, card rounded-3xl + shadow
- Variant 'destructive' (icône AlertTriangle rouge) ou 'default' (Check vert)
- Boutons côte-à-côte avec icônes X/Trash2/Check, minHeight 52px,
ghost grisé bordé + primary/destructive avec shadow
- Tap backdrop = cancel, tap dialog = no-op via stopPropagation
ScanListItem :
- Remplace Alert.alert natif par ConfirmDialog stylé pour la suppression
- Le swipe gauche → bouton Supprimer → un seul modal ConfirmDialog → confirm
MyPlantsScreen :
- Suppression du DOUBLE modal (Alert dans handleDeleteScan était redondant
avec celui de ScanListItem → 2 modals successifs avant suppression)
→ handleDeleteScan appelle directement deleteScan(id)
- FadeInDown.springify().damping(18) sur header / SearchBar / chaque
date group avec stagger index*60
- Skeleton loading state : 2 groupes simulés (header bar + 3
ScanListItemSkeleton dans une card-loading sans elevation)
DateGroupAccordion :
- Retrait de l'elevation Android sur styles.card → fix le flash "rectangle
blanc + ombre" pendant l'animation FadeInDown du parent. iOS shadow
conservée (composite layer respecte l'opacité)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Animations FadeInDown.springify().damping(16) avec stagger 60ms entre
sections du HomeScreen :
- SearchHeader (delay 0)
- SearchSection (delay 60)
- RecentScans (delay 120)
- FrequentDiseasesHorizontal (delay 200)
- PracticalGuides (delay 280)
Skeleton loading states :
- LargeDiseaseCardCompactSkeleton : nouveau, simule la structure compact
(badge + icon + title + desc + footer) — utilisé dans
FrequentDiseasesHorizontal en remplacement de CarouselCardSkeleton
- ScanListItemSkeleton : nouveau, simule image 64x64 + name + status pill
+ time + confidence tile — utilisé dans RecentScans
- RecentScans / PracticalGuides : nouveau style cardLoading sans
shadow/elevation Android (qui ne respecte pas l'opacité de FadeInDown
→ flash "rectangle blanc + ombre" pendant l'anim). iOS shadow conservé.
isLoading propagé depuis useDiseases / useGuides / useHistory vers les
sections concernées.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Une seule SearchScreen modale gère la recherche depuis Home, MyPlants,
Guides et Map. La SearchBar partagée passe en mode trigger sur ces pages,
ouvrant la modal au tap.
Animation par contexte (native-stack) :
- Home/MyPlants/Guides → 'fade_from_bottom' (fade + léger glissement)
- Map → 'fade' pur (la barre est déjà en haut, pas de mouvement vertical)
via param de route { fromMap: true }
SearchScreen — mode global :
- 3 catégories : Maladies (red), Guides pratiques (blue), Mes plantes (green)
- Filter chips scrollable horizontal : Tout / Maladies / Guides / Plantes
avec count par catégorie
- Sections en accordion (chevron) avec count par section
- Tag coloré sur chaque résultat indiquant la catégorie
- Plantes scannées incluses (recherche par customName / cépage)
- Tap résultat : DiseaseDetail / GuideDetail / Map+focusScanId / ScanDetail
SearchScreen — mode Map (fromMap=true) :
- Liste plate des plantes localisées uniquement
- Distance haversine depuis position courante (formatDistance helper)
- Tri par distance croissante
- Tap → navigate Main/Map avec focusScanId
MapScreen :
- useEffect sur route.params?.focusScanId : animation 2 étapes (zoom large
450ms → zoom serré 500ms) + ouverture du preview
- Caméra décalée vers le sud (lat - delta * 0.18) pour que le marker soit
visible AU-DESSUS du bottom sheet, pas masqué dessous
- Reset du param après usage via setParams
Recents :
- Hook useRecentSearches (AsyncStorage @vineye:recent_searches)
- Max 10, déduplication insensible à la casse
- Affichés quand pas de query, avec clear all + remove individuel
SearchBar partagée :
- Refactorisée 100% Tailwind (sauf 2-3 props RN-spécifiques sur TextInput)
- Nouvelle prop onTriggerPress : devient un Pressable avec Text placeholder
qui navigate vers Search au lieu d'être un input local
Wirings :
- SearchSection (Home + Guides) : trigger
- MyPlantsScreen : trigger (suppression de l'inline filtering, plus utilisé)
- FloatingSearch (Map) : trigger avec fromMap: true
- RootNavigator : Stack.Screen Search avec options dynamique selon param
i18n FR + EN :
- search.{placeholder, placeholderMap, recentTitle, clearAll, noRecent,
resultsTitle, noResults, nearbyPlantsTitle, noPlants}
- search.filter.{all, diseases, guides, plants}
- search.section.{diseases, guides, plants}
- search.tag.{disease, guide, plant}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EditNameBottomSheet :
- Render conditionnel (mounted seulement quand editingName=true) pour ne plus
ouvrir le clavier auto à l'arrivée sur ScanDetail
- BottomSheetScrollView avec contentContainerStyle inline (Tailwind pour le
reste) + insets.bottom pour padding bas
- isDirty disable du Save quand le nom n'a pas changé
- Boutons Annuler/Enregistrer en row via inner View avec icônes X/Check,
font-bold, minHeight 56px
EditProfileModal :
- BottomSheetScrollView : actions déplacées DANS le scroll (juste après le
dernier input email) → toujours visibles sous le formulaire, jamais
poussées en bas du sheet
- snap 95% + topInset safe-area
- Boutons même style (icônes + ghost grisé bordé + primary shadow)
MapBottomSheet rename inline :
- useImperativeHandle pour forwarder le ref correctement et avoir un
internalRef accessible côté composant
- snapToIndex(2) à l'ouverture du form (85%) puis snapToIndex(0) après
save/cancel pour redonner la map
- BottomSheetScrollView pour le rename form avec keyboardShouldPersistTaps
- keyboardBehavior 'interactive' + android_keyboardInputMode 'adjustResize'
- containerStyle { zIndex:100, elevation:100 } pour passer au-dessus des
FloatingActions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remplace le Modal+KeyboardAvoidingView+ScrollView par un BottomSheet
(@gorhom/bottom-sheet) avec snap 95% + topInset safe-area +
keyboardBehavior 'interactive' + android_keyboardInputMode 'adjustResize'.
L'API publique du composant (visible/onClose/onSave) est inchangée.
Boutons Cancel/Save dans le BottomSheetScrollView (juste après le dernier
input email) avec icônes X/Check, layout row via inner View, ghost grisé
bordé + primary shadow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Nouveau composant EditNameBottomSheet (gorhom BottomSheet +
BottomSheetTextInput + BottomSheetScrollView) avec snap 92%, topInset
safe-area, keyboardBehavior interactive, autoFocus
- Mounted conditionnellement (state editingName) pour éviter que l'autoFocus
ouvre le clavier dès l'arrivée sur ScanDetail
- Boutons Annuler / Enregistrer avec icônes X / Check, ghost grisé bordé +
primary shadow, alignés en row via inner View, isDirty disable du Save si
le nom n'a pas changé
- ScanDetailScreen : bouton Pencil flottant à côté du favori, heroTitle
utilise customName en priorité
- useScanDetail : nouvelle méthode renameScan(newName) avec persist storage
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comportement 2-clicks sur la liste des scans :
- 1er clic : map zoom sur le scan + sheet snap au plus bas + tile preview
unique avec hint 'Appuyez à nouveau pour voir les détails'
- 2e clic même scan : navigate vers ScanDetail
- Clic autre scan en preview : switch preview + zoom
Rename inline dans le BottomSheet existant (plus de 2e sheet superposé) :
- Bouton retour (ChevronLeft) au lieu de croix
- Form (BottomSheetTextInput) avec keyboardBehavior='interactive' +
android_keyboardInputMode='adjustResize' pour ne pas masquer l'input
- Boutons Annuler/Enregistrer avec icônes (X / Check), border 1.5px primary,
shadow primary[900] sur le Save, minHeight 56px
- snapToIndex(2) à l'ouverture du form (85%) puis snapToIndex(0) après
save/cancel pour redonner la map à voir
- containerStyle { zIndex:100, elevation:100 } pour passer au-dessus des
FloatingActions quand le sheet s'expand
FloatingActions : couleur d'icône via prop color (className n'est pas
supporté pour la couleur sur lucide-react-native sans cssInterop).
actionsSlot : zIndex/elevation 1 pour passer derrière le sheet expanded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refonte HomeScreen :
- 'Commencer votre collection' (HomeCta) → conditionnellement remplacé par
les 3 derniers scans en mode \"grouped card\" via le nouveau composant
RecentScans (fallback HomeCta si historique vide)
- 'Maladies fréquentes' → SmallDiseaseCard remplacé par LargeDiseaseCard
en mode compact, en scroll horizontal (FrequentDiseasesHorizontal)
- Comment temporairement <SeasonAlert /> en attendant la page Notifications
LargeDiseaseCard : nouvelle prop compact qui réduit hauteur (260→220),
padding, font-sizes et lignes de description (3→2). Border + radius +
shadow Android forcés en style inline pour clip elevation correctement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligne le rendu des date groups sur le pattern PracticalGuides : items
encapsulés dans une card blanche rounded-16, séparés par une ligne grise
indentée.
- ScanListItem : nouvelles props grouped + showSeparator → désactive
borderRadius/margins/border individuels et active la ligne séparatrice
alignée sous le texte
- DateGroupAccordion : wrappe les ScanListItem dans une View card avec
shadow iOS / elevation Android
Le même pattern est réutilisé par RecentScans (Home) — voir commit suivant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extrait la barre de recherche en composant partagé
(components/shared/SearchBar.tsx) avec props placeholder/value/onChangeText/
showFilter.
- Home (SearchSection) : utilise le composant partagé
- Map (FloatingSearch) : remplace l'input custom + ajuste les chips (border
primary, font-size 15→12, MapPin couleur primary)
- MyPlants : remplace l'input custom + son bouton clear
Bonus : SearchBar gère proprement le clavier Android via numberOfLines={1},
multiline={false}, scrollEnabled={false}, lineHeight 20 + textAlignVertical
center + includeFontPadding false → le placeholder ne wrappe plus sur 2 lignes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commente (sans supprimer) les liens et imports de NotificationsScreen :
- HeaderActionButtons : bouton cloche commenté → seul Settings reste
- RootNavigator : import + Stack.Screen commentés
- linking : deep-link 'notifications' commenté
- types/navigation : route param 'Notifications' commenté
À réactiver via la recherche de \"// TODO: réactiver quand la page Notifications\".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConfidenceTile multipliait par 100 une valeur déjà 0-100 (résultat 7000%).
Aligne le composant sur la convention 0-100 utilisée partout dans le projet
(useGameProgress, ScanCard, ScanDetail, ResultScreen, model.ts).
Corrige aussi mockSeed (0.94 → 94, etc.) pour matcher la convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contexte
- Build Android C++ instable sur Windows (CMake/Ninja path too long, Nitro
headers manquants au clean). Modèle .tflite final pas encore prêt.
- Désinstall temporaire des deux libs natives, le mock JS dans model.ts
continue de servir les détections simulées pondérées.
Changements
- package.json : retire react-native-fast-tflite (3.0.1)
- pnpm-lock.yaml : régénéré, -72 packages dont nitro-modules
- src/services/tflite/model.ts : refactor pur mock, interface publique
inchangée (loadModel + runInference), procédure de réintégration
documentée en tête du fichier
- plugins/withCmakeFix.js : plugin Expo config qui injecte les flags
CMake (response files + ninja 1.12.1 + OBJECT_PATH_MAX) à chaque
prebuild — dormant tant que fast-tflite n'est pas réintégré
- app.json : référence le plugin
- CLAUDE.md + .claude/notes/android-build : doc de l'état actuel et
des étapes de réintégration (idéalement via EAS Build)
Reste
- src/assets/models/grapevine_v1.tflite conservé pour la réintégration
- metro.config.js continue de déclarer .tflite dans assetExts
- TypeScript check: 1 erreur préexistante (homeheader.tsx, palette[50]),
non liée à ce changement
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Add mock plants" entry under a new Developer section in the
Settings screen, gated by __DEV__ so it never ships in release. It
calls useHistory.seedTestData() which prepends 5 fake ScanRecords
spread across Bordeaux / Bourgogne / Champagne so all the map
features (region chips, markers, rename) can be exercised without
having to actually walk into a vineyard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the Google-Maps-backed react-native-maps screen with a self-
contained WebView running Leaflet + Carto/OSM tiles. No API key, no
native compilation surface. The map is now driven by the real scan
history (useHistory) instead of mock parcels.
What's on the screen now:
- Markers for every ScanRecord that carries lat/lng, colored by
status (healthy / infected / uncertain) derived from diseaseClass.
- Tapping a marker animates the camera and opens ScanDetail.
- Bottom sheet lists the same located scans with rename support: a
pencil opens a modal-input that calls renameScan() to set
ScanRecord.customName (empty value clears it). When the history is
empty the sheet auto-snaps higher and shows a CTA to the Scanner.
- Region chips (Bordeaux/Bourgogne/Champagne) animate the camera and
draw the actual department polygon as a dashed green outline. The
GeoJSON is fetched on the React Native side (avoids the opaque
origin CORS issue inside `source={{ html }}`) and cached in a
useRef Map.
- "Ma position" filter + Locate FAB drop a circular green pin with a
smiley SVG and a pulse halo at the user's GPS coords.
- FloatingActions and FloatingSearch tags restyled to match the
Apple-inspired Bento spec (rounded-full FABs, 56x56, soft shadows,
primary[900] active state).
VineyardMarker (orphan since markers are SVG inside the WebView) and
the data/mockScans.ts file were removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capture the user's location in parallel with TFLite inference so saving
the scan doesn't slow down the camera flow. Permission is requested
just-in-time on the first capture (not at app boot) and refusals are
surfaced once via toast — repeat refusals stay silent (flag persisted
in AsyncStorage under @vineye:location-permission-asked).
- ScanRecord gains optional latitude / longitude / locationCapturedAt
(plus customName + getScanStatus helper used by the Map screen).
All fields optional so older scans keep working unchanged.
- New useScanLocation hook: requestForegroundPermissionsAsync +
getCurrentPositionAsync(Balanced) with a 5s timeout. On any failure
returns null so the scan still saves without coordinates.
- ScannerScreen runs analyze() and requestAndGetLocation() through
Promise.all so GPS acquisition does not block inference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add VinEye/.npmrc with node-linker=hoisted to avoid Windows MAX_PATH
crashes with .pnpm/<hash>/ deep paths during native compilation
- Replace react-native-maps (Google Maps SDK requires API key) with
react-native-webview (renders Leaflet + OSM tiles, no key needed)
- Add expo-location for GPS capture during scans
- Add expo-dev-client for proper HMR + deep-link launch in dev builds
- Configure expo-location plugin in app.json with FR permission strings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New screens: Guides, Library, Map, Notifications, Settings.
Home refactored with modular components (SearchHeader, HomeCta, FrequentDiseases, SeasonAlert, PracticalGuides).
Replaced hardcoded colors with theme tokens in SeasonAlert and HomeCta.
Updated navigation, i18n, and CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace floating pill tab bar with classic anchored bottom bar (Home | FAB Scan | Map)
- Add central FAB button (green, elevated) for Scanner
- Move History → Notifications and Profile → Settings (accessible via header icons)
- Add SearchHeader with bell (notifications) and settings icons
- Add MapScreen placeholder
- Extract SearchHeader component from HomeScreen
- Switch to lucide-react-native icons for bottom tab bar
- Fix react-dom version mismatch (19.2.4 → 19.1.0)
- Clean up unused imports in homeheader.tsx
- Update navigation types, deep links, and i18n keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>