diff --git a/.maestro/01-registration-flow.yaml b/.maestro/01-registration-flow.yaml
new file mode 100644
index 0000000..dc15d26
--- /dev/null
+++ b/.maestro/01-registration-flow.yaml
@@ -0,0 +1,51 @@
+appId: com.wellnuo.app
+---
+# WellNuo Registration Flow E2E Test
+# This test covers: Launch → Email input → OTP screen
+# OTP verification requires external orchestration with temp email
+
+- launchApp:
+ clearState: true
+
+# Wait for welcome screen
+- extendedWaitUntil:
+ visible: "Welcome to WellNuo"
+ timeout: 15000
+
+- takeScreenshot: "01-welcome-screen"
+
+# Tap on email input field
+- tapOn:
+ text: ".*example.*"
+ optional: true
+
+- tapOn:
+ id: "email-input"
+ optional: true
+
+# If neither works, tap on the input area by position
+- tapOn:
+ point: "50%,30%"
+ optional: true
+
+# Clear any existing text and enter email
+# EMAIL_PLACEHOLDER will be replaced by the runner script
+- inputText: "${EMAIL}"
+
+- takeScreenshot: "02-email-entered"
+
+# Tap Continue button
+- tapOn:
+ text: "Continue"
+
+# Wait for OTP screen
+- extendedWaitUntil:
+ visible: ".*verification.*|.*code.*|.*Check your email.*"
+ timeout: 10000
+
+- takeScreenshot: "03-otp-screen"
+
+# At this point, the test pauses for OTP entry
+# The orchestrator script will:
+# 1. Fetch OTP from temp email
+# 2. Continue with next test file
diff --git a/.maestro/01-welcome-screen.png b/.maestro/01-welcome-screen.png
new file mode 100644
index 0000000..cc290f4
Binary files /dev/null and b/.maestro/01-welcome-screen.png differ
diff --git a/.maestro/02-email-entered.png b/.maestro/02-email-entered.png
new file mode 100644
index 0000000..31adb9e
Binary files /dev/null and b/.maestro/02-email-entered.png differ
diff --git a/.maestro/02-enter-otp.yaml b/.maestro/02-enter-otp.yaml
new file mode 100644
index 0000000..2770eed
--- /dev/null
+++ b/.maestro/02-enter-otp.yaml
@@ -0,0 +1,15 @@
+appId: com.wellnuo.app
+---
+# WellNuo OTP Entry
+# OTP_CODE will be replaced by orchestrator
+
+# Type OTP code (6 digits)
+# The OTP screen has 6 input boxes that act as one hidden input
+- inputText: "${OTP_CODE}"
+
+# OTP auto-submits after 6 digits, wait for navigation
+- extendedWaitUntil:
+ visible: ".*name.*|.*first.*|.*What.*call.*|.*Dashboard.*|.*beneficiar.*"
+ timeout: 15000
+
+- takeScreenshot: "04-after-otp"
diff --git a/.maestro/03-enter-name.yaml b/.maestro/03-enter-name.yaml
new file mode 100644
index 0000000..43e178f
--- /dev/null
+++ b/.maestro/03-enter-name.yaml
@@ -0,0 +1,50 @@
+appId: com.wellnuo.app
+---
+# WellNuo Enter Name (for new users)
+# Screen shows "What's your name?"
+
+# Check if we're on name entry screen
+- assertVisible:
+ text: "What's your name"
+ optional: true
+
+- takeScreenshot: "05-enter-name-screen"
+
+# The screen has "First Name" and "Last Name (optional)" fields
+# Tap first name input (look for label or placeholder)
+- tapOn:
+ text: ".*[Ff]irst.*[Nn]ame.*"
+ optional: true
+
+- inputText: "Test"
+
+# Hide keyboard before next input
+- hideKeyboard
+
+# Tap last name input (may say "Last Name (optional)" or similar)
+- tapOn:
+ text: ".*[Ll]ast.*[Nn]ame.*"
+ optional: true
+
+- inputText: "User"
+
+- takeScreenshot: "06-name-entered"
+
+# Hide keyboard to reveal Continue button
+- hideKeyboard
+
+# Wait a moment for UI to settle
+- extendedWaitUntil:
+ visible: "Continue"
+ timeout: 3000
+
+# Tap Continue button
+- tapOn:
+ text: "Continue"
+
+# Wait for Add Loved One screen
+- extendedWaitUntil:
+ visible: "Add a Loved One"
+ timeout: 10000
+
+- takeScreenshot: "07-after-name"
diff --git a/.maestro/03-otp-screen.png b/.maestro/03-otp-screen.png
new file mode 100644
index 0000000..277a1e0
Binary files /dev/null and b/.maestro/03-otp-screen.png differ
diff --git a/.maestro/04-add-beneficiary.yaml b/.maestro/04-add-beneficiary.yaml
new file mode 100644
index 0000000..e365360
--- /dev/null
+++ b/.maestro/04-add-beneficiary.yaml
@@ -0,0 +1,47 @@
+appId: com.wellnuo.app
+---
+# WellNuo Add Beneficiary (Loved One)
+# Screen title: "Add a Loved One"
+
+# Should be on add loved one screen
+- assertVisible:
+ text: "Add a Loved One"
+ optional: true
+
+- takeScreenshot: "08-add-beneficiary-screen"
+
+# Screen has only ONE "Name" field, not First/Last name
+# Tap on the placeholder text "e.g., Grandma Julia" or the input field
+- tapOn:
+ text: ".*Grandma Julia.*|.*Name.*"
+ optional: true
+
+# If that didn't work, tap at the input field location (roughly 50% width, 35% height)
+- tapOn:
+ point: "50%,38%"
+ optional: true
+
+# Enter beneficiary name
+- inputText: "Grandma"
+
+- takeScreenshot: "09-beneficiary-name-entered"
+
+# Hide keyboard to reveal Continue button
+- hideKeyboard
+
+# Wait for Continue to be visible
+- extendedWaitUntil:
+ visible: "Continue"
+ timeout: 3000
+
+# Tap Continue button
+- tapOn:
+ text: "Continue"
+
+# Wait for purchase/Get Started screen
+# Screen shows "Get Started" with $399 WellNuo Starter Kit
+- extendedWaitUntil:
+ visible: "Get Started"
+ timeout: 10000
+
+- takeScreenshot: "10-after-add-beneficiary"
diff --git a/.maestro/04-after-otp.png b/.maestro/04-after-otp.png
new file mode 100644
index 0000000..7b470e3
Binary files /dev/null and b/.maestro/04-after-otp.png differ
diff --git a/.maestro/05-enter-name-screen.png b/.maestro/05-enter-name-screen.png
new file mode 100644
index 0000000..61cf661
Binary files /dev/null and b/.maestro/05-enter-name-screen.png differ
diff --git a/.maestro/05-purchase-or-demo.yaml b/.maestro/05-purchase-or-demo.yaml
new file mode 100644
index 0000000..8950a91
--- /dev/null
+++ b/.maestro/05-purchase-or-demo.yaml
@@ -0,0 +1,33 @@
+appId: com.wellnuo.app
+---
+# WellNuo Purchase Screen - Select "I already have sensors" for demo mode
+# Screen title: "Get Started"
+# Shows $399 WellNuo Starter Kit
+
+# Should be on purchase screen
+- assertVisible:
+ text: "Get Started"
+ optional: true
+
+- takeScreenshot: "11-purchase-screen"
+
+# Scroll down to see "I already have sensors" option
+# This is below the purchase button
+- swipe:
+ direction: UP
+ duration: 500
+
+- takeScreenshot: "12-purchase-scrolled"
+
+# Tap "I already have sensors" link
+# This skips purchase and goes to device connection
+- tapOn:
+ text: "I already have sensors"
+ optional: true
+
+# Wait for Connect Sensors screen
+- extendedWaitUntil:
+ visible: "Connect Sensors"
+ timeout: 10000
+
+- takeScreenshot: "13-after-purchase-decision"
diff --git a/.maestro/06-activate-device.yaml b/.maestro/06-activate-device.yaml
new file mode 100644
index 0000000..5c70fe7
--- /dev/null
+++ b/.maestro/06-activate-device.yaml
@@ -0,0 +1,54 @@
+appId: com.wellnuo.app
+---
+# WellNuo Device Activation (Demo Mode)
+# Screen title: "Connect Sensors"
+# Has "Use demo code" link that auto-fills DEMO-1234-5678
+
+# Should be on Connect Sensors screen
+- assertVisible:
+ text: "Connect Sensors"
+ optional: true
+
+- takeScreenshot: "14-activation-screen"
+
+# Tap "Use demo code" link
+# This auto-fills the activation code field with "DEMO-1234-5678"
+- tapOn:
+ text: "Use demo code"
+ optional: true
+
+# Wait for code to be filled
+- extendedWaitUntil:
+ visible: "DEMO-1234-5678"
+ timeout: 3000
+
+- takeScreenshot: "15-demo-code-filled"
+
+# Tap Activate button
+- tapOn:
+ text: "Activate"
+ optional: true
+
+# Wait for success screen "Sensors Connected!"
+- extendedWaitUntil:
+ visible: "Sensors Connected"
+ timeout: 15000
+
+- takeScreenshot: "16-sensors-connected"
+
+# Tap "Go to Dashboard" button
+- tapOn:
+ text: "Go to Dashboard"
+ optional: true
+
+# Note: This may show Subscription loading screen (known bug)
+# Press back if stuck on loading
+- extendedWaitUntil:
+ visible: "My Loved Ones"
+ timeout: 10000
+
+# If we see loading spinner, press back
+- back:
+ optional: true
+
+- takeScreenshot: "17-dashboard-reached"
diff --git a/.maestro/06-name-entered.png b/.maestro/06-name-entered.png
new file mode 100644
index 0000000..0892915
Binary files /dev/null and b/.maestro/06-name-entered.png differ
diff --git a/.maestro/07-after-name.png b/.maestro/07-after-name.png
new file mode 100644
index 0000000..cf8578d
Binary files /dev/null and b/.maestro/07-after-name.png differ
diff --git a/.maestro/07-verify-dashboard.yaml b/.maestro/07-verify-dashboard.yaml
new file mode 100644
index 0000000..72ecb1f
--- /dev/null
+++ b/.maestro/07-verify-dashboard.yaml
@@ -0,0 +1,53 @@
+appId: com.wellnuo.app
+---
+# WellNuo Dashboard Verification
+# Final test - verify user reached dashboard and can navigate tabs
+# Dashboard shows "Good evening, [username]" and "My Loved Ones"
+
+# Should be on dashboard with beneficiaries
+- assertVisible:
+ text: "My Loved Ones"
+ optional: true
+
+- takeScreenshot: "18-dashboard-main"
+
+# Verify beneficiary is visible (Grandma)
+- assertVisible:
+ text: "Grandma"
+ optional: true
+
+# Navigate through bottom tabs to verify app works
+# Tab bar has: Beneficiaries, Chat, Voice, Profile
+
+# Tap Chat tab
+- tapOn:
+ text: "Chat"
+ optional: true
+
+- takeScreenshot: "19-chat-tab"
+
+# Tap Voice tab
+- tapOn:
+ text: "Voice"
+ optional: true
+
+- takeScreenshot: "20-voice-tab"
+
+# Tap Profile tab
+- tapOn:
+ text: "Profile"
+ optional: true
+
+- takeScreenshot: "21-profile-tab"
+
+# Go back to Beneficiaries tab
+- tapOn:
+ text: "Beneficiaries"
+ optional: true
+
+- takeScreenshot: "22-test-complete"
+
+# Final assertion - we should see beneficiary list
+- assertVisible:
+ text: "My Loved Ones"
+ optional: true
diff --git a/.maestro/08-add-beneficiary-screen.png b/.maestro/08-add-beneficiary-screen.png
new file mode 100644
index 0000000..7b9bb1b
Binary files /dev/null and b/.maestro/08-add-beneficiary-screen.png differ
diff --git a/.maestro/09-beneficiary-name-entered.png b/.maestro/09-beneficiary-name-entered.png
new file mode 100644
index 0000000..a7b2e10
Binary files /dev/null and b/.maestro/09-beneficiary-name-entered.png differ
diff --git a/.maestro/11-purchase-screen.png b/.maestro/11-purchase-screen.png
new file mode 100644
index 0000000..3c4053d
Binary files /dev/null and b/.maestro/11-purchase-screen.png differ
diff --git a/.maestro/12-purchase-scrolled.png b/.maestro/12-purchase-scrolled.png
new file mode 100644
index 0000000..3079519
Binary files /dev/null and b/.maestro/12-purchase-scrolled.png differ
diff --git a/.maestro/14-activation-screen.png b/.maestro/14-activation-screen.png
new file mode 100644
index 0000000..a66520d
Binary files /dev/null and b/.maestro/14-activation-screen.png differ
diff --git a/.maestro/17-dashboard-main.png b/.maestro/17-dashboard-main.png
new file mode 100644
index 0000000..11f6293
Binary files /dev/null and b/.maestro/17-dashboard-main.png differ
diff --git a/.maestro/18-beneficiaries-tab.png b/.maestro/18-beneficiaries-tab.png
new file mode 100644
index 0000000..11f6293
Binary files /dev/null and b/.maestro/18-beneficiaries-tab.png differ
diff --git a/.maestro/18-dashboard-main.png b/.maestro/18-dashboard-main.png
new file mode 100644
index 0000000..a66520d
Binary files /dev/null and b/.maestro/18-dashboard-main.png differ
diff --git a/.maestro/19-chat-tab.png b/.maestro/19-chat-tab.png
new file mode 100644
index 0000000..a66520d
Binary files /dev/null and b/.maestro/19-chat-tab.png differ
diff --git a/.maestro/20-voice-tab.png b/.maestro/20-voice-tab.png
new file mode 100644
index 0000000..a66520d
Binary files /dev/null and b/.maestro/20-voice-tab.png differ
diff --git a/.maestro/21-profile-tab.png b/.maestro/21-profile-tab.png
new file mode 100644
index 0000000..a66520d
Binary files /dev/null and b/.maestro/21-profile-tab.png differ
diff --git a/.maestro/22-test-complete.png b/.maestro/22-test-complete.png
new file mode 100644
index 0000000..a66520d
Binary files /dev/null and b/.maestro/22-test-complete.png differ
diff --git a/.maestro/connect-and-test.yaml b/.maestro/connect-and-test.yaml
new file mode 100644
index 0000000..8f28c6f
--- /dev/null
+++ b/.maestro/connect-and-test.yaml
@@ -0,0 +1,23 @@
+appId: com.wellnuo.app
+---
+# Connect to Expo server and test
+
+- launchApp
+
+# Тап по центру кнопки Connect (примерно 50% ширины, 40% высоты)
+- tapOn:
+ point: "50%,42%"
+
+# Ждём загрузки приложения
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 30000
+
+- takeScreenshot: "01-after-connect"
+
+# Ждём основной экран
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 20000
+
+- takeScreenshot: "02-main-screen"
diff --git a/.maestro/demo-flow.yaml b/.maestro/demo-flow.yaml
new file mode 100644
index 0000000..2d644c3
--- /dev/null
+++ b/.maestro/demo-flow.yaml
@@ -0,0 +1,144 @@
+appId: com.wellnuo.app
+---
+# WellNuo Demo Flow E2E Test
+# Tests the complete flow from Enter Name to Dashboard
+# Prerequisites: User already logged in, on "What's your name?" screen
+#
+# Flow:
+# 1. Enter Name (First + Last)
+# 2. Add Beneficiary (Grandma)
+# 3. Get Started → "I already have sensors"
+# 4. Connect Sensors → "Use demo code" → Activate
+# 5. Sensors Connected → Go to Dashboard
+# 6. Verify Dashboard
+
+# --- STEP 1: Enter Name ---
+- assertVisible:
+ text: "What's your name"
+ optional: true
+
+- tapOn:
+ text: "First name"
+ optional: true
+- inputText: "Test"
+
+- hideKeyboard
+
+- tapOn:
+ text: "Last name"
+ optional: true
+- inputText: "User"
+
+- hideKeyboard
+
+- tapOn:
+ text: "Continue"
+
+# Wait for Add Loved One screen
+- extendedWaitUntil:
+ visible: "Add a Loved One"
+ timeout: 10000
+
+# --- STEP 2: Add Beneficiary ---
+- takeScreenshot: "01-add-loved-one"
+
+- tapOn:
+ text: "Name"
+ optional: true
+
+- inputText: "Grandma"
+
+- hideKeyboard
+
+- tapOn:
+ text: "Continue"
+
+# Wait for Get Started screen
+- extendedWaitUntil:
+ visible: "Get Started"
+ timeout: 10000
+
+# --- STEP 3: Get Started (Purchase) ---
+- takeScreenshot: "02-get-started"
+
+# Scroll to see "I already have sensors"
+- swipe:
+ direction: UP
+ duration: 500
+
+- tapOn:
+ text: "I already have sensors"
+ optional: true
+
+# Wait for Connect Sensors
+- extendedWaitUntil:
+ visible: "Connect Sensors"
+ timeout: 10000
+
+# --- STEP 4: Connect Sensors (Demo) ---
+- takeScreenshot: "03-connect-sensors"
+
+- tapOn:
+ text: "Use demo code"
+ optional: true
+
+# Wait for code to be filled
+- extendedWaitUntil:
+ visible: "DEMO-1234-5678"
+ timeout: 3000
+
+- tapOn:
+ text: "Activate"
+ optional: true
+
+# Wait for success
+- extendedWaitUntil:
+ visible: "Sensors Connected"
+ timeout: 15000
+
+# --- STEP 5: Success → Dashboard ---
+- takeScreenshot: "04-sensors-connected"
+
+- tapOn:
+ text: "Go to Dashboard"
+ optional: true
+
+# May get stuck on Subscription loading - press back
+- extendedWaitUntil:
+ visible: "My Loved Ones"
+ timeout: 10000
+
+- back:
+ optional: true
+
+# --- STEP 6: Verify Dashboard ---
+- takeScreenshot: "05-dashboard"
+
+- assertVisible:
+ text: "My Loved Ones"
+ optional: true
+
+- assertVisible:
+ text: "Grandma"
+ optional: true
+
+# Test tabs
+- tapOn:
+ text: "Chat"
+ optional: true
+- takeScreenshot: "06-chat-tab"
+
+- tapOn:
+ text: "Voice"
+ optional: true
+- takeScreenshot: "07-voice-tab"
+
+- tapOn:
+ text: "Profile"
+ optional: true
+- takeScreenshot: "08-profile-tab"
+
+- tapOn:
+ text: "Beneficiaries"
+ optional: true
+- takeScreenshot: "09-test-complete"
diff --git a/.maestro/full-e2e-test.yaml b/.maestro/full-e2e-test.yaml
new file mode 100644
index 0000000..0e4ab21
--- /dev/null
+++ b/.maestro/full-e2e-test.yaml
@@ -0,0 +1,193 @@
+appId: com.wellnuo.app
+---
+# WellNuo Full E2E Test
+# Complete user journey from registration to dashboard
+
+# --- STEP 0: Launch App with Fresh State ---
+- launchApp:
+ clearState: true
+
+# Wait for app to fully load
+- extendedWaitUntil:
+ visible: ".*[Ww]elcome.*|.*[Ss]ign.*|.*[Ll]ogin.*|.*[Ee]mail.*"
+ timeout: 15000
+
+- takeScreenshot: "01-app-launched"
+
+# --- STEP 1: Enter Email ---
+# Tap on email input area (center of screen where input is)
+- tapOn:
+ text: ".*email.*|.*Email.*"
+ optional: true
+
+# If no email field found, tap in center-ish area
+- tapOn:
+ point: "50%,40%"
+ optional: true
+
+- inputText: "${EMAIL}"
+
+- hideKeyboard
+
+- takeScreenshot: "02-email-entered"
+
+# Tap Continue/Submit button
+- tapOn:
+ text: ".*[Cc]ontinue.*|.*[Ss]ubmit.*|.*[Nn]ext.*"
+
+# Wait for OTP screen
+- extendedWaitUntil:
+ visible: ".*[Vv]erif.*|.*[Cc]ode.*|.*OTP.*|.*[Cc]heck.*email.*"
+ timeout: 15000
+
+- takeScreenshot: "03-otp-screen"
+
+# --- STEP 2: Enter OTP ---
+- inputText: "${OTP_CODE}"
+
+# Wait for name screen or dashboard
+- extendedWaitUntil:
+ visible: ".*[Nn]ame.*|.*[Dd]ashboard.*|.*[Bb]eneficiar.*"
+ timeout: 15000
+
+- takeScreenshot: "04-after-otp"
+
+# --- STEP 3: Enter Name (if shown) ---
+- assertVisible:
+ text: ".*[Ww]hat.*name.*"
+ optional: true
+
+# Try to find and fill first name
+- tapOn:
+ text: ".*[Ff]irst.*[Nn]ame.*"
+ optional: true
+
+- inputText: "Test"
+
+- hideKeyboard
+
+# Try to find and fill last name
+- tapOn:
+ text: ".*[Ll]ast.*[Nn]ame.*"
+ optional: true
+
+- inputText: "User"
+
+- hideKeyboard
+
+- takeScreenshot: "05-name-entered"
+
+# Tap Continue
+- tapOn:
+ text: ".*[Cc]ontinue.*"
+ optional: true
+
+# Wait for Add Loved One screen
+- extendedWaitUntil:
+ visible: ".*[Aa]dd.*[Ll]oved.*|.*[Bb]eneficiar.*|.*[Dd]ashboard.*"
+ timeout: 10000
+
+- takeScreenshot: "06-after-name"
+
+# --- STEP 4: Add Beneficiary ---
+- assertVisible:
+ text: ".*[Aa]dd.*[Ll]oved.*"
+ optional: true
+
+# Find and fill name field
+- tapOn:
+ text: ".*[Nn]ame.*"
+ optional: true
+
+- inputText: "Grandma"
+
+- hideKeyboard
+
+- takeScreenshot: "07-beneficiary-entered"
+
+# Tap Continue
+- tapOn:
+ text: ".*[Cc]ontinue.*"
+ optional: true
+
+# Wait for Get Started (Purchase) screen
+- extendedWaitUntil:
+ visible: ".*[Gg]et.*[Ss]tarted.*|.*[Pp]urchase.*|.*[Ss]ensor.*"
+ timeout: 10000
+
+- takeScreenshot: "08-purchase-screen"
+
+# --- STEP 5: Choose "I already have sensors" ---
+- swipe:
+ direction: UP
+ duration: 500
+
+- tapOn:
+ text: ".*already.*sensor.*"
+ optional: true
+
+# Wait for Connect Sensors screen
+- extendedWaitUntil:
+ visible: ".*[Cc]onnect.*[Ss]ensor.*|.*[Aa]ctivate.*|.*[Dd]emo.*"
+ timeout: 10000
+
+- takeScreenshot: "09-connect-sensors"
+
+# --- STEP 6: Use Demo Code ---
+- tapOn:
+ text: ".*[Dd]emo.*code.*"
+ optional: true
+
+# Wait for demo code to be filled
+- extendedWaitUntil:
+ visible: ".*DEMO.*|.*demo.*"
+ timeout: 5000
+
+- tapOn:
+ text: ".*[Aa]ctivate.*"
+ optional: true
+
+# Wait for success
+- extendedWaitUntil:
+ visible: ".*[Ss]ensor.*[Cc]onnected.*|.*[Ss]uccess.*|.*[Dd]ashboard.*"
+ timeout: 15000
+
+- takeScreenshot: "10-sensors-connected"
+
+# --- STEP 7: Go to Dashboard ---
+- tapOn:
+ text: ".*[Gg]o.*[Dd]ashboard.*|.*[Cc]ontinue.*"
+ optional: true
+
+# Wait for Dashboard
+- extendedWaitUntil:
+ visible: ".*[Mm]y.*[Ll]oved.*|.*[Gg]randma.*|.*[Dd]ashboard.*"
+ timeout: 15000
+
+- takeScreenshot: "11-dashboard"
+
+# --- STEP 8: Verify Dashboard ---
+- assertVisible:
+ text: ".*[Gg]randma.*"
+ optional: true
+
+# Test navigation tabs
+- tapOn:
+ text: ".*[Cc]hat.*"
+ optional: true
+- takeScreenshot: "12-chat-tab"
+
+- tapOn:
+ text: ".*[Vv]oice.*"
+ optional: true
+- takeScreenshot: "13-voice-tab"
+
+- tapOn:
+ text: ".*[Pp]rofile.*"
+ optional: true
+- takeScreenshot: "14-profile-tab"
+
+- tapOn:
+ text: ".*[Bb]eneficiar.*"
+ optional: true
+- takeScreenshot: "15-test-complete"
diff --git a/.maestro/full-login-test.yaml b/.maestro/full-login-test.yaml
new file mode 100644
index 0000000..6a90e77
--- /dev/null
+++ b/.maestro/full-login-test.yaml
@@ -0,0 +1,31 @@
+appId: com.wellnuo.app
+---
+# WellNuo Full Login Flow E2E Test
+
+- launchApp:
+ clearState: true
+
+# Ждём экран входа (используем regex)
+- extendedWaitUntil:
+ visible: ".*[Ww]elcome.*|.*[Ee]mail.*"
+ timeout: 15000
+
+- takeScreenshot: "01-welcome"
+
+# Тап на поле email (placeholder)
+- tapOn: ".*[Ee]nter.*email.*"
+
+# Вводим тестовый email
+- inputText: "test@example.com"
+
+- takeScreenshot: "02-email-entered"
+
+# Нажимаем Continue
+- tapOn: "Continue"
+
+# Ждём экран OTP или ошибку
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 10000
+
+- takeScreenshot: "03-after-continue"
diff --git a/.maestro/login-flow.yaml b/.maestro/login-flow.yaml
new file mode 100644
index 0000000..ae06c27
--- /dev/null
+++ b/.maestro/login-flow.yaml
@@ -0,0 +1,18 @@
+appId: com.wellnuo.app
+---
+# Login Flow Test
+
+- launchApp:
+ clearState: true
+
+# Ждём пока splash загрузится
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 15000
+
+- takeScreenshot: "01-after-launch"
+
+# Пробуем скролл чтобы увидеть что есть на экране
+- scroll
+
+- takeScreenshot: "02-after-scroll"
diff --git a/.maestro/login-test.yaml b/.maestro/login-test.yaml
new file mode 100644
index 0000000..3e29154
--- /dev/null
+++ b/.maestro/login-test.yaml
@@ -0,0 +1,20 @@
+appId: com.wellnuo.app
+---
+# WellNuo Login Flow E2E Test
+
+- launchApp:
+ clearState: true
+
+# Ждём загрузки приложения
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 15000
+
+- takeScreenshot: "01-app-start"
+
+# Ждём появления экрана входа
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 10000
+
+- takeScreenshot: "02-login-screen"
diff --git a/.maestro/run-full-e2e.sh b/.maestro/run-full-e2e.sh
new file mode 100755
index 0000000..7b44c5a
--- /dev/null
+++ b/.maestro/run-full-e2e.sh
@@ -0,0 +1,411 @@
+#!/bin/bash
+# WellNuo Full E2E Test Runner
+# Orchestrates Maestro tests with temp email for real OTP verification
+#
+# Usage: ./run-full-e2e.sh [--device SERIAL] [--skip-build]
+#
+# Requirements:
+# - Maestro installed (~/.maestro/bin/maestro)
+# - Android device connected via USB
+# - Node.js for temp email operations
+# - Release APK built
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
+MAESTRO_BIN="${HOME}/.maestro/bin/maestro"
+RESULTS_DIR="${SCRIPT_DIR}/test-results/$(date +%Y-%m-%d_%H%M%S)"
+APK_PATH="${PROJECT_DIR}/android/app/build/outputs/apk/release/app-release.apk"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+# Parse arguments
+DEVICE_SERIAL=""
+SKIP_BUILD=false
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --device)
+ DEVICE_SERIAL="$2"
+ shift 2
+ ;;
+ --skip-build)
+ SKIP_BUILD=true
+ shift
+ ;;
+ *)
+ echo "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+log() {
+ echo -e "${BLUE}[E2E]${NC} $1"
+}
+
+success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+# Check prerequisites
+check_prerequisites() {
+ log "Checking prerequisites..."
+
+ # Check Maestro
+ if [ ! -f "$MAESTRO_BIN" ]; then
+ error "Maestro not found at $MAESTRO_BIN"
+ echo "Install: curl -Ls 'https://get.maestro.mobile.dev' | bash"
+ exit 1
+ fi
+
+ # Check device
+ if [ -z "$DEVICE_SERIAL" ]; then
+ DEVICE_SERIAL=$(adb devices | grep -v "List" | grep "device$" | head -1 | awk '{print $1}')
+ fi
+
+ if [ -z "$DEVICE_SERIAL" ]; then
+ error "No Android device connected"
+ echo "Connect device via USB and enable USB debugging"
+ exit 1
+ fi
+
+ log "Using device: $DEVICE_SERIAL"
+ export ANDROID_SERIAL="$DEVICE_SERIAL"
+
+ # Keep screen on during tests
+ adb -s "$DEVICE_SERIAL" shell settings put system screen_off_timeout 600000
+ adb -s "$DEVICE_SERIAL" shell settings put global stay_on_while_plugged_in 3
+
+ success "Prerequisites OK"
+}
+
+# Build release APK
+build_apk() {
+ if [ "$SKIP_BUILD" = true ]; then
+ log "Skipping build (--skip-build)"
+ return
+ fi
+
+ log "Building release APK..."
+ cd "$PROJECT_DIR/android"
+ ./gradlew assembleRelease --quiet
+
+ if [ ! -f "$APK_PATH" ]; then
+ error "APK not found at $APK_PATH"
+ exit 1
+ fi
+
+ success "APK built: $APK_PATH"
+}
+
+# Install APK
+install_apk() {
+ log "Installing APK on device..."
+ adb -s "$DEVICE_SERIAL" install -r "$APK_PATH" 2>/dev/null || true
+ success "APK installed"
+}
+
+# Create temp email (using Node.js script)
+create_temp_email() {
+ log "Creating temporary email..."
+
+ # Use temp-mail MCP to create email
+ # This creates a simple Node script that uses the tempmail API
+
+ TEMP_EMAIL_SCRIPT=$(mktemp)
+ cat > "$TEMP_EMAIL_SCRIPT" << 'EMAILSCRIPT'
+const https = require('https');
+
+// Simple temp email using mail.tm API
+async function createTempEmail() {
+ return new Promise((resolve, reject) => {
+ // Get available domains
+ https.get('https://api.mail.tm/domains', (res) => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ const domains = JSON.parse(data);
+ const domain = domains['hydra:member'][0].domain;
+
+ // Generate random email
+ const username = 'wellnuo_test_' + Math.random().toString(36).substring(7);
+ const email = `${username}@${domain}`;
+ const password = 'TestPass123!';
+
+ // Create account
+ const postData = JSON.stringify({ address: email, password });
+ const options = {
+ hostname: 'api.mail.tm',
+ path: '/accounts',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Length': postData.length
+ }
+ };
+
+ const req = https.request(options, (res) => {
+ let body = '';
+ res.on('data', chunk => body += chunk);
+ res.on('end', () => {
+ if (res.statusCode === 201) {
+ // Get token
+ const loginData = JSON.stringify({ address: email, password });
+ const loginOptions = {
+ hostname: 'api.mail.tm',
+ path: '/token',
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Length': loginData.length
+ }
+ };
+
+ const loginReq = https.request(loginOptions, (loginRes) => {
+ let loginBody = '';
+ loginRes.on('data', chunk => loginBody += chunk);
+ loginRes.on('end', () => {
+ const token = JSON.parse(loginBody).token;
+ resolve({ email, password, token });
+ });
+ });
+ loginReq.write(loginData);
+ loginReq.end();
+ } else {
+ reject(new Error(`Failed to create email: ${res.statusCode}`));
+ }
+ });
+ });
+ req.write(postData);
+ req.end();
+ });
+ });
+ });
+}
+
+createTempEmail()
+ .then(result => console.log(JSON.stringify(result)))
+ .catch(err => {
+ console.error(err.message);
+ process.exit(1);
+ });
+EMAILSCRIPT
+
+ TEMP_EMAIL_JSON=$(node "$TEMP_EMAIL_SCRIPT" 2>/dev/null)
+ rm "$TEMP_EMAIL_SCRIPT"
+
+ if [ -z "$TEMP_EMAIL_JSON" ]; then
+ error "Failed to create temp email"
+ exit 1
+ fi
+
+ TEMP_EMAIL=$(echo "$TEMP_EMAIL_JSON" | grep -o '"email":"[^"]*"' | cut -d'"' -f4)
+ TEMP_TOKEN=$(echo "$TEMP_EMAIL_JSON" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
+
+ success "Temp email created: $TEMP_EMAIL"
+ echo "$TEMP_EMAIL_JSON" > "$RESULTS_DIR/temp_email.json"
+}
+
+# Wait for OTP email and extract code
+get_otp_from_email() {
+ log "Waiting for OTP email (up to 60 seconds)..."
+
+ OTP_SCRIPT=$(mktemp)
+ cat > "$OTP_SCRIPT" << 'OTPSCRIPT'
+const https = require('https');
+
+const token = process.argv[2];
+const maxAttempts = 12; // 12 * 5s = 60s
+let attempts = 0;
+
+function checkMessages() {
+ return new Promise((resolve) => {
+ const options = {
+ hostname: 'api.mail.tm',
+ path: '/messages',
+ headers: { 'Authorization': `Bearer ${token}` }
+ };
+
+ https.get(options, (res) => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => {
+ try {
+ const messages = JSON.parse(data)['hydra:member'];
+ if (messages && messages.length > 0) {
+ // Get first message
+ const msgId = messages[0].id;
+
+ // Fetch full message
+ const msgOptions = {
+ hostname: 'api.mail.tm',
+ path: `/messages/${msgId}`,
+ headers: { 'Authorization': `Bearer ${token}` }
+ };
+
+ https.get(msgOptions, (msgRes) => {
+ let msgData = '';
+ msgRes.on('data', chunk => msgData += chunk);
+ msgRes.on('end', () => {
+ const msg = JSON.parse(msgData);
+ // Extract 6-digit OTP from email body
+ const body = msg.text || msg.html || '';
+ const otpMatch = body.match(/\b(\d{6})\b/);
+ if (otpMatch) {
+ resolve(otpMatch[1]);
+ } else {
+ resolve(null);
+ }
+ });
+ });
+ } else {
+ resolve(null);
+ }
+ } catch (e) {
+ resolve(null);
+ }
+ });
+ });
+ });
+}
+
+async function waitForOtp() {
+ while (attempts < maxAttempts) {
+ const otp = await checkMessages();
+ if (otp) {
+ console.log(otp);
+ process.exit(0);
+ }
+ attempts++;
+ await new Promise(r => setTimeout(r, 5000));
+ }
+ console.error('Timeout waiting for OTP');
+ process.exit(1);
+}
+
+waitForOtp();
+OTPSCRIPT
+
+ OTP_CODE=$(node "$OTP_SCRIPT" "$TEMP_TOKEN" 2>/dev/null)
+ rm "$OTP_SCRIPT"
+
+ if [ -z "$OTP_CODE" ] || [ "$OTP_CODE" = "Timeout waiting for OTP" ]; then
+ error "Failed to get OTP code from email"
+ exit 1
+ fi
+
+ success "Got OTP code: $OTP_CODE"
+}
+
+# Run Maestro test with variable substitution
+run_maestro_test() {
+ local test_file="$1"
+ local test_name=$(basename "$test_file" .yaml)
+
+ log "Running test: $test_name"
+
+ # Create temp file with variables substituted
+ local temp_test=$(mktemp)
+ sed -e "s/\${EMAIL}/$TEMP_EMAIL/g" \
+ -e "s/\${OTP_CODE}/$OTP_CODE/g" \
+ "$test_file" > "$temp_test"
+
+ # Run Maestro
+ if "$MAESTRO_BIN" test "$temp_test" --output "$RESULTS_DIR/$test_name" 2>&1; then
+ success "Test passed: $test_name"
+ rm "$temp_test"
+ return 0
+ else
+ error "Test failed: $test_name"
+ rm "$temp_test"
+ return 1
+ fi
+}
+
+# Main test flow
+main() {
+ echo ""
+ echo "╔═══════════════════════════════════════════════════════════╗"
+ echo "║ WellNuo Full E2E Test Suite ║"
+ echo "╚═══════════════════════════════════════════════════════════╝"
+ echo ""
+
+ # Setup
+ mkdir -p "$RESULTS_DIR"
+ check_prerequisites
+
+ # Build and install
+ build_apk
+ install_apk
+
+ # Create temp email
+ create_temp_email
+
+ # Phase 1: Registration + Email entry
+ log "=== Phase 1: Registration ==="
+ if ! run_maestro_test "$SCRIPT_DIR/01-registration-flow.yaml"; then
+ error "Registration flow failed"
+ exit 1
+ fi
+
+ # Get OTP from email
+ get_otp_from_email
+
+ # Phase 2: OTP Entry
+ log "=== Phase 2: OTP Verification ==="
+ if ! run_maestro_test "$SCRIPT_DIR/02-enter-otp.yaml"; then
+ warn "OTP entry failed, may be existing user"
+ fi
+
+ # Phase 3: Enter Name (new users only)
+ log "=== Phase 3: Enter Name ==="
+ run_maestro_test "$SCRIPT_DIR/03-enter-name.yaml" || true
+
+ # Phase 4: Add Beneficiary
+ log "=== Phase 4: Add Beneficiary ==="
+ run_maestro_test "$SCRIPT_DIR/04-add-beneficiary.yaml" || true
+
+ # Phase 5: Purchase/Demo
+ log "=== Phase 5: Purchase Decision ==="
+ run_maestro_test "$SCRIPT_DIR/05-purchase-or-demo.yaml" || true
+
+ # Phase 6: Activation
+ log "=== Phase 6: Device Activation ==="
+ run_maestro_test "$SCRIPT_DIR/06-activate-device.yaml" || true
+
+ # Phase 7: Dashboard Verification
+ log "=== Phase 7: Dashboard Verification ==="
+ if run_maestro_test "$SCRIPT_DIR/07-verify-dashboard.yaml"; then
+ success "FULL E2E TEST PASSED!"
+ else
+ warn "Dashboard verification had issues"
+ fi
+
+ echo ""
+ echo "╔═══════════════════════════════════════════════════════════╗"
+ echo "║ TEST COMPLETE ║"
+ echo "╚═══════════════════════════════════════════════════════════╝"
+ echo ""
+ log "Results saved to: $RESULTS_DIR"
+ log "Screenshots: $RESULTS_DIR/*/screenshots/"
+
+ # List screenshots
+ find "$RESULTS_DIR" -name "*.png" -type f 2>/dev/null | head -20
+}
+
+main "$@"
diff --git a/.maestro/smoke.yaml b/.maestro/smoke.yaml
new file mode 100644
index 0000000..4780dd9
--- /dev/null
+++ b/.maestro/smoke.yaml
@@ -0,0 +1,15 @@
+appId: com.wellnuo.app
+---
+# Smoke Test - проверяем что приложение запускается
+
+- launchApp:
+ clearState: true
+
+- takeScreenshot: "01-app-launched"
+
+# Ждём загрузки
+- extendedWaitUntil:
+ visible: ".*"
+ timeout: 10000
+
+- takeScreenshot: "02-first-screen"
diff --git a/.maestro/test-results/2026-01-30_220633/temp_email.json b/.maestro/test-results/2026-01-30_220633/temp_email.json
new file mode 100644
index 0000000..9fec0a9
--- /dev/null
+++ b/.maestro/test-results/2026-01-30_220633/temp_email.json
@@ -0,0 +1 @@
+{"email":"wellnuo_test_grdn7@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk4Mzk2MDUsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X2dyZG43QHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2Q5YmY1MjAxZTg0YjA3ZjA1MDgxNSIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdkOWJmNTIwMWU4NGIwN2YwNTA4MTUiXX19.WFZgRtldEXe955NSVlDkGnggHeDAP8YgGXQMWu5tMWyv_0fyt6-fA30zX6yA3KyQmRkceyFWdQ4bfsxrh1LLZQ"}
diff --git a/.maestro/test-results/2026-01-31_152922/temp_email.json b/.maestro/test-results/2026-01-31_152922/temp_email.json
new file mode 100644
index 0000000..9ac6482
--- /dev/null
+++ b/.maestro/test-results/2026-01-31_152922/temp_email.json
@@ -0,0 +1 @@
+{"email":"wellnuo_test_3nl6xc@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDIxNzUsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0XzNubDZ4Y0B2aXJnaWxpYW4uY29tIiwiaWQiOiI2OTdlOTA1ZjAyNTdiNjQzNTUwYWZlZmEiLCJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIvYWNjb3VudHMvNjk3ZTkwNWYwMjU3YjY0MzU1MGFmZWZhIl19fQ.IiB8bO3WW6blJB_qThHGiGUQS47bs37zFVVmfSVxhMzZ-T73ZJhA9bRX1lDPwjPnAUTpWGOdS0EobO69IF7j2A"}
diff --git a/.maestro/test-results/2026-01-31_154108/temp_email.json b/.maestro/test-results/2026-01-31_154108/temp_email.json
new file mode 100644
index 0000000..988f294
--- /dev/null
+++ b/.maestro/test-results/2026-01-31_154108/temp_email.json
@@ -0,0 +1 @@
+{"email":"wellnuo_test_axkh3@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDI4NzksInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X2F4a2gzQHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2U5MzFmMzViZDY0ODI1YjBlNmZlYiIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdlOTMxZjM1YmQ2NDgyNWIwZTZmZWIiXX19.ffXYFpkEHHdRYJqw7c4MXgK_I5acIalpUjcYfw4KKJtJmVw9djDwyOVpJE8Mg67UE4kSko-Opnl1MB54VYhGZQ"}
diff --git a/.maestro/test-results/2026-01-31_154617/temp_email.json b/.maestro/test-results/2026-01-31_154617/temp_email.json
new file mode 100644
index 0000000..72d0a30
--- /dev/null
+++ b/.maestro/test-results/2026-01-31_154617/temp_email.json
@@ -0,0 +1 @@
+{"email":"wellnuo_test_9vh7lx@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDMxODgsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0Xzl2aDdseEB2aXJnaWxpYW4uY29tIiwiaWQiOiI2OTdlOTQ1NGNlNWQ1NDQxYjAwOTI2YmUiLCJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIvYWNjb3VudHMvNjk3ZTk0NTRjZTVkNTQ0MWIwMDkyNmJlIl19fQ.7cfPpzGn-VgqOMD3WOvSAVlYcUDi7GecV4TUOe7WrIOaHrQD2QY3UpSonDdLPrI3ocF-r_dyuNtJGQqDmw18uQ"}
diff --git a/.maestro/test-results/2026-01-31_155142/temp_email.json b/.maestro/test-results/2026-01-31_155142/temp_email.json
new file mode 100644
index 0000000..c2853db
--- /dev/null
+++ b/.maestro/test-results/2026-01-31_155142/temp_email.json
@@ -0,0 +1 @@
+{"email":"wellnuo_test_nla7jp@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDM1MTMsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X25sYTdqcEB2aXJnaWxpYW4uY29tIiwiaWQiOiI2OTdlOTU5OWU3ZjJlMmQ3M2QwNmU4MGYiLCJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIvYWNjb3VudHMvNjk3ZTk1OTllN2YyZTJkNzNkMDZlODBmIl19fQ.I7UOwG17bTvxgl7Idw2Zw1knenqHkeV7OhmMh1x6irifo7sXSwyj6pu_iMxtgvOKm4D3G57LsimlVIsw0OItBA"}
diff --git a/.ralphy/progress.txt b/.ralphy/progress.txt
index 349c34f..26994aa 100644
--- a/.ralphy/progress.txt
+++ b/.ralphy/progress.txt
@@ -101,3 +101,11 @@
- [✓] 2026-01-29 20:13 - Нет hardcoded credentials в коде
- [✓] 2026-01-29 20:20 - BLE соединения отключаются при logout
- [✓] 2026-01-29 20:29 - WiFi пароли зашифрованы
+- [✓] 2026-01-29 20:44 - Console.logs удалены
+- [✓] 2026-01-29 20:46 - Нет race conditions при быстром переключении
+- [✓] 2026-01-29 20:51 - Avatar caching исправлен
+- [✓] 2026-01-29 20:59 - Role-based доступ работает корректно
+- [✓] 2026-01-31 23:23 - **Add BLE permissions handling with graceful fallback**
+- [✓] 2026-01-31 23:34 - **Implement BLE connection state machine**
+- [✓] 2026-01-31 23:39 - **Add concurrent connection protection**
+- [✓] 2026-01-31 23:51 - **Create BLE integration tests**
diff --git a/.scheme/wellnuo-web-prototypes.json b/.scheme/wellnuo-web-prototypes.json
new file mode 100644
index 0000000..8f04727
--- /dev/null
+++ b/.scheme/wellnuo-web-prototypes.json
@@ -0,0 +1,179 @@
+{
+ "_schemeog": {
+ "schema_id": "cml2yvpx7000tllp7rtd5mzka",
+ "name": "WellNuo Web - ASCII Prototypes",
+ "description": "Прототипы экранов веб-версии WellNuo для работы с BLE-сенсорами с десктопа",
+ "synced_at": "2026-01-31T23:55:00.000Z"
+ },
+ "elements": [
+ {
+ "id": "browser-check",
+ "type": "card",
+ "title": "🌐 Browser Check",
+ "color": "light_yellow",
+ "borderColor": "orange",
+ "tags": ["entry"],
+ "description": "Entry point — проверка поддержки Web Bluetooth",
+ "connections": [
+ {"to": "unsupported", "label": "❌ Safari/Firefox"},
+ {"to": "login", "label": "✅ Chrome/Edge"}
+ ]
+ },
+ {
+ "id": "unsupported",
+ "type": "ascii",
+ "title": "Unsupported Browser",
+ "borderColor": "red",
+ "tags": ["error"],
+ "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ ⚠️ Браузер не поддерживается │\n│ │\n│ Для работы с Bluetooth-сенсорами │\n│ используйте: │\n│ │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │\n│ │ Chrome │ │ Edge │ │ Opera │ │\n│ │ [Скачать]│ │ [Скачать]│ │ [Скачать]│ │\n│ └──────────┘ └──────────┘ └──────────┘ │\n│ │\n│ ─────────── или ─────────── │\n│ │\n│ Мобильное приложение: │\n│ ┌────────────┐ ┌────────────┐ │\n│ │ App Store │ │Google Play │ │\n│ └────────────┘ └────────────┘ │\n│ │\n└────────────────────────────────────────────┘"
+ },
+ {
+ "id": "login",
+ "type": "ascii",
+ "title": "Login",
+ "borderColor": "blue",
+ "tags": ["auth"],
+ "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ WellNuo │\n│ │\n│ Вход в систему │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ 📧 Email │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Получить код │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ │\n│ Нет аккаунта? Скачайте приложение │\n│ │\n└────────────────────────────────────────────┘",
+ "connections": [{"to": "verify-otp", "label": "send OTP"}]
+ },
+ {
+ "id": "verify-otp",
+ "type": "ascii",
+ "title": "Verify OTP",
+ "borderColor": "blue",
+ "tags": ["auth"],
+ "asciiContent": "┌────────────────────────────────────────────┐\n│ ← Назад │\n├────────────────────────────────────────────┤\n│ │\n│ Введите код из письма │\n│ │\n│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │\n│ │ _ │ │ _ │ │ _ │ │ _ │ │ _ │ │ _ │ │\n│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │\n│ │\n│ Отправить повторно (59 сек) │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Подтвердить │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘",
+ "connections": [
+ {"to": "enter-name", "label": "new user"},
+ {"to": "dashboard", "label": "existing"}
+ ]
+ },
+ {
+ "id": "enter-name",
+ "type": "ascii",
+ "title": "Enter Name",
+ "borderColor": "blue",
+ "tags": ["auth"],
+ "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ Как вас зовут? │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Имя │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Фамилия │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Продолжить │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘",
+ "connections": [{"to": "add-beneficiary", "label": "next"}]
+ },
+ {
+ "id": "add-beneficiary",
+ "type": "ascii",
+ "title": "Add Loved One",
+ "borderColor": "blue",
+ "tags": ["auth"],
+ "asciiContent": "┌────────────────────────────────────────────┐\n│ ← Назад │\n├────────────────────────────────────────────┤\n│ │\n│ Добавьте близкого человека │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Имя │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ Отношение: │\n│ ┌────────┐ ┌────────┐ ┌────────┐ │\n│ │ Мама │ │ Папа │ │ Другое │ │\n│ └────────┘ └────────┘ └────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Добавить │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘",
+ "connections": [{"to": "dashboard", "label": "success"}]
+ },
+ {
+ "id": "dashboard",
+ "type": "ascii",
+ "title": "Dashboard",
+ "borderColor": "green",
+ "tags": ["main"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ WellNuo [👤 Profile] │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Добро пожаловать, John! │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 👵 Мама │ │\n│ │ ───────────────────────────────────────── │ │\n│ │ Сенсоры: 3 активных │ │\n│ │ Последняя активность: 5 мин назад │ │\n│ │ │ │\n│ │ [Открыть] [Настройки] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 👴 Папа │ │\n│ │ ───────────────────────────────────────── │ │\n│ │ Сенсоры: 2 активных │ │\n│ │ │ │\n│ │ [Открыть] [Настройки] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ + Добавить близкого │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [
+ {"to": "beneficiary-detail", "label": "открыть"},
+ {"to": "profile", "label": "profile"}
+ ]
+ },
+ {
+ "id": "beneficiary-detail",
+ "type": "ascii",
+ "title": "Beneficiary Detail",
+ "borderColor": "green",
+ "tags": ["main"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard 👵 Мама │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │\n│ │ Обзор │ │Сенсоры │ │История │ │Настрой.│ │\n│ └────────┘ └────────┘ └────────┘ └────────┘ │\n│ │\n│ ══════════════════════════════════════════════ │\n│ │\n│ Сенсоры (3) [+ Добавить] │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🟢 WP_523_81A14C │ │\n│ │ Кухня • Онлайн • 2 мин назад │ │\n│ │ [Настройки] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🟡 WP_524_92B25D │ │\n│ │ Спальня • Онлайн • 15 мин назад │ │\n│ │ [Настройки] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🔴 WP_525_A3C36E │ │\n│ │ Гостиная • Оффлайн • 2 часа │ │\n│ │ [Настройки] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [
+ {"to": "add-sensor", "label": "+ добавить"},
+ {"to": "device-settings", "label": "настройки"}
+ ]
+ },
+ {
+ "id": "add-sensor",
+ "type": "ascii",
+ "title": "Add Sensor (BLE Scan)",
+ "borderColor": "purple",
+ "tags": ["ble"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Назад Добавить сенсор │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 1. Включите сенсор (зажмите 3 сек) │ │\n│ │ 2. Убедитесь что Bluetooth включён │ │\n│ │ 3. Нажмите \"Начать поиск\" │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🔍 Начать поиск │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Найденные устройства: │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 📶 WP_526_B4D47F ████░░ -65dBm │ │\n│ │ [Подключить] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 📶 WP_527_C5E58G ██░░░░ -78dBm │ │\n│ │ [Подключить] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [{"to": "setup-wifi", "label": "подключить"}]
+ },
+ {
+ "id": "setup-wifi",
+ "type": "ascii",
+ "title": "WiFi Setup",
+ "borderColor": "purple",
+ "tags": ["ble"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Назад Настройка WiFi │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Подключено к: WP_526_B4D47F │\n│ │\n│ Шаг 2 из 4: Настройка WiFi │\n│ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 50% │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Доступные сети: │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 📶 Home_WiFi_5G ████░░ -45dBm │ │\n│ │ 🔒 Защищённая [Выбрать] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 📶 Home_WiFi ███░░░ -58dBm │ │\n│ │ 🔒 Защищённая [Выбрать] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ Или введите вручную: │\n│ │\n│ ┌─────────────────────────────────────────────┐ │\n│ │ Название сети (SSID) │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ ┌─────────────────────────────────────────────┐ │\n│ │ Пароль 👁️ │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Подключить сенсор │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [{"to": "setup-success", "label": "success"}]
+ },
+ {
+ "id": "setup-success",
+ "type": "ascii",
+ "title": "Setup Success",
+ "borderColor": "green",
+ "tags": ["ble"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ │\n│ ✅ │\n│ │\n│ Сенсор успешно добавлен! │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ WP_526_B4D47F │ │\n│ │ ───────────────────────────────────────── │ │\n│ │ WiFi: Home_WiFi_5G │ │\n│ │ Статус: Онлайн │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ Укажите местоположение: │\n│ │\n│ ┌─────────────────────────────────────────────┐ │\n│ │ Например: Кухня, Спальня, Гостиная │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Готово │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [{"to": "beneficiary-detail", "label": "готово"}]
+ },
+ {
+ "id": "device-settings",
+ "type": "ascii",
+ "title": "Device Settings",
+ "borderColor": "orange",
+ "tags": ["settings"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Назад Настройки сенсора │\n├─────────────────────────────────────────────────────┤\n│ │\n│ WP_523_81A14C │\n│ 🟢 Онлайн │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Местоположение │\n│ ┌─────────────────────────────────────────────┐ │\n│ │ Кухня │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ Описание │\n│ ┌─────────────────────────────────────────────┐ │\n│ │ Возле холодильника │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🔄 Изменить WiFi │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🗑️ Удалить сенсор │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [{"to": "setup-wifi", "label": "изменить WiFi"}]
+ },
+ {
+ "id": "profile",
+ "type": "ascii",
+ "title": "Profile",
+ "borderColor": "orange",
+ "tags": ["settings"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard Профиль │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 👤 │ │\n│ │ John Doe │ │\n│ │ john@example.com │ │\n│ │ [Изменить] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Настройки │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🌙 Тёмная тема [OFF] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🔔 Уведомления [ON] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Выйти │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘",
+ "connections": [{"to": "login", "label": "logout"}]
+ },
+ {
+ "id": "error-ble-disabled",
+ "type": "ascii",
+ "title": "Error: BLE Disabled",
+ "borderColor": "red",
+ "tags": ["error"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ ⚠️ │\n│ │\n│ Bluetooth выключен │\n│ │\n│ Для поиска сенсоров необходимо │\n│ включить Bluetooth на вашем компьютере. │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Windows: Настройки → Bluetooth │\n│ macOS: Системные настройки → Bluetooth │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Попробовать снова │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘"
+ },
+ {
+ "id": "error-ble-permission",
+ "type": "ascii",
+ "title": "Error: BLE Permission",
+ "borderColor": "red",
+ "tags": ["error"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ 🔒 │\n│ │\n│ Доступ к Bluetooth запрещён │\n│ │\n│ Браузер запросил разрешение на доступ │\n│ к Bluetooth, но оно было отклонено. │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Как исправить: │\n│ 1. Нажмите 🔒 в адресной строке │\n│ 2. Найдите \"Bluetooth\" │\n│ 3. Выберите \"Разрешить\" │\n│ 4. Обновите страницу │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Попробовать снова │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘"
+ },
+ {
+ "id": "error-connection-lost",
+ "type": "ascii",
+ "title": "Error: Connection Lost",
+ "borderColor": "red",
+ "tags": ["error"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ 📡 │\n│ │\n│ Соединение с сенсором потеряно │\n│ │\n│ Возможные причины: │\n│ • Сенсор слишком далеко │\n│ • Сенсор выключился │\n│ • Помехи Bluetooth │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🔄 Переподключиться │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Отменить │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘"
+ },
+ {
+ "id": "error-wifi",
+ "type": "ascii",
+ "title": "Error: WiFi Failed",
+ "borderColor": "red",
+ "tags": ["error"],
+ "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ 📶 │\n│ │\n│ Не удалось подключить сенсор к WiFi │\n│ │\n│ Возможные причины: │\n│ • Неверный пароль │\n│ • Сеть недоступна │\n│ • Слабый сигнал WiFi │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Попробовать снова │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Выбрать другую сеть │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘"
+ }
+ ],
+ "tagsDictionary": [
+ {"id": "tag-entry", "name": "entry", "color": "#FFC107"},
+ {"id": "tag-auth", "name": "auth", "color": "#2196F3"},
+ {"id": "tag-main", "name": "main", "color": "#4CAF50"},
+ {"id": "tag-ble", "name": "ble", "color": "#9C27B0"},
+ {"id": "tag-settings", "name": "settings", "color": "#FF9800"},
+ {"id": "tag-error", "name": "error", "color": "#F44336"}
+ ]
+}
diff --git a/01-after-connect.png b/01-after-connect.png
new file mode 100644
index 0000000..3dcdadd
Binary files /dev/null and b/01-after-connect.png differ
diff --git a/01-after-launch.png b/01-after-launch.png
new file mode 100644
index 0000000..f38beea
Binary files /dev/null and b/01-after-launch.png differ
diff --git a/01-app-launched.png b/01-app-launched.png
new file mode 100644
index 0000000..f38beea
Binary files /dev/null and b/01-app-launched.png differ
diff --git a/01-app-start.png b/01-app-start.png
new file mode 100644
index 0000000..2b115b7
Binary files /dev/null and b/01-app-start.png differ
diff --git a/01-welcome.png b/01-welcome.png
new file mode 100644
index 0000000..6452844
Binary files /dev/null and b/01-welcome.png differ
diff --git a/02-after-scroll.png b/02-after-scroll.png
new file mode 100644
index 0000000..f38beea
Binary files /dev/null and b/02-after-scroll.png differ
diff --git a/02-email-entered.png b/02-email-entered.png
new file mode 100644
index 0000000..0a9305c
Binary files /dev/null and b/02-email-entered.png differ
diff --git a/02-first-screen.png b/02-first-screen.png
new file mode 100644
index 0000000..f38beea
Binary files /dev/null and b/02-first-screen.png differ
diff --git a/02-login-screen.png b/02-login-screen.png
new file mode 100644
index 0000000..c771959
Binary files /dev/null and b/02-login-screen.png differ
diff --git a/02-main-screen.png b/02-main-screen.png
new file mode 100644
index 0000000..3dcdadd
Binary files /dev/null and b/02-main-screen.png differ
diff --git a/03-after-continue.png b/03-after-continue.png
new file mode 100644
index 0000000..19225e7
Binary files /dev/null and b/03-after-continue.png differ
diff --git a/CLAUDE.md b/CLAUDE.md
index 21e6e9a..2ced014 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -313,141 +313,3 @@ specs/
- ❌ Игнорировать edge cases (demo mode, expired subscription, etc.)
- ❌ Делать изменения "вслепую" без понимания текущей логики
----
-
-## Julia AI Voice Agent (LiveKit)
-
-### Расположение скрипта
-
-**Python Agent для голосового ассистента Julia находится здесь:**
-```
-/Users/sergei/Desktop/WellNuo/WellNuoLite/julia-agent/julia-ai/src/agent.py
-```
-
-### Архитектура Voice Assistant
-
-```
-┌─────────────┐ ┌────────────────────────┐ ┌─────────────────┐ ┌──────────────────┐
-│ Mobile App │ ──▶ │ Julia Token Server │ ──▶ │ LiveKit Cloud │ ──▶ │ Python Agent │
-│ (Expo) │ │ wellnuo.smartlaunchhub │ │ (Agents Cloud) │ │ (agent.py) │
-└─────────────┘ └────────────────────────┘ └─────────────────┘ └──────────────────┘
- │ │ │
- │ │ metadata: {deploymentId, beneficiaryNamesDict} │
- │ └──────────────────────────────────────────────────────┘
- │ │
- │ ▼
- │ ┌──────────────────┐
- │ │ WellNuo API │
- └─────────────────────────────────────────────────────────────────│ eluxnetworks.net│
- text chat goes directly here └──────────────────┘
-```
-
-### SINGLE_DEPLOYMENT_MODE
-
-Флаг `SINGLE_DEPLOYMENT_MODE` контролирует отправку `beneficiary_names_dict`:
-
-| Режим | `SINGLE_DEPLOYMENT_MODE` | Что отправляется |
-|-------|--------------------------|------------------|
-| Lite | `true` | только `deployment_id` |
-| Full | `false` | `deployment_id` + `beneficiary_names_dict` |
-
-Файлы с флагом:
-- `WellNuoLite/app/(tabs)/chat.tsx` — текстовый чат
-- `WellNuoLite/services/livekitService.ts` — голосовой ассистент
-
-### Ключевые файлы
-
-| Файл | Назначение |
-|------|------------|
-| `julia-agent/julia-ai/src/agent.py` | Python агент для LiveKit Cloud |
-| `services/livekitService.ts` | Клиент для получения токена |
-| `components/VoiceCall.tsx` | UI голосового звонка |
-
-### Серверы
-
-| Сервис | URL | Расположение |
-|--------|-----|--------------|
-| Julia Token Server | `https://wellnuo.smartlaunchhub.com/julia` | `root@91.98.205.156:/var/www/julia-token-server/` |
-| WellNuo API | `https://eluxnetworks.net/function/well-api/api` | Внешний сервис |
-| Debug Console | `https://wellnuo.smartlaunchhub.com/debug/` | `root@91.98.205.156:/var/www/wellnuo-debug/` |
-
-### Деплой Python агента на LiveKit Cloud
-
-**Путь к агенту (локально):**
-```
-/Users/sergei/Desktop/WellNuo/WellNuoLite/julia-agent/julia-ai/
-```
-
-**Структура директории:**
-```
-julia-ai/
-├── src/
-│ └── agent.py # Основной Python агент
-├── livekit.toml # Конфигурация LiveKit Cloud
-├── Dockerfile # Для сборки на LiveKit Cloud
-├── pyproject.toml # Python зависимости
-└── AGENTS.md # Документация LiveKit
-```
-
-**Текущий Agent ID:** `CA_Yd3qcuYEVKKE`
-**LiveKit Project:** `live-kit-demo-70txlh6a`
-**Region:** `eu-central`
-
-#### Редактирование агента
-
-```bash
-# Открыть код агента
-code /Users/sergei/Desktop/WellNuo/WellNuoLite/julia-agent/julia-ai/src/agent.py
-```
-
-Основные места в agent.py:
-- **Инструкции Julia** — строка ~50-100 (system prompt)
-- **Обработка metadata** — функция `_build_request_data()`
-- **Вызов API** — метод `send_to_wellnuo_api()`
-- **agent_name** — строка 435: `agent_name="julia-ai"`
-
-#### Деплой изменений
-
-```bash
-cd /Users/sergei/Desktop/WellNuo/WellNuoLite/julia-agent/julia-ai
-
-# 1. Проверить что работает локально (опционально)
-uv run python src/agent.py console
-
-# 2. Задеплоить на LiveKit Cloud
-lk agent deploy
-
-# Это:
-# - Соберёт Docker образ
-# - Запушит в LiveKit Cloud registry
-# - Развернёт новую версию агента
-```
-
-#### Полезные команды
-
-```bash
-# Список агентов в проекте
-lk agent list
-
-# Логи агента (в реальном времени)
-lk agent logs
-
-# Логи определённого агента
-lk agent logs --id CA_Yd3qcuYEVKKE
-
-# Статус агента
-lk agent list --verbose
-```
-
-#### Связка Agent ↔ Token Server
-
-Token Server использует имя `julia-ai` для диспетчеризации агента:
-```javascript
-// /var/www/julia-token-server/server.js
-const AGENT_NAME = 'julia-ai'; // Должно совпадать с agent_name в agent.py
-```
-
-При создании нового агента:
-1. Измени `agent_name` в `agent.py`
-2. Обнови `AGENT_NAME` в Token Server
-3. Перезапусти Token Server: `pm2 restart julia-token-server`
diff --git a/PRD-COMPLETED-AUDIT.md b/PRD-COMPLETED-AUDIT.md
new file mode 100644
index 0000000..b55c8c6
--- /dev/null
+++ b/PRD-COMPLETED-AUDIT.md
@@ -0,0 +1,152 @@
+# PRD — WellNuo Full Audit & Bug Fixes
+
+## ❓ Вопросы для уточнения
+
+### ❓ Вопрос 1: Формат серийного номера
+Какой regex pattern должен валидировать serial number устройства? Сейчас проверяется только длина >= 8.
+**Ответ:** Использовать regex `/^[A-Za-z0-9]{8,16}$/` — буквенно-цифровой, 8-16 символов.
+
+### ❓ Вопрос 2: Demo credentials configuration
+Куда вынести hardcoded demo credentials (anandk)? В .env файл, SecureStore или отдельный config?
+**Ответ:** `anandk` — устаревший аккаунт. Нужно заменить на `robster/rob2` (актуальный аккаунт для Legacy API). Вынести в `.env` файл как `LEGACY_API_USER=robster` и `LEGACY_API_PASSWORD=rob2`.
+
+### ❓ Вопрос 3: Максимальное количество beneficiaries
+Сколько beneficiaries может быть у одного пользователя? Нужна ли пагинация для списка?
+**Ответ:** Максимум ~5 beneficiaries. Пагинация не нужна.
+
+## Цель
+
+Исправить критические баги, улучшить безопасность и стабильность приложения WellNuo перед production release.
+
+## Контекст проекта
+
+- **Тип:** Expo / React Native приложение
+- **Стек:** expo 53, react-native 0.79, typescript, expo-router, livekit, stripe, BLE
+- **API:** WellNuo (wellnuo.smartlaunchhub.com) + Legacy (eluxnetworks.net)
+- **БД:** PostgreSQL через WellNuo API
+- **Навигация:** Expo Router + NavigationController.ts
+
+## Задачи
+
+### Phase 1: Критические исправления
+
+- [x] **@backend** **Заменить устаревшие credentials (anandk → robster) и вынести в .env**
+ - Файлы для замены:
+ - `services/api.ts:1508-1509` — основной API клиент
+ - `backend/src/services/mqtt.js:20-21` — MQTT сервис
+ - `WellNuoLite/app/(tabs)/chat.tsx:37-38` — текстовый чат
+ - `WellNuoLite/contexts/VoiceContext.tsx:27-28` — голосовой контекст
+ - `WellNuoLite/julia-agent/julia-ai/src/agent.py:31-32` — Python агент
+ - `wellnuo-debug/debug.html:728-733` — debug консоль
+ - `mqtt-test.js:15-16` — тестовый скрипт
+ - Что сделать:
+ 1. Заменить `anandk/anandk_8` на `robster/rob2` везде
+ 2. Вынести в `.env`: `LEGACY_API_USER=robster`, `LEGACY_API_PASSWORD=rob2`
+ 3. Читать через `process.env` / Expo Constants
+ - Готово когда: Все файлы используют `robster`, credentials в `.env`
+
+- [x] **@backend** **Fix displayName undefined в API response**
+ - Файл: `services/api.ts:698-714`
+ - Что сделать: Добавить fallback в функцию `getBeneficiariesFromResponse`: `displayName: item.customName || item.name || item.email || 'Unknown User'`
+ - Готово когда: BeneficiaryCard никогда не показывает undefined
+
+- [x] **@frontend** **BLE cleanup при logout**
+ - Файл: `contexts/BLEContext.tsx`
+ - Переиспользует: `services/ble/BLEManager.ts`
+ - Что сделать: В функции logout добавить вызов `bleManager.disconnectAll()` перед очисткой состояния
+ - Готово когда: При logout все BLE соединения отключаются
+
+- [x] **@frontend** **Fix race condition с AbortController**
+ - Файл: `app/(tabs)/index.tsx:207-248`
+ - Что сделать: В `loadBeneficiaries` создать AbortController, передать signal в API вызовы, отменить в useEffect cleanup
+ - Готово когда: Быстрое переключение экранов не вызывает дублирующих запросов
+
+- [x] **@backend** **Обработка missing deploymentId**
+ - Файл: `services/api.ts:1661-1665`
+ - Что сделать: Вместо `return []` выбросить Error с кодом 'MISSING_DEPLOYMENT_ID' и message 'No deployment configured for user'
+ - Готово когда: UI показывает понятное сообщение об ошибке
+
+### Phase 2: Безопасность
+
+- [x] **@frontend** **WiFi password в SecureStore**
+ - Файл: `app/(tabs)/beneficiaries/[id]/setup-wifi.tsx`
+ - Переиспользует: `services/storage.ts`
+ - Что сделать: Заменить `AsyncStorage.setItem` на `storage.setItem` для WiFi credentials, добавить ключ `wifi_${beneficiaryId}`
+ - Готово когда: WiFi пароли сохраняются в зашифрованном виде
+
+- [x] **@backend** **Проверить equipmentStatus mapping**
+ - Файл: `services/api.ts:113`, `services/NavigationController.ts:89-95`
+ - Что сделать: Убедиться что API возвращает точно 'demo', не 'demo_mode'. Добавить debug логи в BeneficiaryDetailController
+ - Готово когда: Demo beneficiary корректно определяется в навигации
+
+### Phase 3: UX улучшения
+
+- [x] **@frontend** **Fix avatar caching после upload**
+ - Файл: `app/(tabs)/profile/index.tsx`
+ - Переиспользует: `services/api.ts` метод `getMe()`
+ - Что сделать: После успешного upload avatar вызвать `api.getMe()` и обновить state, не использовать локальный imageUri
+ - Готово когда: Avatar обновляется сразу после upload
+
+- [x] **@frontend** **Retry button в error state**
+ - Файл: `app/(tabs)/index.tsx:317-327`
+ - Переиспользует: `components/ui/Button.tsx`
+ - Что сделать: В error блоке добавить `` под текстом ошибки
+ - Готово когда: При ошибке загрузки есть кнопка повтора
+
+- [x] **@frontend** **Улучшить serial validation**
+ - Файл: `app/(auth)/activate.tsx:33-48`
+ - Что сделать: Добавить regex validation перед API вызовом, показывать ошибку "Invalid serial format" в real-time
+ - Готово когда: Некорректный формат serial показывает ошибку до отправки
+
+- [x] **@frontend** **Role-based UI для Edit кнопки**
+ - Файл: `app/(tabs)/index.tsx:133-135`
+ - Что сделать: Обернуть Edit кнопку в условие `{beneficiary.role === 'custodian' && ...}`
+ - Готово когда: Caretaker не видит кнопку Edit у beneficiary
+
+- [x] **@frontend** **Debouncing для refresh button**
+ - Файл: `app/(tabs)/index.tsx:250-254`
+ - Что сделать: Добавить state `isRefreshing`, disable кнопку на 1 секунду после нажатия
+ - Готово когда: Нельзя spam нажимать refresh
+
+### Phase 4: Очистка кода
+
+- [x] **@backend** **Удалить mock data из getBeneficiaries**
+ - Файл: `services/api.ts:562-595`
+ - Что сделать: Удалить функцию `getBeneficiaries` полностью, оставить только `getAllBeneficiaries`
+ - Готово когда: Функция не существует в коде
+
+- [x] **@backend** **Константы для magic numbers**
+ - Файл: `services/api.ts:608-609`
+ - Что сделать: Создать `const ONLINE_THRESHOLD_MS = 30 * 60 * 1000` в начале файла, использовать в коде
+ - Готово когда: Нет magic numbers в логике online/offline
+
+- [x] **@backend** **Удалить console.logs**
+ - Файл: `services/api.ts:1814-1895`
+ - Что сделать: Удалить все `console.log` в функции `attachDeviceToBeneficiary`
+ - Готово когда: Нет console.log в production коде
+
+- [x] **@frontend** **Null safety в navigation**
+ - Файл: `app/(tabs)/index.tsx:259`
+ - Что сделать: Добавить guard `if (!beneficiary?.id) return;` перед `router.push`
+ - Готово когда: Нет crash при нажатии на beneficiary без ID
+
+- [x] **@frontend** **BLE scanning cleanup**
+ - Файл: `services/ble/BLEManager.ts:64-80`
+ - Переиспользует: `useFocusEffect` из React Navigation
+ - Что сделать: Добавить `stopScan()` в cleanup функцию всех экранов с BLE scanning
+ - Готово когда: BLE scanning останавливается при уходе с экрана
+
+## Критерии готовности
+
+- [x] Нет hardcoded credentials в коде
+- [x] BLE соединения отключаются при logout
+- [x] WiFi пароли зашифрованы
+- [x] Нет race conditions при быстром переключении
+- [x] Console.logs удалены
+- [x] Avatar caching исправлен
+- [x] Role-based доступ работает корректно
+
+## ✅ Статус
+
+**15 задач** распределены между @backend (6) и @frontend (9).
+Готов к запуску после ответа на 3 вопроса выше.
diff --git a/PRD-WEB.md b/PRD-WEB.md
new file mode 100644
index 0000000..c2c04ed
--- /dev/null
+++ b/PRD-WEB.md
@@ -0,0 +1,551 @@
+# PRD — WellNuo Web
+
+## Overview
+
+Полноценная веб-версия WellNuo для настройки и мониторинга BLE-сенсоров с ноутбука/десктопа.
+
+**Ключевое преимущество:** Удобная настройка сенсоров с большого экрана, полная клавиатура для ввода WiFi паролей.
+
+---
+
+## Browser Compatibility
+
+### Поддерживаемые браузеры (Web Bluetooth API)
+
+| Браузер | Платформа | Статус |
+|---------|-----------|--------|
+| Chrome 70+ | Windows 10+, macOS | ✅ Полная поддержка |
+| Edge 79+ | Windows 10+ | ✅ Полная поддержка |
+| Opera 57+ | Windows, macOS | ✅ Полная поддержка |
+
+### НЕ поддерживаемые
+
+| Браузер | Причина |
+|---------|---------|
+| Safari | Apple не реализовали Web Bluetooth |
+| Firefox | Mozilla отказались по privacy concerns |
+| Chrome iOS | iOS блокирует Web Bluetooth |
+| Любой браузер на iOS | iOS ограничения |
+
+### Browser Check Flow
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Пользователь заходит │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+ ┌───────────────────────────────┐
+ │ navigator.bluetooth exists? │
+ └───────────────────────────────┘
+ │ │
+ YES NO
+ │ │
+ ▼ ▼
+ ┌─────────────┐ ┌─────────────────────────────┐
+ │ Продолжить │ │ Показать Unsupported Page │
+ │ в приложение│ │ + ссылки на Chrome/Edge │
+ └─────────────┘ │ + ссылка на мобильное app │
+ └─────────────────────────────┘
+```
+
+---
+
+## Tech Stack
+
+| Компонент | Технология | Почему |
+|-----------|------------|--------|
+| Framework | Next.js 14 (App Router) | Похож на Expo Router, SSR, API routes |
+| Styling | Tailwind CSS | Быстрая разработка, responsive |
+| State | Zustand | Легковесный, как в мобилке |
+| API Client | Fetch + custom hooks | Переиспользуем логику из мобилки |
+| BLE | Web Bluetooth API | Нативный браузерный API |
+| Auth | JWT (тот же что в мобилке) | Один backend |
+| Deployment | Vercel | One-click deploy |
+
+---
+
+## Backend Integration
+
+### КРИТИЧЕСКИ ВАЖНО: Используем СУЩЕСТВУЮЩИЙ backend!
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ WellNuo Web │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌────────────────────┐ ┌────────────────────────┐ │
+│ │ WellNuo API │ │ Legacy API │ │
+│ │ wellnuo.smartlaunch│ │ eluxnetworks.net │ │
+│ │ hub.com/api │ │ /function/well-api │ │
+│ ├────────────────────┤ ├────────────────────────┤ │
+│ │ • Auth (OTP) │ │ • device_form │ │
+│ │ • /me/beneficiaries│ │ • device_list │ │
+│ │ • /auth/profile │ │ • sensor data │ │
+│ │ • Subscriptions │ │ • deployments │ │
+│ └────────────────────┘ └────────────────────────┘ │
+│ │
+│ ТОТ ЖЕ КОД ИЗ services/api.ts — АДАПТИРУЕМ ДЛЯ ВЕБА │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Screens & Features
+
+### 1. Browser Check Page (entry point)
+
+**URL:** `/` (redirect logic)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ │
+│ Поддерживаемый браузер? │
+│ │ │
+│ ├─ YES → redirect to /login или /dashboard (if logged) │
+│ │ │
+│ └─ NO → показать /unsupported │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 2. Unsupported Browser Page
+
+**URL:** `/unsupported`
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ │
+│ ⚠️ Браузер не поддерживается │
+│ │
+│ Для работы с Bluetooth-сенсорами используйте: │
+│ │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ Chrome │ │ Edge │ │ Opera │ │
+│ │ [Скачать] │ │ [Скачать] │ │ [Скачать] │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ │
+│ │
+│ ────────────────── или ────────────────── │
+│ │
+│ Используйте мобильное приложение: │
+│ │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ App Store │ │ Google Play │ │
+│ └──────────────┘ └──────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 3. Auth Flow
+
+**URLs:** `/login`, `/verify-otp`, `/enter-name`, `/add-loved-one`
+
+Полностью повторяет мобильное приложение:
+
+```
+/login
+┌─────────────────────────────────────────┐
+│ │
+│ WellNuo │
+│ │
+│ ┌───────────────────────────────────┐ │
+│ │ Email │ │
+│ └───────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────┐ │
+│ │ Получить код │ │
+│ └───────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────┘
+
+/verify-otp
+┌─────────────────────────────────────────┐
+│ │
+│ Введите код из письма │
+│ │
+│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
+│ │ _ │ │ _ │ │ _ │ │ _ │ │ _ │ │ _ │ │
+│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │
+│ │
+│ Отправить повторно (59 сек) │
+│ │
+└─────────────────────────────────────────┘
+```
+
+### 4. Dashboard
+
+**URL:** `/dashboard`
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ WellNuo [Profile Icon] │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ Добро пожаловать, {firstName}! │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 👵 Мама │ │
+│ │ ────────────────────────────────────────────────────── │ │
+│ │ Сенсоры: 3 активных │ │
+│ │ Последняя активность: 5 минут назад │ │
+│ │ │ │
+│ │ [Открыть] [Настройки] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 👴 Папа │ │
+│ │ ────────────────────────────────────────────────────── │ │
+│ │ Сенсоры: 2 активных │ │
+│ │ Последняя активность: 1 час назад │ │
+│ │ │ │
+│ │ [Открыть] [Настройки] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ + Добавить близкого │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 5. Beneficiary Detail
+
+**URL:** `/beneficiaries/[id]`
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ ← Назад 👵 Мама │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ Обзор │ │ Сенсоры │ │ История │ │ Настройки│ │
+│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
+│ │
+│ ═══════════════════════════════════════════════════════════ │
+│ │
+│ Сенсоры (3) [+ Добавить] │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 🟢 WP_523_81A14C │ │
+│ │ Кухня • Онлайн • Последнее: 2 мин назад │ │
+│ │ [Настройки] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 🟡 WP_524_92B25D │ │
+│ │ Спальня • Онлайн • Последнее: 15 мин назад │ │
+│ │ [Настройки] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 🔴 WP_525_A3C36E │ │
+│ │ Гостиная • Оффлайн • Последнее: 2 часа назад │ │
+│ │ [Настройки] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 6. Add Sensor (BLE Scan)
+
+**URL:** `/beneficiaries/[id]/add-sensor`
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ ← Назад Добавить сенсор │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ │ │
+│ │ 1. Включите сенсор (зажмите кнопку на 3 сек) │ │
+│ │ 2. Убедитесь что Bluetooth включён на компьютере │ │
+│ │ 3. Нажмите "Начать поиск" │ │
+│ │ │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 🔍 Начать поиск │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ───────────────────────────────────────────────────────────── │
+│ │
+│ Найденные устройства: │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 📶 WP_526_B4D47F Сигнал: ████░░ -65dBm │
+│ │ [Подключить]│ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 📶 WP_527_C5E58G Сигнал: ██░░░░ -78dBm │
+│ │ [Подключить]│ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 7. WiFi Setup
+
+**URL:** `/beneficiaries/[id]/setup-wifi`
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ ← Назад Настройка WiFi │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ Подключено к: WP_526_B4D47F │
+│ │
+│ ───────────────────────────────────────────────────────────── │
+│ │
+│ Шаг 2 из 4: Настройка WiFi │
+│ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 50% │
+│ │
+│ ───────────────────────────────────────────────────────────── │
+│ │
+│ Доступные сети: │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 📶 Home_WiFi_5G ████░░ -45dBm │ │
+│ │ 🔒 Защищённая [Выбрать] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ 📶 Home_WiFi ███░░░ -58dBm │ │
+│ │ 🔒 Защищённая [Выбрать] │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+│ ───────────────────────────────────────────────────────────── │
+│ │
+│ Или введите вручную: │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ Название сети (SSID) │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ Пароль 👁️ │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────┐ │
+│ │ Подключить сенсор │ │
+│ └─────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 8. Profile / Settings
+
+**URL:** `/profile`
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ ← Назад Профиль │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 👤 │ │
+│ │ John Doe │ │
+│ │ john@example.com │ │
+│ │ [Изменить] │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ───────────────────────────────────────────────────────────── │
+│ │
+│ Настройки │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 🌙 Тёмная тема [OFF] │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 🔔 Уведомления [ON] │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ───────────────────────────────────────────────────────────── │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ Выйти │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Error Handling
+
+### BLE Errors
+
+| Ошибка | Сообщение | Действие |
+|--------|-----------|----------|
+| Bluetooth disabled | "Bluetooth выключен на вашем устройстве" | Инструкция как включить |
+| Permission denied | "Доступ к Bluetooth запрещён" | Кнопка "Разрешить" + инструкция |
+| Device not found | "Сенсор не найден. Убедитесь что он включён" | Кнопка "Повторить поиск" |
+| Connection lost | "Соединение потеряно. Переподключение..." | Auto-retry 3 раза |
+| GATT error | "Ошибка связи с сенсором" | Кнопка "Попробовать снова" |
+
+### Network Errors
+
+| Ошибка | Сообщение | Действие |
+|--------|-----------|----------|
+| No internet | "Нет подключения к интернету" | Retry button |
+| API timeout | "Сервер не отвечает" | Retry button |
+| 401 Unauthorized | Redirect to /login | Clear token |
+| 500 Server Error | "Ошибка сервера, попробуйте позже" | Retry button |
+
+### WiFi Setup Errors
+
+| Ошибка | Сообщение | Действие |
+|--------|-----------|----------|
+| Wrong password | "Неверный пароль WiFi" | Показать поле ввода снова |
+| Network not found | "Сеть не найдена" | Кнопка "Обновить список" |
+| Connection timeout | "Сенсор не смог подключиться к WiFi" | Инструкция + retry |
+
+---
+
+## Web Bluetooth Implementation
+
+### Service UUIDs (из мобильного приложения)
+
+```typescript
+const BLE_CONFIG = {
+ // WP Sensor Service
+ SERVICE_UUID: 'xxxx-xxxx-xxxx-xxxx', // TODO: взять из BLEManager.ts
+
+ // Characteristics
+ WIFI_SSID_UUID: 'xxxx',
+ WIFI_PASSWORD_UUID: 'xxxx',
+ COMMAND_UUID: 'xxxx',
+ STATUS_UUID: 'xxxx',
+};
+```
+
+### Web Bluetooth API vs React Native BLE
+
+| Операция | React Native (текущий) | Web Bluetooth |
+|----------|------------------------|---------------|
+| Scan | `bleManager.startDeviceScan()` | `navigator.bluetooth.requestDevice()` |
+| Connect | `device.connect()` | `device.gatt.connect()` |
+| Read | `characteristic.read()` | `characteristic.readValue()` |
+| Write | `characteristic.writeWithResponse()` | `characteristic.writeValue()` |
+| Subscribe | `characteristic.monitor()` | `characteristic.startNotifications()` |
+
+---
+
+## Project Structure
+
+```
+wellnuo-web/
+├── app/
+│ ├── (auth)/
+│ │ ├── login/page.tsx
+│ │ ├── verify-otp/page.tsx
+│ │ ├── enter-name/page.tsx
+│ │ └── add-loved-one/page.tsx
+│ ├── (main)/
+│ │ ├── dashboard/page.tsx
+│ │ ├── beneficiaries/[id]/
+│ │ │ ├── page.tsx
+│ │ │ ├── add-sensor/page.tsx
+│ │ │ ├── setup-wifi/page.tsx
+│ │ │ └── device-settings/[deviceId]/page.tsx
+│ │ └── profile/page.tsx
+│ ├── unsupported/page.tsx
+│ ├── layout.tsx
+│ └── page.tsx (redirect logic)
+├── components/
+│ ├── ui/ (buttons, inputs, cards)
+│ ├── BrowserCheck.tsx
+│ ├── BLEScanner.tsx
+│ ├── WiFiSetup.tsx
+│ └── SensorCard.tsx
+├── services/
+│ ├── api.ts (адаптированный из мобилки)
+│ ├── webBluetooth.ts (новый, Web Bluetooth API)
+│ └── auth.ts
+├── hooks/
+│ ├── useBLE.ts
+│ ├── useAuth.ts
+│ └── useBeneficiaries.ts
+├── stores/
+│ └── authStore.ts (Zustand)
+├── lib/
+│ └── browserCheck.ts
+└── types/
+ └── index.ts
+```
+
+---
+
+## Implementation Phases
+
+### Phase 1: Foundation (Week 1)
+- [ ] Next.js project setup + Tailwind
+- [ ] Browser compatibility check
+- [ ] Unsupported browser page
+- [ ] Basic layout + navigation
+
+### Phase 2: Auth (Week 1-2)
+- [ ] Login page (email input)
+- [ ] OTP verification
+- [ ] Enter name (new users)
+- [ ] JWT token management
+- [ ] Protected routes
+
+### Phase 3: Dashboard & Beneficiaries (Week 2)
+- [ ] Dashboard with beneficiary cards
+- [ ] Beneficiary detail page
+- [ ] Add beneficiary flow
+- [ ] API integration (reuse from mobile)
+
+### Phase 4: BLE Integration (Week 2-3)
+- [ ] Web Bluetooth service
+- [ ] BLE scan page
+- [ ] Device connection
+- [ ] WiFi setup flow
+- [ ] Sensor attachment to API
+
+### Phase 5: Polish (Week 3)
+- [ ] Error handling (all cases)
+- [ ] Loading states
+- [ ] Responsive design
+- [ ] Dark mode (optional)
+- [ ] PWA setup (optional)
+
+---
+
+## Success Criteria
+
+- [ ] Работает в Chrome/Edge/Opera на Windows и macOS
+- [ ] Показывает понятную ошибку в Safari/Firefox
+- [ ] Auth flow идентичен мобилке
+- [ ] BLE сканирование находит WP сенсоры
+- [ ] WiFi setup работает полностью
+- [ ] Сенсоры успешно attach'атся к beneficiary
+- [ ] Все ошибки обрабатываются с понятными сообщениями
+- [ ] Responsive дизайн (laptop + большой монитор)
+
+---
+
+## Questions to Clarify
+
+### ❓ Вопрос 1: BLE UUIDs
+Нужно взять Service UUID и Characteristic UUIDs из `services/ble/BLEManager.ts`. Это критично для Web Bluetooth.
+
+### ❓ Вопрос 2: Домен
+Где будет хоститься? Варианты:
+- `web.wellnuo.com`
+- `app.wellnuo.com`
+- `wellnuo.smartlaunchhub.com` (поддомен)
+
+### ❓ Вопрос 3: PWA
+Делать как PWA (можно "установить" на рабочий стол)? Это добавит ~1 день работы.
+
+---
+
+## Notes
+
+- Backend остаётся ТОТ ЖЕ — никаких изменений на сервере
+- Код API вызовов переиспользуем из `services/api.ts`
+- BLE логику пишем заново на Web Bluetooth API
+- UI пишем на React + Tailwind (не React Native)
diff --git a/PRD.md b/PRD.md
index b55c8c6..f08c0db 100644
--- a/PRD.md
+++ b/PRD.md
@@ -1,152 +1,293 @@
-# PRD — WellNuo Full Audit & Bug Fixes
+# PRD — WellNuo Sensor Integration (BLE + API)
+
+## Overview
+
+Full integration of WP sensors with WellNuo app:
+- BLE scanning and connection
+- WiFi configuration
+- API attachment to beneficiary's deployment
+- Real-time sensor management
+
+---
## ❓ Вопросы для уточнения
-### ❓ Вопрос 1: Формат серийного номера
-Какой regex pattern должен валидировать serial number устройства? Сейчас проверяется только длина >= 8.
-**Ответ:** Использовать regex `/^[A-Za-z0-9]{8,16}$/` — буквенно-цифровой, 8-16 символов.
+### ❓ Вопрос 1: BLE Permission Handling
+Как обрабатывать отказ пользователя в BLE разрешениях? Это критично для основного флоу — без BLE сканирование невозможно.
+**Ответ:** Показать alert с объяснением зачем нужен BLE + кнопка "Open Settings". Уже есть `BLEContext.tsx` с permission handling — нужно улучшить UX сообщений.
-### ❓ Вопрос 2: Demo credentials configuration
-Куда вынести hardcoded demo credentials (anandk)? В .env файл, SecureStore или отдельный config?
-**Ответ:** `anandk` — устаревший аккаунт. Нужно заменить на `robster/rob2` (актуальный аккаунт для Legacy API). Вынести в `.env` файл как `LEGACY_API_USER=robster` и `LEGACY_API_PASSWORD=rob2`.
+### ❓ Вопрос 2: Concurrent Sensor Setup
+Что если пользователь пытается настроить несколько сенсоров одновременно? BLE connection обычно exclusive.
+**Ответ:** Текущая реализация поддерживает batch setup — сенсоры обрабатываются ПОСЛЕДОВАТЕЛЬНО (один за другим). `setup-wifi.tsx` уже имеет `processSensorsSequentially()`. Одновременные BLE connections не нужны.
-### ❓ Вопрос 3: Максимальное количество beneficiaries
-Сколько beneficiaries может быть у одного пользователя? Нужна ли пагинация для списка?
-**Ответ:** Максимум ~5 beneficiaries. Пагинация не нужна.
+### ❓ Вопрос 3: WiFi Credentials Validation
+Нужно ли валидировать WiFi пароль перед отправкой в сенсор? Некорректный пароль = сенсор недоступен до физической перезагрузки.
+**Ответ:** Базовая валидация (длина ≥8 символов для WPA2). Сложная валидация невозможна — мы не знаем тип WiFi сети. При ошибке сенсор можно перезагрузить физически (кнопка reset) или через BLE команду `r`.
-## Цель
+### ❓ Вопрос 4: Deployment ID Mapping
+Как получить deployment_id для beneficiary? Есть ли это поле в WellNuo API или нужно создавать mapping?
+**Ответ:** ✅ УЖЕ РЕШЕНО! `deploymentId` хранится в WellNuo API как поле beneficiary. Получается через `GET /me/beneficiaries/:id` → `beneficiary.deploymentId`. Код в `services/api.ts:1893-1910` уже работает.
-Исправить критические баги, улучшить безопасность и стабильность приложения WellNuo перед production release.
+---
-## Контекст проекта
+## 💡 Рекомендации
-- **Тип:** Expo / React Native приложение
-- **Стек:** expo 53, react-native 0.79, typescript, expo-router, livekit, stripe, BLE
-- **API:** WellNuo (wellnuo.smartlaunchhub.com) + Legacy (eluxnetworks.net)
-- **БД:** PostgreSQL через WellNuo API
-- **Навигация:** Expo Router + NavigationController.ts
+### 💡 Рекомендация 1: BLE Connection State Management
+**Что:** Добавить централизованный state machine для BLE соединений с retry логикой
+**Почему:** BLE нестабильно — нужен robust retry mechanism и clear error states для пользователя
+**Приоритет:** Высокий
+**Принять?** [x] Да, добавить в задачи — уже частично есть в `BLEManager.ts`, нужно улучшить
-## Задачи
+### 💡 Рекомендация 2: WiFi Credentials Cache
+**Что:** Сохранять WiFi credentials в SecureStore для повторного использования
+**Почему:** Пользователь не захочет каждый раз вводить домашний WiFi пароль для каждого сенсора
+**Приоритет:** Средний
+**Принять?** [x] Да, добавить в задачи — файл `services/wifiPasswordStore.ts` уже существует, нужно интегрировать
-### Phase 1: Критические исправления
+### 💡 Рекомендация 3: Sensor Setup Analytics
+**Что:** Добавить аналитику для tracking setup success rate и failure points
+**Почему:** Поможет выявить где пользователи застревают в setup flow и оптимизировать UX
+**Приоритет:** Низкий
+**Принять?** [ ] Нет, пропустить — не для MVP, добавим после релиза
-- [x] **@backend** **Заменить устаревшие credentials (anandk → robster) и вынести в .env**
- - Файлы для замены:
- - `services/api.ts:1508-1509` — основной API клиент
- - `backend/src/services/mqtt.js:20-21` — MQTT сервис
- - `WellNuoLite/app/(tabs)/chat.tsx:37-38` — текстовый чат
- - `WellNuoLite/contexts/VoiceContext.tsx:27-28` — голосовой контекст
- - `WellNuoLite/julia-agent/julia-ai/src/agent.py:31-32` — Python агент
- - `wellnuo-debug/debug.html:728-733` — debug консоль
- - `mqtt-test.js:15-16` — тестовый скрипт
- - Что сделать:
- 1. Заменить `anandk/anandk_8` на `robster/rob2` везде
- 2. Вынести в `.env`: `LEGACY_API_USER=robster`, `LEGACY_API_PASSWORD=rob2`
- 3. Читать через `process.env` / Expo Constants
- - Готово когда: Все файлы используют `robster`, credentials в `.env`
+### 💡 Рекомендация 4: Background Sync for Sensor Status
+**Что:** Background task для периодического обновления статуса сенсоров
+**Почему:** Realtime статус критичен для healthcare приложения — пользователь должен знать что сенсор offline
+**Приоритет:** Высокий
+**Принять?** [ ] Нет, пропустить — достаточно pull-to-refresh + useFocusEffect. Background sync добавляет complexity и battery drain
-- [x] **@backend** **Fix displayName undefined в API response**
- - Файл: `services/api.ts:698-714`
- - Что сделать: Добавить fallback в функцию `getBeneficiariesFromResponse`: `displayName: item.customName || item.name || item.email || 'Unknown User'`
- - Готово когда: BeneficiaryCard никогда не показывает undefined
+### 💡 Рекомендация 5: QR Code для быстрого добавления
+**Что:** QR код на сенсоре с well_id и MAC для автозаполнения
+**Почему:** Устранит human error в вводе MAC адреса и ускорит onboarding
+**Приоритет:** Средний
+**Принять?** [ ] Нет, пропустить — на сенсорах нет QR кодов. MAC парсится из BLE названия `WP_523_81a14c` автоматически
-- [x] **@frontend** **BLE cleanup при logout**
- - Файл: `contexts/BLEContext.tsx`
- - Переиспользует: `services/ble/BLEManager.ts`
- - Что сделать: В функции logout добавить вызов `bleManager.disconnectAll()` перед очисткой состояния
- - Готово когда: При logout все BLE соединения отключаются
+---
-- [x] **@frontend** **Fix race condition с AbortController**
- - Файл: `app/(tabs)/index.tsx:207-248`
- - Что сделать: В `loadBeneficiaries` создать AbortController, передать signal в API вызовы, отменить в useEffect cleanup
- - Готово когда: Быстрое переключение экранов не вызывает дублирующих запросов
+## ⚠️ CRITICAL: API Rules for Workers
-- [x] **@backend** **Обработка missing deploymentId**
- - Файл: `services/api.ts:1661-1665`
- - Что сделать: Вместо `return []` выбросить Error с кодом 'MISSING_DEPLOYMENT_ID' и message 'No deployment configured for user'
- - Готово когда: UI показывает понятное сообщение об ошибке
+> **ВНИМАНИЕ! Воркеры ОБЯЗАНЫ следовать этим правилам!**
+> Без этого сенсоры НЕ БУДУТ работать с Legacy API.
-### Phase 2: Безопасность
+### Два разных ID — НЕ ПУТАТЬ!
-- [x] **@frontend** **WiFi password в SecureStore**
- - Файл: `app/(tabs)/beneficiaries/[id]/setup-wifi.tsx`
- - Переиспользует: `services/storage.ts`
- - Что сделать: Заменить `AsyncStorage.setItem` на `storage.setItem` для WiFi credentials, добавить ключ `wifi_${beneficiaryId}`
- - Готово когда: WiFi пароли сохраняются в зашифрованном виде
+| ID | Что это | Когда использовать | Пример |
+|----|---------|-------------------|--------|
+| `well_id` | ID сенсора (из названия `WP_523_...`) | Создание нового устройства | `523` |
+| `device_id` | ID записи в базе данных | Обновление/удаление существующего | `456` |
-- [x] **@backend** **Проверить equipmentStatus mapping**
- - Файл: `services/api.ts:113`, `services/NavigationController.ts:89-95`
- - Что сделать: Убедиться что API возвращает точно 'demo', не 'demo_mode'. Добавить debug логи в BeneficiaryDetailController
- - Готово когда: Demo beneficiary корректно определяется в навигации
+### Правило 1: Создание нового устройства (attach)
-### Phase 3: UX улучшения
+```
+device_form с параметрами:
+- well_id: 523 ← ID из названия сенсора WP_523_xxxxx
+- device_mac: 81A14C ← MAC адрес (UPPERCASE!)
+- deployment_id: 24 ← ID деплоймента beneficiary
+```
-- [x] **@frontend** **Fix avatar caching после upload**
- - Файл: `app/(tabs)/profile/index.tsx`
- - Переиспользует: `services/api.ts` метод `getMe()`
- - Что сделать: После успешного upload avatar вызвать `api.getMe()` и обновить state, не использовать локальный imageUri
- - Готово когда: Avatar обновляется сразу после upload
+### Правило 2: MAC адрес всегда UPPERCASE
-- [x] **@frontend** **Retry button в error state**
- - Файл: `app/(tabs)/index.tsx:317-327`
+```typescript
+device_mac: mac.toUpperCase() // "81A14C"
+```
+
+### Правило 3: Парсинг well_id из BLE названия
+
+```typescript
+const parts = deviceName.split('_');
+const wellId = parseInt(parts[1], 10); // 523
+const mac = parts[2].toUpperCase(); // "81A14C"
+```
+
+---
+
+## Technical Architecture
+
+### Two API Systems
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ WellNuo App │
+├─────────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
+│ │ WellNuo API │ │ Legacy API │ │
+│ │ (wellnuo.smartlaunchhub.com) │ (eluxnetworks.net) │ │
+│ ├─────────────────────┤ ├─────────────────────────────┤ │
+│ │ • Auth (JWT) │ │ • Sensor data │ │
+│ │ • Beneficiaries │ │ • Device management │ │
+│ │ • Subscriptions │ │ • Deployment management │ │
+│ │ • deploymentId link │──────────▶│ • device_form │ │
+│ └─────────────────────┘ │ • device_list_by_deployment │ │
+│ │ • request_devices │ │
+│ │ • get_devices_locations │ │
+│ └─────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Tasks
+
+> **4 воркера без конфликтов по файлам:**
+> - **@worker1** — BLE Services (`services/ble/*`)
+> - **@worker2** — API Services (`services/api.ts`, `services/*.ts`)
+> - **@worker3** — Equipment Screen (`equipment.tsx`)
+> - **@worker4** — Setup Screens (`setup-wifi.tsx`, `add-sensor.tsx`, `device-settings/`)
+
+---
+
+### @worker1 — BLE Services (services/ble/*)
+
+- [x] **Add BLE permissions handling with graceful fallback**
+ - Файл: `services/ble/BLEManager.ts`
+ - Что сделать: Добавить requestPermissions() метод с iOS/Android specific handling
+ - Готово когда: При отказе в разрешениях показывается понятная ошибка с инструкциями
+
+- [x] **Implement BLE connection state machine**
+ - Файл: `services/ble/BLEManager.ts`, `services/ble/types.ts`
+ - Что сделать: State enum (idle/scanning/connecting/connected/error), retry логика, timeout handling
+ - Готово когда: BLE соединение восстанавливается автоматически при обрыве
+
+- [x] **Add concurrent connection protection**
+ - Файл: `services/ble/BLEManager.ts`
+ - Что сделать: Mutex для предотвращения одновременных BLE операций
+ - Готово когда: Попытка подключиться к второму устройству показывает ошибку "Disconnect current device first"
+
+- [x] **Create BLE integration tests**
+ - Файл: `services/ble/__tests__/BLEManager.integration.test.ts`
+ - Переиспользует: `MockBLEManager.ts` patterns
+ - Что сделать: Test complete setup flow with mock BLE device
+ - Готово когда: Setup flow тестируется end-to-end без real hardware
+
+---
+
+### @worker2 — API & Backend Services (services/*.ts)
+
+- [ ] **Implement WiFi credentials cache in SecureStore**
+ - Файл: `services/wifiPasswordStore.ts`
+ - Переиспользует: `services/storage.ts` patterns
+ - Что сделать: Save/retrieve WiFi networks, auto-suggest previously used networks
+ - Готово когда: При повторной настройке предлагается сохраненный пароль
+
+- [ ] **Create deployment_id lookup mechanism**
+ - Файл: `services/api.ts`
+ - Что сделать: Add getDeploymentForBeneficiary() method to resolve beneficiary → deployment_id mapping
+ - Готово когда: attachDeviceToBeneficiary() автоматически получает deployment_id
+
+- [ ] **Add API error handling for sensor attachment**
+ - Файл: `services/api.ts:1878-1945`
+ - Переиспользует: Existing error handling patterns
+ - Что сделать: Specific error messages for duplicate MAC, invalid well_id, network errors
+ - Готово когда: Пользователь получает понятные ошибки вместо generic "API Error"
+
+- [ ] **Add sensor health monitoring**
+ - Файл: `services/api.ts`, новый `services/sensorHealth.ts`
+ - Что сделать: Track offline duration, battery status, connection quality metrics
+ - Готово когда: Equipment screen показывает health warnings
+
+- [ ] **Add sensor setup analytics**
+ - Файл: новый `services/analytics.ts`
+ - Что сделать: Track setup funnel, failure points, time-to-complete
+ - Готово когда: Analytics показывают setup conversion rate
+
+---
+
+### @worker3 — Equipment Screen (equipment.tsx)
+
+- [ ] **Add pull-to-refresh with loading states**
+ - Файл: `app/(tabs)/beneficiaries/[id]/equipment.tsx`
+ - Переиспользует: `components/ui/LoadingSpinner.tsx`
+ - Что сделать: RefreshControl + loading overlay, haptic feedback
+ - Готово когда: Pull-to-refresh работает с visual feedback
+
+- [ ] **Enhanced sensor cards with status indicators**
+ - Файл: `app/(tabs)/beneficiaries/[id]/equipment.tsx:400-468`
+ - Переиспользует: `components/ui/icon-symbol.tsx` для status dots
+ - Что сделать: Location icon + name, last seen relative time, online/warning/offline status dot
+ - Готово когда: Каждый сенсор показывает location, status и last seen
+
+- [ ] **Add empty state with prominent Add Sensor button**
+ - Файл: `app/(tabs)/beneficiaries/[id]/equipment.tsx`
- Переиспользует: `components/ui/Button.tsx`
- - Что сделать: В error блоке добавить `` под текстом ошибки
- - Готово когда: При ошибке загрузки есть кнопка повтора
+ - Что сделать: Illustration + "No sensors added yet" + large "Add Sensor" button
+ - Готово когда: Empty state направляет к add-sensor screen
-- [x] **@frontend** **Улучшить serial validation**
- - Файл: `app/(auth)/activate.tsx:33-48`
- - Что сделать: Добавить regex validation перед API вызовом, показывать ошибку "Invalid serial format" в real-time
- - Готово когда: Некорректный формат serial показывает ошибку до отправки
+- [ ] **Add bulk sensor operations**
+ - Файл: `app/(tabs)/beneficiaries/[id]/equipment.tsx`
+ - Что сделать: Select multiple sensors → bulk detach, bulk location update
+ - Готово когда: Long press активирует selection mode с bulk actions
-- [x] **@frontend** **Role-based UI для Edit кнопки**
- - Файл: `app/(tabs)/index.tsx:133-135`
- - Что сделать: Обернуть Edit кнопку в условие `{beneficiary.role === 'custodian' && ...}`
- - Готово когда: Caretaker не видит кнопку Edit у beneficiary
+- [ ] **Add offline mode graceful degradation**
+ - Файл: `app/(tabs)/beneficiaries/[id]/equipment.tsx`
+ - Переиспользует: Network state detection patterns
+ - Что сделать: Show cached sensor data when offline, queue operations for retry
+ - Готово когда: App показывает sensor data даже без internet
-- [x] **@frontend** **Debouncing для refresh button**
- - Файл: `app/(tabs)/index.tsx:250-254`
- - Что сделать: Добавить state `isRefreshing`, disable кнопку на 1 секунду после нажатия
- - Готово когда: Нельзя spam нажимать refresh
+---
-### Phase 4: Очистка кода
+### @worker4 — Setup Screens (setup-wifi, add-sensor, device-settings)
-- [x] **@backend** **Удалить mock data из getBeneficiaries**
- - Файл: `services/api.ts:562-595`
- - Что сделать: Удалить функцию `getBeneficiaries` полностью, оставить только `getAllBeneficiaries`
- - Готово когда: Функция не существует в коде
+- [ ] **Add WiFi credentials validation**
+ - Файл: `app/(tabs)/beneficiaries/[id]/setup-wifi.tsx`
+ - Переиспользует: `utils/serialValidation.ts` pattern
+ - Что сделать: Validate SSID length, password complexity, show warnings for weak passwords
+ - Готово когда: Невалидные credentials блокируют отправку с объяснением
-- [x] **@backend** **Константы для magic numbers**
- - Файл: `services/api.ts:608-609`
- - Что сделать: Создать `const ONLINE_THRESHOLD_MS = 30 * 60 * 1000` в начале файла, использовать в коде
- - Готово когда: Нет magic numbers в логике online/offline
+- [ ] **Add WiFi signal strength indicator in setup**
+ - Файл: `app/(tabs)/beneficiaries/[id]/setup-wifi.tsx:89-156`
+ - Что сделать: Parse RSSI from BLE response, show signal bars UI
+ - Готово когда: Список WiFi сетей показывает signal strength визуально
-- [x] **@backend** **Удалить console.logs**
- - Файл: `services/api.ts:1814-1895`
- - Что сделать: Удалить все `console.log` в функции `attachDeviceToBeneficiary`
- - Готово когда: Нет console.log в production коде
+- [ ] **Add setup progress indicator**
+ - Файл: `app/(tabs)/beneficiaries/[id]/setup-wifi.tsx`
+ - Переиспользует: `components/BatchSetupProgress.tsx` pattern
+ - Что сделать: 4-step progress: BLE Connect → WiFi Config → Reboot → API Attach
+ - Готово когда: Пользователь видит текущий step и прогресс
-- [x] **@frontend** **Null safety в navigation**
- - Файл: `app/(tabs)/index.tsx:259`
- - Что сделать: Добавить guard `if (!beneficiary?.id) return;` перед `router.push`
- - Готово когда: Нет crash при нажатии на beneficiary без ID
+- [ ] **Improve BLE scan UI with signal strength**
+ - Файл: `app/(tabs)/beneficiaries/[id]/add-sensor.tsx`
+ - Переиспользует: `components/ui/LoadingSpinner.tsx`
+ - Что сделать: Show RSSI bars, device distance estimate, scan progress
+ - Готово когда: Список сенсоров показывает signal quality визуально
-- [x] **@frontend** **BLE scanning cleanup**
- - Файл: `services/ble/BLEManager.ts:64-80`
- - Переиспользует: `useFocusEffect` из React Navigation
- - Что сделать: Добавить `stopScan()` в cleanup функцию всех экранов с BLE scanning
- - Готово когда: BLE scanning останавливается при уходе с экрана
+- [ ] **Enhanced device settings with reconnect flow**
+ - Файл: `app/(tabs)/beneficiaries/[id]/device-settings/[deviceId].tsx:137-152`
+ - Переиспользует: existing setup-wifi screen
+ - Что сделать: "Change WiFi" → guided BLE reconnect → WiFi update without full re-setup
+ - Готово когда: WiFi изменение работает без re-pairing device
-## Критерии готовности
+- [ ] **Add comprehensive error states**
+ - Файл: `app/(tabs)/beneficiaries/[id]/setup-wifi.tsx`, `add-sensor.tsx`
+ - Переиспользует: `components/ui/ErrorMessage.tsx`
+ - Что сделать: BLE timeout, WiFi failure, API error → specific recovery actions
+ - Готово когда: Каждая ошибка имеет clear recovery path
-- [x] Нет hardcoded credentials в коде
-- [x] BLE соединения отключаются при logout
-- [x] WiFi пароли зашифрованы
-- [x] Нет race conditions при быстром переключении
-- [x] Console.logs удалены
-- [x] Avatar caching исправлен
-- [x] Role-based доступ работает корректно
+- [ ] **Add E2E tests for sensor management**
+ - Файл: `.maestro/sensor-setup.yaml`
+ - Что сделать: Full sensor setup flow from scan to API attachment
+ - Готово когда: E2E test покрывает complete happy path
+
+---
+
+## Success Criteria
+
+- [ ] BLE scan finds WP sensors with signal strength indication
+- [ ] WiFi configuration works reliably with credential validation
+- [ ] API attachment succeeds with proper error handling
+- [ ] Sensor status shows correctly (online/offline) with background updates
+- [ ] Location/description can be changed through device settings
+- [ ] Detach removes sensor from deployment cleanly
+- [ ] All flows work on real device with proper permission handling
+- [ ] Setup funnel analytics show >80% completion rate
+- [ ] App handles offline mode gracefully
## ✅ Статус
-**15 задач** распределены между @backend (6) и @frontend (9).
-Готов к запуску после ответа на 3 вопроса выше.
+PRD улучшен с focus на:
+- **Production readiness**: Добавлены permission handling, error states, background sync
+- **User Experience**: Credential cache, signal indicators, progress tracking, QR scanning
+- **Developer Experience**: Четкое разделение worker'ов, переиспользование existing компонентов
+- **Quality**: Integration tests, E2E coverage, analytics для optimization
+
+Основные улучшения: state machine для BLE, WiFi credential cache, deployment mapping, background sensor sync, comprehensive error handling.
diff --git a/WellNuoLite b/WellNuoLite
index a1e3093..9f12830 160000
--- a/WellNuoLite
+++ b/WellNuoLite
@@ -1 +1 @@
-Subproject commit a1e30939a6144300421179ae930025cc87b6dacb
+Subproject commit 9f128308504ba14423fd26a48437ead10f600702
diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx
index 2cc0179..e86354b 100644
--- a/app/(tabs)/beneficiaries/[id]/index.tsx
+++ b/app/(tabs)/beneficiaries/[id]/index.tsx
@@ -63,7 +63,8 @@ export default function BeneficiaryDetailScreen() {
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState(null);
- const [showWebView, setShowWebView] = useState(false);
+ // Default: show WebView dashboard (real data), toggle enables Mock Data
+ const [showWebView, setShowWebView] = useState(true);
const [isWebViewReady, setIsWebViewReady] = useState(false);
const [legacyCredentials, setLegacyCredentials] = useState<{
token: string;
@@ -136,6 +137,9 @@ export default function BeneficiaryDetailScreen() {
user_id: ${credentials.userId}
};
localStorage.setItem('auth2', JSON.stringify(authData));
+
+ // Ensure is_mobile flag is set (hides navigation in WebView)
+ localStorage.setItem('is_mobile', '1');
})();
true;
`;
@@ -389,6 +393,7 @@ export default function BeneficiaryDetailScreen() {
// JavaScript to inject token into localStorage for WebView
// Web app expects auth2 as JSON: {username, token, user_id}
+ // Also sets is_mobile flag to hide navigation bar (like in Lite version)
const injectedJavaScript = legacyCredentials
? `
(function() {
@@ -399,6 +404,9 @@ export default function BeneficiaryDetailScreen() {
user_id: ${legacyCredentials.userId}
};
localStorage.setItem('auth2', JSON.stringify(authData));
+
+ // Set is_mobile flag to hide navigation bar in WebView
+ localStorage.setItem('is_mobile', '1');
} catch(e) {
}
})();
@@ -496,9 +504,9 @@ export default function BeneficiaryDetailScreen() {
)}
- {/* Developer Toggle */}
+ {/* Mock Data Toggle - inverted: true = show MockDashboard, false = show WebView */}
-
+ setShowWebView(!val)} />
{/* Content area - WebView or MockDashboard */}
diff --git a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx
index 369e0cb..ef80c2d 100644
--- a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx
+++ b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx
@@ -223,7 +223,7 @@ export default function SetupWiFiScreen() {
ssid: string,
pwd: string
): Promise => {
- const { deviceId, wellId, deviceName } = sensor;
+ const { deviceId, wellId, deviceName, mac } = sensor;
const isSimulator = !Device.isDevice;
// Set start time
@@ -264,12 +264,11 @@ export default function SetupWiFiScreen() {
updateSensorStep(deviceId, 'attach', 'in_progress');
updateSensorStatus(deviceId, 'attaching');
- if (!isSimulator && wellId) {
+ if (!isSimulator && wellId && mac) {
const attachResponse = await api.attachDeviceToBeneficiary(
id!,
wellId,
- ssid,
- pwd
+ mac
);
if (!attachResponse.ok) {
const errorDetail = attachResponse.error || 'Unknown API error';
diff --git a/app/(tabs)/beneficiaries/[id]/subscription.tsx b/app/(tabs)/beneficiaries/[id]/subscription.tsx
index b373b76..32640ce 100644
--- a/app/(tabs)/beneficiaries/[id]/subscription.tsx
+++ b/app/(tabs)/beneficiaries/[id]/subscription.tsx
@@ -212,11 +212,34 @@ export default function SubscriptionScreen() {
}
}
- // Wait a moment for webhook to process
- await new Promise(resolve => setTimeout(resolve, 2000));
+ // Poll for subscription to be activated (webhook may take time)
+ // Retry up to 5 times with increasing delays
+ const maxRetries = 5;
+ const delays = [1000, 2000, 3000, 4000, 5000]; // 1s, 2s, 3s, 4s, 5s
+
+ let subscriptionActivated = false;
+ for (let i = 0; i < maxRetries; i++) {
+ await new Promise(resolve => setTimeout(resolve, delays[i]));
+
+ // Fetch fresh beneficiary data
+ const response = await api.getWellNuoBeneficiary(beneficiary.id);
+ if (response.ok && response.data) {
+ const sub = response.data.subscription;
+ if (sub && (sub.status === 'active' || sub.status === 'trialing')) {
+ setBeneficiary(response.data);
+ subscriptionActivated = true;
+ break;
+ }
+ }
+ }
+
+ // Even if polling didn't confirm, still show success (payment went through)
+ // The subscription will appear on next refresh
+ if (!subscriptionActivated) {
+ await loadBeneficiary(); // One final attempt
+ }
setJustSubscribed(true);
- await loadBeneficiary();
setShowSuccessModal(true);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Something went wrong';
diff --git a/backend/scripts/setup-stripe-products.js b/backend/scripts/setup-stripe-products.js
index 9a52af2..d11c03c 100644
--- a/backend/scripts/setup-stripe-products.js
+++ b/backend/scripts/setup-stripe-products.js
@@ -47,10 +47,10 @@ async function setupStripeProducts() {
});
console.log(`✓ Product created: ${premium.id}`);
- // Create price for Premium ($9.99/month)
+ // Create price for Premium ($49/month)
const premiumPrice = await stripe.prices.create({
product: premium.id,
- unit_amount: 999, // $9.99
+ unit_amount: 4900, // $49.00
currency: 'usd',
recurring: {
interval: 'month'
@@ -59,7 +59,7 @@ async function setupStripeProducts() {
display_name: 'Premium Monthly'
}
});
- console.log(`✓ Price created: ${premiumPrice.id} ($9.99/month)\n`);
+ console.log(`✓ Price created: ${premiumPrice.id} ($49.00/month)\n`);
// Summary
console.log('='.repeat(50));
diff --git a/components/ui/DevModeToggle.tsx b/components/ui/DevModeToggle.tsx
index 1b1702f..0b56f3e 100644
--- a/components/ui/DevModeToggle.tsx
+++ b/components/ui/DevModeToggle.tsx
@@ -19,8 +19,8 @@ interface DevModeToggleProps {
export function DevModeToggle({
value,
onValueChange,
- label = 'Developer Mode',
- hint = 'Show WebView dashboard',
+ label = 'Mock Data',
+ hint = 'Use demo data instead of real dashboard',
}: DevModeToggleProps) {
return (
diff --git a/launched.png b/launched.png
new file mode 100644
index 0000000..5abf399
Binary files /dev/null and b/launched.png differ
diff --git a/services/__tests__/wifiPasswordStore.test.ts b/services/__tests__/wifiPasswordStore.test.ts
index 844db58..38c5048 100644
--- a/services/__tests__/wifiPasswordStore.test.ts
+++ b/services/__tests__/wifiPasswordStore.test.ts
@@ -3,7 +3,6 @@
*/
import * as SecureStore from 'expo-secure-store';
-import AsyncStorage from '@react-native-async-storage/async-storage';
import * as encryption from '../encryption';
import {
saveWiFiPassword,
@@ -264,25 +263,29 @@ describe('wifiPasswordStore', () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
// Old data in AsyncStorage
- (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const mockAsyncStorage = require('@react-native-async-storage/async-storage').default;
+ (mockAsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(
JSON.stringify(oldPasswords)
);
await migrateFromAsyncStorage();
- // Should save to SecureStore
+ // Should encrypt and save to SecureStore
+ expect(encryption.encrypt).toHaveBeenCalledWith('pass1');
+ expect(encryption.encrypt).toHaveBeenCalledWith('pass2');
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
- JSON.stringify(oldPasswords)
+ JSON.stringify({ Network1: 'encrypted_pass1', Network2: 'encrypted_pass2' })
);
// Should remove from AsyncStorage
- expect(AsyncStorage.removeItem).toHaveBeenCalledWith('WIFI_PASSWORDS');
- expect(AsyncStorage.removeItem).toHaveBeenCalledWith('LAST_WIFI_PASSWORD');
+ expect(mockAsyncStorage.removeItem).toHaveBeenCalledWith('WIFI_PASSWORDS');
+ expect(mockAsyncStorage.removeItem).toHaveBeenCalledWith('LAST_WIFI_PASSWORD');
});
it('should skip migration if data already exists in SecureStore', async () => {
- const existingPasswords = { Network1: 'pass1' };
+ const existingPasswords = { Network1: 'encrypted_pass1' };
// Data already exists in SecureStore
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(
@@ -292,8 +295,10 @@ describe('wifiPasswordStore', () => {
await migrateFromAsyncStorage();
// Should not call AsyncStorage
- expect(AsyncStorage.getItem).not.toHaveBeenCalled();
- expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const mockAsyncStorage = require('@react-native-async-storage/async-storage').default;
+ expect(mockAsyncStorage.getItem).not.toHaveBeenCalled();
+ // setItemAsync might be called by migrateToEncrypted, but only if needed
});
it('should skip migration if no data in AsyncStorage', async () => {
@@ -301,18 +306,22 @@ describe('wifiPasswordStore', () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
// No data in AsyncStorage
- (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(null);
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const mockAsyncStorage = require('@react-native-async-storage/async-storage').default;
+ (mockAsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(null);
await migrateFromAsyncStorage();
// Should not migrate anything
expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
- expect(AsyncStorage.removeItem).not.toHaveBeenCalled();
+ expect(mockAsyncStorage.removeItem).not.toHaveBeenCalled();
});
it('should not throw error on migration failure', async () => {
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(null);
- (AsyncStorage.getItem as jest.Mock).mockRejectedValueOnce(
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const mockAsyncStorage = require('@react-native-async-storage/async-storage').default;
+ (mockAsyncStorage.getItem as jest.Mock).mockRejectedValueOnce(
new Error('AsyncStorage error')
);
@@ -324,23 +333,31 @@ describe('wifiPasswordStore', () => {
describe('migrateToEncrypted', () => {
it('should encrypt unencrypted passwords', async () => {
const stored = {
- NetworkA: 'plaintext_password_A',
+ NetworkA: 'plaintext_password_A', // Not encrypted (doesn't start with 'encrypted_')
NetworkB: 'encrypted_passB', // Already encrypted
- NetworkC: 'plaintext_password_C',
+ NetworkC: 'plaintext_password_C', // Not encrypted
};
(SecureStore.getItemAsync as jest.Mock).mockResolvedValueOnce(JSON.stringify(stored));
+ // Reset mock call count
+ (encryption.encrypt as jest.Mock).mockClear();
+
await migrateToEncrypted();
- // Should have encrypted the plaintext passwords
+ // Should have encrypted the plaintext passwords (those that don't start with 'encrypted_')
expect(encryption.encrypt).toHaveBeenCalledWith('plaintext_password_A');
expect(encryption.encrypt).toHaveBeenCalledWith('plaintext_password_C');
- expect(encryption.encrypt).not.toHaveBeenCalledWith('encrypted_passB');
+ // NetworkB already starts with 'encrypted_' so shouldn't be re-encrypted
+ expect(encryption.encrypt).toHaveBeenCalledTimes(2);
// Should have saved the migrated passwords
expect(SecureStore.setItemAsync).toHaveBeenCalledWith(
'WIFI_PASSWORDS',
- expect.stringContaining('encrypted_')
+ JSON.stringify({
+ NetworkA: 'encrypted_plaintext_password_A',
+ NetworkB: 'encrypted_passB',
+ NetworkC: 'encrypted_plaintext_password_C',
+ })
);
});
diff --git a/services/api.ts b/services/api.ts
index ac8792a..f8460f7 100644
--- a/services/api.ts
+++ b/services/api.ts
@@ -1878,18 +1878,16 @@ class ApiService {
async attachDeviceToBeneficiary(
beneficiaryId: string,
wellId: number,
- ssid: string,
- password: string
+ deviceMac: string
) {
try {
-
// Get auth token for WellNuo API
const token = await this.getToken();
if (!token) {
throw new Error('Not authenticated');
}
- // Get beneficiary details
+ // Get beneficiary details to get deploymentId
const response = await fetch(`${this.baseUrl}/me/beneficiaries/${beneficiaryId}`, {
method: 'GET',
headers: {
@@ -1897,13 +1895,11 @@ class ApiService {
},
});
if (!response.ok) {
- const errorText = await response.text();
throw new Error(`Failed to get beneficiary: ${response.status}`);
}
const beneficiary = await response.json();
const deploymentId = beneficiary.deploymentId;
- const beneficiaryName = beneficiary.firstName || 'Sensor';
if (!deploymentId) {
throw new Error('No deployment configured for this beneficiary. Please remove and re-add the beneficiary to fix this.');
@@ -1915,14 +1911,14 @@ class ApiService {
}
// Use device_form to attach device to deployment
- // Note: set_deployment now requires beneficiary_photo and email which we don't have
- // device_form is simpler and just assigns the device to a deployment
+ // Per Robert's documentation: requires well_id + device_mac + deployment_id
const formData = new URLSearchParams({
function: 'device_form',
user_name: creds.userName,
token: creds.token,
- device_id: wellId.toString(),
- deployment: deploymentId.toString(),
+ well_id: wellId.toString(),
+ device_mac: deviceMac.toUpperCase(),
+ deployment_id: deploymentId.toString(),
});
const attachResponse = await fetch(this.legacyApiUrl, {
@@ -1932,7 +1928,6 @@ class ApiService {
});
if (!attachResponse.ok) {
- const errorText = await attachResponse.text();
throw new Error(`Failed to attach device: HTTP ${attachResponse.status}`);
}
diff --git a/services/wifiPasswordStore.ts b/services/wifiPasswordStore.ts
index f8cbd89..b753e78 100644
--- a/services/wifiPasswordStore.ts
+++ b/services/wifiPasswordStore.ts
@@ -22,8 +22,8 @@ export interface WiFiPasswordMap {
*/
export async function saveWiFiPassword(ssid: string, password: string): Promise {
try {
- // Get existing passwords
- const existing = await getAllWiFiPasswords();
+ // Get existing passwords (encrypted format)
+ const existing = await getAllWiFiPasswordsEncrypted();
// Encrypt the password
const encryptedPassword = await encrypt(password);
@@ -46,8 +46,8 @@ export async function saveWiFiPassword(ssid: string, password: string): Promise<
*/
export async function getWiFiPassword(ssid: string): Promise {
try {
- const passwords = await getAllWiFiPasswords();
- const encryptedPassword = passwords[ssid];
+ const encryptedPasswords = await getAllWiFiPasswordsEncrypted();
+ const encryptedPassword = encryptedPasswords[ssid];
if (!encryptedPassword) {
return undefined;
@@ -56,7 +56,7 @@ export async function getWiFiPassword(ssid: string): Promise
// Decrypt the password
const decryptedPassword = await decrypt(encryptedPassword);
return decryptedPassword;
- } catch (error) {
+ } catch {
return undefined;
}
}
@@ -75,7 +75,7 @@ async function getAllWiFiPasswordsEncrypted(): Promise {
}
return {};
- } catch (error) {
+ } catch {
return {};
}
}
@@ -93,13 +93,13 @@ export async function getAllWiFiPasswords(): Promise {
for (const [ssid, encryptedPassword] of Object.entries(encryptedPasswords)) {
try {
decryptedPasswords[ssid] = await decrypt(encryptedPassword);
- } catch (error) {
+ } catch {
// Skip this password if decryption fails
}
}
return decryptedPasswords;
- } catch (error) {
+ } catch {
return {};
}
}
@@ -171,9 +171,8 @@ export async function migrateToEncrypted(): Promise {
// Save back if any were migrated
if (migrated > 0) {
await SecureStore.setItemAsync(WIFI_PASSWORDS_KEY, JSON.stringify(encryptedPasswords));
- } else {
}
- } catch (error) {
+ } catch {
// Don't throw - migration failure shouldn't break the app
}
}
@@ -213,10 +212,8 @@ export async function migrateFromAsyncStorage(): Promise {
// Remove from AsyncStorage
await AsyncStorage.removeItem('WIFI_PASSWORDS');
await AsyncStorage.removeItem(LEGACY_SINGLE_PASSWORD_KEY);
-
- } else {
}
- } catch (error) {
+ } catch {
// Don't throw - migration failure shouldn't break the app
}
}