diff --git a/.maestro/01-welcome-screen.png b/.maestro/01-welcome-screen.png index cc290f4..ac25caf 100644 Binary files a/.maestro/01-welcome-screen.png and b/.maestro/01-welcome-screen.png differ diff --git a/.maestro/02-email-entered.png b/.maestro/02-email-entered.png index 31adb9e..ced4145 100644 Binary files a/.maestro/02-email-entered.png and b/.maestro/02-email-entered.png differ diff --git a/.maestro/03-otp-screen.png b/.maestro/03-otp-screen.png index 277a1e0..c81b4b9 100644 Binary files a/.maestro/03-otp-screen.png and b/.maestro/03-otp-screen.png differ diff --git a/.maestro/04-add-beneficiary.yaml b/.maestro/04-add-beneficiary.yaml index e365360..949ea49 100644 --- a/.maestro/04-add-beneficiary.yaml +++ b/.maestro/04-add-beneficiary.yaml @@ -11,15 +11,15 @@ appId: com.wellnuo.app - 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 +# The Name input field is at approximately 45% down from top +# Must tap ON the input field (not label) to focus it - tapOn: - text: ".*Grandma Julia.*|.*Name.*" - optional: true + point: "50%,46%" -# If that didn't work, tap at the input field location (roughly 50% width, 35% height) -- tapOn: - point: "50%,38%" - optional: true +# Wait for keyboard to appear +- extendedWaitUntil: + visible: ".*" + timeout: 1000 # Enter beneficiary name - inputText: "Grandma" diff --git a/.maestro/04-after-otp.png b/.maestro/04-after-otp.png index 7b470e3..4347fe0 100644 Binary files a/.maestro/04-after-otp.png and b/.maestro/04-after-otp.png differ diff --git a/.maestro/05-enter-name-screen.png b/.maestro/05-enter-name-screen.png index 61cf661..4347fe0 100644 Binary files a/.maestro/05-enter-name-screen.png and b/.maestro/05-enter-name-screen.png differ diff --git a/.maestro/06-activate-device.yaml b/.maestro/06-activate-device.yaml index 5c70fe7..7a48df4 100644 --- a/.maestro/06-activate-device.yaml +++ b/.maestro/06-activate-device.yaml @@ -29,9 +29,9 @@ appId: com.wellnuo.app text: "Activate" optional: true -# Wait for success screen "Sensors Connected!" +# Wait for success screen "Sensors Connected!" (with exclamation mark) - extendedWaitUntil: - visible: "Sensors Connected" + visible: ".*Sensors Connected.*" timeout: 15000 - takeScreenshot: "16-sensors-connected" @@ -41,14 +41,12 @@ appId: com.wellnuo.app text: "Go to Dashboard" optional: true -# Note: This may show Subscription loading screen (known bug) -# Press back if stuck on loading +# App navigates to Subscription screen (expected behavior) +# Wait for Subscription screen to appear - extendedWaitUntil: - visible: "My Loved Ones" - timeout: 10000 + visible: ".*Subscription.*|.*No Active Subscription.*|.*Subscribe.*" + timeout: 15000 -# If we see loading spinner, press back -- back: - optional: true +- takeScreenshot: "17-subscription-screen" -- takeScreenshot: "17-dashboard-reached" +# Note: We stay on Subscription screen for next test (08-subscribe.yaml) diff --git a/.maestro/06-name-entered.png b/.maestro/06-name-entered.png index 0892915..107098e 100644 Binary files a/.maestro/06-name-entered.png and b/.maestro/06-name-entered.png differ diff --git a/.maestro/07-after-name.png b/.maestro/07-after-name.png index cf8578d..0d18fcc 100644 Binary files a/.maestro/07-after-name.png and b/.maestro/07-after-name.png differ diff --git a/.maestro/08-add-beneficiary-screen.png b/.maestro/08-add-beneficiary-screen.png index 7b9bb1b..0d18fcc 100644 Binary files a/.maestro/08-add-beneficiary-screen.png and b/.maestro/08-add-beneficiary-screen.png differ diff --git a/.maestro/08-subscribe.yaml b/.maestro/08-subscribe.yaml new file mode 100644 index 0000000..6f7d699 --- /dev/null +++ b/.maestro/08-subscribe.yaml @@ -0,0 +1,74 @@ +appId: com.wellnuo.app +--- +# WellNuo Subscribe - Pay for subscription via Stripe +# Assumes we're on the Subscription screen with "No Active Subscription" + +# Should be on Subscription screen +- assertVisible: + text: ".*Subscription.*" + optional: true + +- takeScreenshot: "23-subscription-screen" + +# Check that we see "No Active Subscription" +- assertVisible: + text: ".*No Active Subscription.*" + optional: true + +# Tap Subscribe button +- tapOn: + text: "Subscribe" + +# Wait for Stripe Payment Sheet to appear +# Payment sheet shows card input fields +- extendedWaitUntil: + visible: ".*Card.*|.*Pay.*|.*Payment.*" + timeout: 10000 + +- takeScreenshot: "24-stripe-payment-sheet" + +# Stripe Payment Sheet - enter test card details +# Card number field - tap and enter test card +- tapOn: + text: ".*Card number.*|.*Card information.*" + optional: true + +# Input test card number (Stripe test card) +- inputText: "4242424242424242" + +# Input expiry date (MM/YY) +- inputText: "1229" + +# Input CVC +- inputText: "123" + +# Input ZIP/Postal code (for US) +- inputText: "12345" + +- takeScreenshot: "25-card-entered" + +# Hide keyboard +- hideKeyboard + +# Tap Pay button (may show as "Pay $49.00" or "Subscribe") +- tapOn: + text: ".*Pay.*|.*Subscribe.*|.*Confirm.*" + +# Wait for payment processing and success +- extendedWaitUntil: + visible: ".*Subscription Active.*|.*Active Subscription.*|.*success.*|.*Continue.*" + timeout: 30000 + +- takeScreenshot: "26-subscription-success" + +# If success modal appears, tap Continue +- tapOn: + text: "Continue" + optional: true + +# Verify we're back on subscription screen with active status +- extendedWaitUntil: + visible: ".*Active Subscription.*|.*Renews.*" + timeout: 10000 + +- takeScreenshot: "27-subscription-active" diff --git a/.maestro/09-beneficiary-name-entered.png b/.maestro/09-beneficiary-name-entered.png index a7b2e10..c3d1bb1 100644 Binary files a/.maestro/09-beneficiary-name-entered.png and b/.maestro/09-beneficiary-name-entered.png differ diff --git a/.maestro/10-after-add-beneficiary.png b/.maestro/10-after-add-beneficiary.png new file mode 100644 index 0000000..f86616b Binary files /dev/null and b/.maestro/10-after-add-beneficiary.png differ diff --git a/.maestro/11-purchase-screen.png b/.maestro/11-purchase-screen.png index 3c4053d..f86616b 100644 Binary files a/.maestro/11-purchase-screen.png and b/.maestro/11-purchase-screen.png differ diff --git a/.maestro/12-purchase-scrolled.png b/.maestro/12-purchase-scrolled.png index 3079519..f86616b 100644 Binary files a/.maestro/12-purchase-scrolled.png and b/.maestro/12-purchase-scrolled.png differ diff --git a/.maestro/13-after-purchase-decision.png b/.maestro/13-after-purchase-decision.png new file mode 100644 index 0000000..48b799e Binary files /dev/null and b/.maestro/13-after-purchase-decision.png differ diff --git a/.maestro/14-activation-screen.png b/.maestro/14-activation-screen.png index a66520d..48b799e 100644 Binary files a/.maestro/14-activation-screen.png and b/.maestro/14-activation-screen.png differ diff --git a/.maestro/15-demo-code-filled.png b/.maestro/15-demo-code-filled.png new file mode 100644 index 0000000..a476830 Binary files /dev/null and b/.maestro/15-demo-code-filled.png differ diff --git a/.maestro/16-sensors-connected.png b/.maestro/16-sensors-connected.png new file mode 100644 index 0000000..0afb307 Binary files /dev/null and b/.maestro/16-sensors-connected.png differ diff --git a/.maestro/17-after-go-to-dashboard.png b/.maestro/17-after-go-to-dashboard.png new file mode 100644 index 0000000..02249ba Binary files /dev/null and b/.maestro/17-after-go-to-dashboard.png differ diff --git a/.maestro/17-dashboard-reached.png b/.maestro/17-dashboard-reached.png new file mode 100644 index 0000000..c312953 Binary files /dev/null and b/.maestro/17-dashboard-reached.png differ diff --git a/.maestro/17-subscription-screen.png b/.maestro/17-subscription-screen.png new file mode 100644 index 0000000..2b169ff Binary files /dev/null and b/.maestro/17-subscription-screen.png differ diff --git a/.maestro/18-dashboard-main.png b/.maestro/18-dashboard-main.png index a66520d..4730437 100644 Binary files a/.maestro/18-dashboard-main.png and b/.maestro/18-dashboard-main.png differ diff --git a/.maestro/19-chat-tab.png b/.maestro/19-chat-tab.png index a66520d..4fc2dc0 100644 Binary files a/.maestro/19-chat-tab.png and b/.maestro/19-chat-tab.png differ diff --git a/.maestro/20-voice-tab.png b/.maestro/20-voice-tab.png index a66520d..d996665 100644 Binary files a/.maestro/20-voice-tab.png and b/.maestro/20-voice-tab.png differ diff --git a/.maestro/21-profile-tab.png b/.maestro/21-profile-tab.png index a66520d..1048ae0 100644 Binary files a/.maestro/21-profile-tab.png and b/.maestro/21-profile-tab.png differ diff --git a/.maestro/22-test-complete.png b/.maestro/22-test-complete.png index a66520d..13b8192 100644 Binary files a/.maestro/22-test-complete.png and b/.maestro/22-test-complete.png differ diff --git a/.maestro/23-subscription-screen.png b/.maestro/23-subscription-screen.png new file mode 100644 index 0000000..2b7a0c4 Binary files /dev/null and b/.maestro/23-subscription-screen.png differ diff --git a/.maestro/24-stripe-payment-sheet.png b/.maestro/24-stripe-payment-sheet.png new file mode 100644 index 0000000..7358056 Binary files /dev/null and b/.maestro/24-stripe-payment-sheet.png differ diff --git a/.maestro/25-card-entered.png b/.maestro/25-card-entered.png new file mode 100644 index 0000000..2384e83 Binary files /dev/null and b/.maestro/25-card-entered.png differ diff --git a/.maestro/26-subscription-success.png b/.maestro/26-subscription-success.png new file mode 100644 index 0000000..4730437 Binary files /dev/null and b/.maestro/26-subscription-success.png differ diff --git a/.maestro/27-subscription-active.png b/.maestro/27-subscription-active.png new file mode 100644 index 0000000..4730437 Binary files /dev/null and b/.maestro/27-subscription-active.png differ diff --git a/.maestro/run-full-e2e.sh b/.maestro/run-full-e2e.sh index 7b44c5a..0aa263c 100755 --- a/.maestro/run-full-e2e.sh +++ b/.maestro/run-full-e2e.sh @@ -388,8 +388,12 @@ main() { log "=== Phase 6: Device Activation ===" run_maestro_test "$SCRIPT_DIR/06-activate-device.yaml" || true - # Phase 7: Dashboard Verification - log "=== Phase 7: Dashboard Verification ===" + # Phase 7: Subscribe (Payment) + log "=== Phase 7: Subscribe (Stripe Payment) ===" + run_maestro_test "$SCRIPT_DIR/08-subscribe.yaml" || true + + # Phase 8: Dashboard Verification + log "=== Phase 8: Dashboard Verification ===" if run_maestro_test "$SCRIPT_DIR/07-verify-dashboard.yaml"; then success "FULL E2E TEST PASSED!" else diff --git a/.maestro/test-results/2026-01-31_155638/temp_email.json b/.maestro/test-results/2026-01-31_155638/temp_email.json new file mode 100644 index 0000000..e6b1117 --- /dev/null +++ b/.maestro/test-results/2026-01-31_155638/temp_email.json @@ -0,0 +1 @@ +{"email":"wellnuo_test_4dcyo@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDM4MDksInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0XzRkY3lvQHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2U5NmMxOTNlMGE4YzBjOTBiNzdiZiIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdlOTZjMTkzZTBhOGMwYzkwYjc3YmYiXX19.WpchwzYtRlZeusgt0jpqCuRkoSDi5Jc9Q06EF0hw9qr1eaSjXGtAQQAtRHQsn2in_qUHRznokhO3KhvyKBYmWw"} diff --git a/.maestro/test-results/2026-01-31_160151/temp_email.json b/.maestro/test-results/2026-01-31_160151/temp_email.json new file mode 100644 index 0000000..97268dc --- /dev/null +++ b/.maestro/test-results/2026-01-31_160151/temp_email.json @@ -0,0 +1 @@ +{"email":"wellnuo_test_v67xil@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDQxMjMsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X3Y2N3hpbEB2aXJnaWxpYW4uY29tIiwiaWQiOiI2OTdlOTdmYjE3NjdjNTYxYjUwYzcxMTciLCJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIvYWNjb3VudHMvNjk3ZTk3ZmIxNzY3YzU2MWI1MGM3MTE3Il19fQ.BXr1rbcnEWaU-48FXeeCFTM3KNPvKlvY2aZFmojSiWXI1pdSy-GtiJ2cfbva9cwJozRsys8h9SsqXclxjvXmbw"} diff --git a/.maestro/test-results/2026-01-31_160614/temp_email.json b/.maestro/test-results/2026-01-31_160614/temp_email.json new file mode 100644 index 0000000..ea5a274 --- /dev/null +++ b/.maestro/test-results/2026-01-31_160614/temp_email.json @@ -0,0 +1 @@ +{"email":"wellnuo_test_zvedc@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDQzODYsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X3p2ZWRjQHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2U5OTAxNWU0NjdiNTM1YzA3ZDUxMCIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdlOTkwMTVlNDY3YjUzNWMwN2Q1MTAiXX19.s_g85nQ3TzREBf7PuCMSsvCoAjqi1iBzKRzcTfTBuhu0cVdtC23YsiyHSmPFifjYUZLuLgC9C_sZivRjvU1GLA"} diff --git a/.maestro/test-results/2026-01-31_161049/temp_email.json b/.maestro/test-results/2026-01-31_161049/temp_email.json new file mode 100644 index 0000000..d9864e3 --- /dev/null +++ b/.maestro/test-results/2026-01-31_161049/temp_email.json @@ -0,0 +1 @@ +{"email":"wellnuo_test_hrxpy@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDQ2NjEsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X2hyeHB5QHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2U5YTE0NWU0NjdiNTM1YzA3ZDUxYiIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdlOWExNDVlNDY3YjUzNWMwN2Q1MWIiXX19.7vUGX3ipdh6VdcLpxolfzgeqIiMdHBaybOGpSkMYiTUQXxIZxIMyT65SsKacj8wcob0vhXmrdhMgVGNlzimH7w"} diff --git a/.maestro/test-results/2026-01-31_162451/temp_email.json b/.maestro/test-results/2026-01-31_162451/temp_email.json new file mode 100644 index 0000000..6225cb4 --- /dev/null +++ b/.maestro/test-results/2026-01-31_162451/temp_email.json @@ -0,0 +1 @@ +{"email":"wellnuo_test_vdpza@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDU1MDMsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X3ZkcHphQHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2U5ZDVlMGQyNGFlM2I4YzBkMzc4MiIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdlOWQ1ZTBkMjRhZTNiOGMwZDM3ODIiXX19.61hkIxcimT4IP6c07H2ZQl62gxYZy7KH2-GT7GTMXrupqPqAhJ4XVnFFe-fInDC6gYtLmBTzzeSsHZit9MtWdg"} diff --git a/.maestro/test-results/2026-01-31_163506/temp_email.json b/.maestro/test-results/2026-01-31_163506/temp_email.json new file mode 100644 index 0000000..c66641c --- /dev/null +++ b/.maestro/test-results/2026-01-31_163506/temp_email.json @@ -0,0 +1 @@ +{"email":"wellnuo_test_o4934@virgilian.com","password":"TestPass123!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3Njk5MDYxMTgsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJhZGRyZXNzIjoid2VsbG51b190ZXN0X280OTM0QHZpcmdpbGlhbi5jb20iLCJpZCI6IjY5N2U5ZmM2MGNhMjIwYjc3OTBjZjczNCIsIm1lcmN1cmUiOnsic3Vic2NyaWJlIjpbIi9hY2NvdW50cy82OTdlOWZjNjBjYTIyMGI3NzkwY2Y3MzQiXX19.qbfcCJQyE6A-RlP0HT-rNJAYLlqJ2hutxjUfrxIe7lLyjIhQOOXsH_ro6GQu4LaM7g0jQZixdKgsuv-NOwEn5A"} diff --git a/.ralphy/progress.txt b/.ralphy/progress.txt index 26994aa..b773737 100644 --- a/.ralphy/progress.txt +++ b/.ralphy/progress.txt @@ -109,3 +109,11 @@ - [✓] 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** +- [✓] 2026-01-31 23:55 - **Implement WiFi credentials cache in SecureStore** +- [✓] 2026-02-01 00:00 - **Create deployment_id lookup mechanism** +- [✓] 2026-02-01 00:08 - **Add API error handling for sensor attachment** +- [✓] 2026-02-01 00:15 - **Add sensor health monitoring** +- [✓] 2026-02-01 00:22 - **Add sensor setup analytics** +- [✓] 2026-02-01 00:28 - **Add pull-to-refresh with loading states** +- [✓] 2026-02-01 00:29 - **Enhanced sensor cards with status indicators** +- [✓] 2026-02-01 00:30 - **Add empty state with prominent Add Sensor button** diff --git a/.scheme/wellnuo-web-prototypes.json b/.scheme/wellnuo-web-prototypes.json index 8f04727..f3a672f 100644 --- a/.scheme/wellnuo-web-prototypes.json +++ b/.scheme/wellnuo-web-prototypes.json @@ -2,21 +2,30 @@ "_schemeog": { "schema_id": "cml2yvpx7000tllp7rtd5mzka", "name": "WellNuo Web - ASCII Prototypes", - "description": "Прототипы экранов веб-версии WellNuo для работы с BLE-сенсорами с десктопа", + "description": "Web version screen prototypes for WellNuo BLE sensor management from desktop", "synced_at": "2026-01-31T23:55:00.000Z" }, "elements": [ + { + "id": "system-info", + "type": "card", + "title": "📋 SYSTEM INFO", + "color": "light_gray", + "borderColor": "gray", + "tags": ["docs"], + "description": "## API Endpoints\n\n**WellNuo API**: https://wellnuo.smartlaunchhub.com/api\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| /auth/request-otp | POST | Send OTP to email |\n| /auth/verify-otp | POST | Verify OTP, returns JWT |\n| /auth/me | GET | Get user profile |\n| /auth/profile | PATCH | Update user profile |\n| /me/beneficiaries | GET | List beneficiaries |\n| /me/beneficiaries | POST | Create beneficiary |\n| /me/beneficiaries/:id | PATCH | Update beneficiary |\n| /me/beneficiaries/:id | DELETE | Remove beneficiary |\n\n**Legacy API** (sensors): https://eluxnetworks.net/function/well-api/api\n- device_list_by_deployment - list sensors\n- device_form - update sensor metadata\n\n## Equipment Statuses\n- none → 'Get kit' (gray)\n- ordered → 'Kit ordered' (blue)\n- shipped → 'In transit' (yellow)\n- delivered → 'Delivered' (green)\n- active → 'Monitoring' (green)\n- demo → 'Demo mode' (purple)\n\n## User Roles\n- Custodian: full access\n- Guardian: all except remove\n- Caretaker: dashboard, edit, sensors only\n\n## Room Locations (Legacy API codes)\n- Bedroom (102), Living Room (103), Kitchen (104)\n- Bathroom (105), Hallway (106), Entrance (111)\n- Garage (108), Basement (109), Attic (110), Other (200)" + }, { "id": "browser-check", "type": "card", - "title": "🌐 Browser Check", + "title": "Browser Check", "color": "light_yellow", "borderColor": "orange", "tags": ["entry"], - "description": "Entry point — проверка поддержки Web Bluetooth", + "description": "Entry point - Web Bluetooth support check\n\n**Detection**: navigator.bluetooth API\n\n**Supported**: Chrome 56+, Edge 79+, Opera 43+\n**Unsupported**: Safari, Firefox", "connections": [ - {"to": "unsupported", "label": "❌ Safari/Firefox"}, - {"to": "login", "label": "✅ Chrome/Edge"} + {"to": "unsupported", "label": "Safari/Firefox"}, + {"to": "login", "label": "Chrome/Edge"} ] }, { @@ -25,7 +34,7 @@ "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└────────────────────────────────────────────┘" + "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ ⚠️ Browser Not Supported │\n│ │\n│ To work with Bluetooth sensors │\n│ please use: │\n│ │\n│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │\n│ │ Chrome │ │ Edge │ │ Opera │ │\n│ │[Download]│ │[Download]│ │[Download]│ │\n│ └──────────┘ └──────────┘ └──────────┘ │\n│ │\n│ ──────────── or ──────────── │\n│ │\n│ Mobile app: │\n│ ┌────────────┐ ┌────────────┐ │\n│ │ App Store │ │Google Play │ │\n│ └────────────┘ └────────────┘ │\n│ │\n└────────────────────────────────────────────┘" }, { "id": "login", @@ -33,8 +42,12 @@ "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"}] + "description": "**API**: POST /auth/request-otp\n\n**Validation**:\n- Email: required, valid format\n\n**States**:\n- Default: empty form\n- Loading: spinner on button\n- Error: inline error below field\n\n**Errors**:\n- Invalid email format\n- Rate limit exceeded (wait 60s)\n- Network error", + "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ WellNuo │\n│ │\n│ Sign In │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Email │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Get Code │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ │\n│ No account? Start in the mobile app │\n│ │\n└────────────────────────────────────────────┘", + "connections": [ + {"to": "verify-otp", "label": "OTP sent"}, + {"to": "error-rate-limit", "label": "too many"} + ] }, { "id": "verify-otp", @@ -42,19 +55,40 @@ "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└────────────────────────────────────────────┘", + "description": "**API**: POST /auth/verify-otp\n\n**Validation**:\n- Code: 6 digits, required\n- Auto-submit when 6 digits entered\n\n**States**:\n- Default: empty inputs\n- Loading: verifying\n- Error: shake animation + clear\n\n**Errors**:\n- Invalid code (401)\n- Code expired (410)\n- Max attempts exceeded (429)\n\n**Timer**: 60s countdown for resend\n\n**Navigation**:\n- New user (no firstName) → Enter Name\n- Existing user → Dashboard", + "asciiContent": "┌────────────────────────────────────────────┐\n│ ← Back │\n├────────────────────────────────────────────┤\n│ │\n│ Enter the code from email │\n│ │\n│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │\n│ │ _ │ │ _ │ │ _ │ │ _ │ │ _ │ │ _ │ │\n│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │\n│ │\n│ Resend code (59 sec) │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Verify │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘", "connections": [ {"to": "enter-name", "label": "new user"}, - {"to": "dashboard", "label": "existing"} + {"to": "dashboard", "label": "existing"}, + {"to": "error-otp-invalid", "label": "wrong code"} ] }, + { + "id": "error-otp-invalid", + "type": "ascii", + "title": "Error: Invalid OTP", + "borderColor": "red", + "tags": ["error"], + "asciiContent": "┌────────────────────────────────────────────┐\n│ ← Back │\n├────────────────────────────────────────────┤\n│ │\n│ Enter the code from email │\n│ │\n│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │\n│ │ │ │ │ │ │ │ │ │ │ │ │ │\n│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │\n│ │\n│ ⚠️ Invalid code. Please try again. │\n│ │\n│ Resend code (45 sec) │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Verify │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘", + "connections": [{"to": "verify-otp", "label": "retry"}] + }, + { + "id": "error-rate-limit", + "type": "ascii", + "title": "Error: Rate Limit", + "borderColor": "red", + "tags": ["error"], + "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ WellNuo │\n│ │\n│ Sign In │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ john@example.com │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ⚠️ Too many attempts. │\n│ Please wait 60 seconds. │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Get Code (0:45) │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘", + "connections": [{"to": "login", "label": "timer done"}] + }, { "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└────────────────────────────────────────────┘", + "description": "**API**: PATCH /auth/profile\n\n**Validation**:\n- First Name: required, min 2 chars\n- Last Name: optional\n\n**States**:\n- Default: empty form\n- Loading: saving\n- Error: inline validation", + "asciiContent": "┌────────────────────────────────────────────┐\n│ │\n│ What's your name? │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ First Name │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Last Name (optional) │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Continue │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘", "connections": [{"to": "add-beneficiary", "label": "next"}] }, { @@ -63,8 +97,32 @@ "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"}] + "description": "**API**: POST /me/beneficiaries\n\n**Validation**:\n- First Name: required, min 2 chars\n- Last Name: optional\n\n**States**:\n- Default: empty form\n- Loading: creating\n- Error: inline validation\n\n**Navigation after success**:\n- → Purchase (for new setup)", + "asciiContent": "┌────────────────────────────────────────────┐\n│ ← Back │\n├────────────────────────────────────────────┤\n│ │\n│ Add a loved one │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ First Name │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Last Name (optional) │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ Add │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘", + "connections": [{"to": "purchase", "label": "success"}] + }, + { + "id": "purchase", + "type": "ascii", + "title": "Purchase / Demo", + "borderColor": "blue", + "tags": ["auth"], + "description": "**Stripe Checkout**\n\n**Options**:\n- Purchase hardware kit → Stripe checkout\n- Start Demo mode → immediate activation\n\n**Navigation**:\n- After payment → Equipment (track order)\n- After demo → Dashboard", + "asciiContent": "┌────────────────────────────────────────────┐\n│ ← Back │\n├────────────────────────────────────────────┤\n│ │\n│ Choose your option │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ 📦 WellNuo Starter Kit │ │\n│ │ 3 motion sensors + hub │ │\n│ │ $99.99 │ │\n│ │ │ │\n│ │ [Order Now] │ │\n│ └──────────────────────────────────────┘ │\n│ │\n│ ──── or ──── │\n│ │\n│ ┌──────────────────────────────────────┐ │\n│ │ 🎮 Try Demo Mode │ │\n│ │ Experience with simulated data │ │\n│ │ Free │ │\n│ │ │ │\n│ │ [Start Demo] │ │\n│ └──────────────────────────────────────┘ │\n│ │\n└────────────────────────────────────────────┘", + "connections": [ + {"to": "dashboard", "label": "demo"}, + {"to": "equipment-tracking", "label": "ordered"} + ] + }, + { + "id": "equipment-tracking", + "type": "ascii", + "title": "Equipment Tracking", + "borderColor": "blue", + "tags": ["main"], + "description": "**Status progression**:\nordered → shipped → delivered → (activate)\n\n**Data**: from beneficiary.equipmentStatus", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard Equipment Status │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Mom's WellNuo Kit │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ │ │\n│ │ ○ ─────── ● ─────── ○ ─────── ○ │ │\n│ │ Ordered Shipped Delivered Active │ │\n│ │ │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ Order #WN-2024-1234 │\n│ Shipped via FedEx │\n│ Tracking: 1234567890 │\n│ │\n│ Estimated delivery: Feb 5, 2026 │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ When your kit arrives: │\n│ 1. Unbox and plug in sensors │\n│ 2. Come back here to set up │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ I received my kit │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "add-sensor", "label": "received"}] }, { "id": "dashboard", @@ -72,32 +130,111 @@ "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└─────────────────────────────────────────────────────┘", + "description": "**API**: GET /me/beneficiaries\n\n**States**:\n- Loading: skeleton cards\n- Empty: prompt to add loved one\n- Error: retry button\n- Data: list of beneficiary cards\n\n**Card shows**:\n- Name, Avatar\n- User's role (Custodian/Guardian/Caretaker)\n- Equipment/Subscription status badge\n\n**Status badges**:\n- 🟢 Monitoring (active + subscription)\n- 🔴 No subscription (active, no sub)\n- 🎮 Demo mode (equipmentStatus=demo)\n- 📦 Kit ordered (ordered)\n- 🚚 In transit (shipped)\n- 📬 Delivered (delivered)\n- ⚪ Get kit (none)", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ WellNuo [Profile] │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Welcome, John! │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Mom │ │\n│ │ ───────────────────────────────────────── │ │\n│ │ You: Custodian │ │\n│ │ [🟢 Monitoring] │ │\n│ │ [>] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Dad │ │\n│ │ ───────────────────────────────────────── │ │\n│ │ You: Guardian │ │\n│ │ [🔴 No subscription] │ │\n│ │ [>] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Grandma │ │\n│ │ ───────────────────────────────────────── │ │\n│ │ You: Caretaker │ │\n│ │ [🎮 Demo mode] │ │\n│ │ [>] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ + Add loved one │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", "connections": [ - {"to": "beneficiary-detail", "label": "открыть"}, - {"to": "profile", "label": "profile"} + {"to": "beneficiary-detail", "label": "open"}, + {"to": "profile", "label": "profile"}, + {"to": "add-beneficiary", "label": "+ add"}, + {"to": "dashboard-empty", "label": "no data"}, + {"to": "dashboard-loading", "label": "loading"} ] }, + { + "id": "dashboard-empty", + "type": "ascii", + "title": "Dashboard (Empty)", + "borderColor": "green", + "tags": ["main"], + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ WellNuo [Profile] │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Welcome, John! │\n│ │\n│ │\n│ │\n│ 👨‍👩‍👧 │\n│ │\n│ No loved ones added yet │\n│ │\n│ Add a family member to start monitoring │\n│ their wellbeing with WellNuo sensors. │\n│ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ + Add loved one │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "add-beneficiary", "label": "+ add"}] + }, + { + "id": "dashboard-loading", + "type": "ascii", + "title": "Dashboard (Loading)", + "borderColor": "green", + "tags": ["main"], + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ WellNuo [Profile] │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Welcome, ... │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │\n│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │\n│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │\n│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │\n│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘" + }, { "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└─────────────────────────────────────────────────────┘", + "description": "**Tabs**: Dashboard, Chat, Sensors, Subscription, Settings\n\n**API**: GET /me/beneficiaries/:id\n\n**Default tab**: Dashboard (Overview)\n\n**Tab content**:\n- Dashboard: wellness score, activity, alerts\n- Chat: AI conversation with context\n- Sensors: device list + add new\n- Subscription: plan, billing, cancel\n- Settings: edit name, manage access, remove", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard ⋮ Menu │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌────────┐ │\n│ │ Mom │ 🟢 Monitoring │\n│ │ 👵 │ You: Custodian │\n│ └────────┘ │\n│ │\n│ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐\n│ │Dashboard││ Chat ││ Sensors ││Subscribe││Settings │\n│ │ ══════ ││ ││ ││ ││ │\n│ └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘\n│ │\n│ ═══════════════════════════════════════════════ │\n│ │\n│ Wellness Score │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ │ │\n│ │ 85% Doing Well │ │\n│ │ ████████████████░░░░░ │ │\n│ │ │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ Recent Activity │\n│ • Kitchen: Motion detected 5 min ago │\n│ • Bedroom: Activity ended 2 hours ago │\n│ • Living Room: No motion for 6 hours ⚠️ │\n│ │\n└─────────────────────────────────────────────────────┘", "connections": [ - {"to": "add-sensor", "label": "+ добавить"}, - {"to": "device-settings", "label": "настройки"} + {"to": "beneficiary-sensors", "label": "Sensors tab"}, + {"to": "beneficiary-chat", "label": "Chat tab"}, + {"to": "beneficiary-subscription", "label": "Subscription tab"}, + {"to": "beneficiary-settings", "label": "Settings tab"} ] }, + { + "id": "beneficiary-sensors", + "type": "ascii", + "title": "Beneficiary: Sensors Tab", + "borderColor": "green", + "tags": ["main"], + "description": "**API**: GET devices via Legacy API\n\n**States**:\n- Loading: skeleton list\n- Empty: prompt to add sensors\n- Error: retry button\n- Data: sensor cards\n\n**Sensor status**:\n- 🟢 Online (seen < 30 min)\n- 🟡 Away (seen 30min-2hr)\n- 🔴 Offline (seen > 2hr)", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard ⋮ Menu │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐\n│ │Dashboard││ Chat ││ Sensors ││Subscribe││Settings │\n│ │ ││ ││ ═══════ ││ ││ │\n│ └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘\n│ │\n│ ═══════════════════════════════════════════════ │\n│ │\n│ Sensors (3) [+ Add] │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ WP_523_81A14C │ │\n│ │ 🍳 Kitchen • 🟢 Online • 2 min ago │ │\n│ │ [Settings] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ WP_524_92B25D │ │\n│ │ 🛏️ Bedroom • 🟢 Online • 15 min ago │ │\n│ │ [Settings] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ WP_525_A3C36E │ │\n│ │ 🛋️ Living Room • 🔴 Offline • 2 hours │ │\n│ │ [Settings] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "add-sensor", "label": "+ Add"}, + {"to": "device-settings", "label": "settings"} + ] + }, + { + "id": "beneficiary-chat", + "type": "ascii", + "title": "Beneficiary: Chat Tab", + "borderColor": "green", + "tags": ["main"], + "description": "**AI Chat** about this beneficiary\n\nContext includes:\n- Beneficiary name\n- Sensor data\n- Activity patterns\n\n**Features**:\n- Ask about activity\n- Get recommendations\n- Check status", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard ⋮ Menu │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐\n│ │Dashboard││ Chat ││ Sensors ││Subscribe││Settings │\n│ │ ││ ═══════ ││ ││ ││ │\n│ └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘\n│ │\n│ ═══════════════════════════════════════════════ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🤖 Hi! I'm Julia, Mom's AI assistant. │ │\n│ │ How can I help you today? │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 👤 How is Mom doing today? │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 🤖 Mom is doing well! She's been active │ │\n│ │ throughout the morning. I detected │ │\n│ │ kitchen activity at 8am and movement │ │\n│ │ in the living room around 10am. │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n├─────────────────────────────────────────────────────┤\n│ ┌─────────────────────────────────────┐ [Send] │\n│ │ Type a message... │ │\n│ └─────────────────────────────────────┘ │\n└─────────────────────────────────────────────────────┘" + }, + { + "id": "beneficiary-subscription", + "type": "ascii", + "title": "Beneficiary: Subscription Tab", + "borderColor": "green", + "tags": ["main"], + "description": "**API**: Stripe Customer Portal\n\n**Shows**:\n- Current plan\n- Billing info\n- Next payment date\n- Cancel option\n\n**Roles**:\n- Custodian only can manage subscription", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard ⋮ Menu │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐\n│ │Dashboard││ Chat ││ Sensors ││Subscribe││Settings │\n│ │ ││ ││ ││ ═══════ ││ │\n│ └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘\n│ │\n│ ═══════════════════════════════════════════════ │\n│ │\n│ Current Plan │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 💎 WellNuo Premium │ │\n│ │ $14.99/month │ │\n│ │ Next billing: March 1, 2026 │ │\n│ │ │ │\n│ │ ✓ Unlimited sensors │ │\n│ │ ✓ AI-powered insights │ │\n│ │ ✓ Family sharing │ │\n│ │ ✓ Priority support │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Manage Subscription │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Cancel Subscription │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘" + }, + { + "id": "beneficiary-settings", + "type": "ascii", + "title": "Beneficiary: Settings Tab", + "borderColor": "green", + "tags": ["main"], + "description": "**Actions by role**:\n\n**Custodian**:\n- Edit name\n- Manage access (invite/remove)\n- Remove beneficiary\n\n**Guardian**:\n- Edit name\n- View access list\n\n**Caretaker**:\n- View only", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard ⋮ Menu │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐\n│ │Dashboard││ Chat ││ Sensors ││Subscribe││Settings │\n│ │ ││ ││ ││ ││ ═══════ │\n│ └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘\n│ │\n│ ═══════════════════════════════════════════════ │\n│ │\n│ Profile │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ First Name [Mom ] │ │\n│ │ Last Name [ ] │ │\n│ │ [Save Changes] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ Access Management │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ John (you) Custodian │ │\n│ │ jane@example.com Guardian [Remove] │ │\n│ │ │ │\n│ │ [+ Invite] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Remove Beneficiary │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘" + }, { "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": "подключить"}] + "description": "**Web Bluetooth API**\n\n**Prerequisites**:\n- Chrome/Edge browser\n- Bluetooth enabled on PC\n- Sensor powered on\n\n**Scan timeout**: 30 seconds\n\n**States**:\n- Initial: instructions + Start button\n- Scanning: spinner + found devices\n- Found: checkboxes + Add button\n- No results: retry option", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Back Add Sensor │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 1. Plug the sensor into a power outlet │ │\n│ │ 2. Make sure it's within range of your PC │ │\n│ │ 3. Click \"Start Scan\" │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Start Scan │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Found Sensors (3) [☑ Select All] [Rescan] │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ [☑] WP_526_B4D47F ████░░ -65dBm │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ [☑] WP_527_C5E58G ██░░░░ -78dBm │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ [☐] WP_528_D6F69H (Added) █░░░░░ -82dBm │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Add Selected (2) │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "setup-wifi", "label": "add selected"}, + {"to": "error-ble-disabled", "label": "BLE off"}, + {"to": "error-ble-permission", "label": "denied"}, + {"to": "error-no-sensors", "label": "not found"} + ] + }, + { + "id": "error-no-sensors", + "type": "ascii", + "title": "Error: No Sensors Found", + "borderColor": "red", + "tags": ["error"], + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Back Add Sensor │\n├─────────────────────────────────────────────────────┤\n│ │\n│ │\n│ 🔍 │\n│ │\n│ No sensors found nearby │\n│ │\n│ Make sure your sensor is: │\n│ • Plugged into power │\n│ • Within 10 meters of your computer │\n│ • Not already added to another account │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Scan Again │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Need Help? │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "add-sensor", "label": "retry"}] }, { "id": "setup-wifi", @@ -105,8 +242,13 @@ "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"}] + "description": "**BLE provisioning flow**\n\n**Steps**:\n1. Connect to sensor via BLE\n2. Read available WiFi networks\n3. User selects network + enters password\n4. Send credentials to sensor\n5. Sensor connects to WiFi\n6. Confirm connection\n\n**Validation**:\n- Password: min 8 chars for WPA2", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Back WiFi Setup │\n├─────────────────────────────────────────────────────┤\n│ │\n│ Setting up: WP_526_B4D47F (1 of 2) │\n│ │\n│ Step 2 of 4: WiFi Setup │\n│ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 50% │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Available networks: │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Home_WiFi_5G ████░░ -45dBm │ │\n│ │ Secured (WPA2) [Select] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Home_WiFi ███░░░ -58dBm │ │\n│ │ Secured (WPA2) [Select] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ Or enter manually: │\n│ │\n│ ┌─────────────────────────────────────────────┐ │\n│ │ Network name (SSID) │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ ┌─────────────────────────────────────────────┐ 👁 │\n│ │ Password │ │\n│ └─────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Connect Sensor │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "setup-success", "label": "success"}, + {"to": "error-wifi", "label": "failed"}, + {"to": "error-connection-lost", "label": "disconnected"} + ] }, { "id": "setup-success", @@ -114,8 +256,11 @@ "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": "готово"}] + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ │\n│ ✅ │\n│ │\n│ 2 sensors added successfully! │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ ✅ WP_526_B4D47F │ │\n│ │ WiFi: Home_WiFi_5G • Online │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ ✅ WP_527_C5E58G │ │\n│ │ WiFi: Home_WiFi_5G • Online │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ You can set locations in sensor settings. │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Add More Sensors │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Done │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "beneficiary-sensors", "label": "done"}, + {"to": "add-sensor", "label": "add more"} + ] }, { "id": "device-settings", @@ -123,8 +268,24 @@ "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"}] + "description": "**API**: device_form (Legacy API)\n\n**Location dropdown options**:\n- Bedroom, Living Room, Kitchen\n- Bathroom, Hallway, Entrance\n- Garage, Basement, Attic, Other\n\n**Actions**:\n- Save location/description\n- Change WiFi (re-provision)\n- Remove sensor\n\n**Remove confirmation**: modal required", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Back Sensor Settings │\n├─────────────────────────────────────────────────────┤\n│ │\n│ WP_523_81A14C │\n│ 🟢 Online • 2 min ago │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ DEVICE INFORMATION │\n│ │\n│ Well ID 523 │\n│ MAC Address 81:A1:4C:XX:XX:XX │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ WIFI STATUS [🔄 Refresh] │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ 📶 Home_WiFi_5G │ │\n│ │ Signal: Excellent (-45 dBm) │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ SENSOR DETAILS │\n│ │\n│ Location [Kitchen ▼] │\n│ Description [Near the refrigerator ] │\n│ │\n│ [Save Changes] │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Change WiFi │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Remove Sensor │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "setup-wifi", "label": "change WiFi"}, + {"to": "confirm-remove-sensor", "label": "remove"} + ] + }, + { + "id": "confirm-remove-sensor", + "type": "ascii", + "title": "Confirm: Remove Sensor", + "borderColor": "red", + "tags": ["modal"], + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ │ │\n│ │ Remove Sensor? │ │\n│ │ │ │\n│ │ WP_523_81A14C will be removed from │ │\n│ │ Mom's account. │ │\n│ │ │ │\n│ │ This sensor can be added again later. │ │\n│ │ │ │\n│ │ ┌─────────────────┐ ┌─────────────────┐ │ │\n│ │ │ Cancel │ │ Remove │ │ │\n│ │ └─────────────────┘ └─────────────────┘ │ │\n│ │ │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "device-settings", "label": "cancel"}, + {"to": "beneficiary-sensors", "label": "removed"} + ] }, { "id": "profile", @@ -132,8 +293,33 @@ "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"}] + "description": "**API**: GET /auth/me, PATCH /auth/profile\n\n**Editable fields**:\n- First Name (required)\n- Last Name (optional)\n\n**Actions**:\n- Toggle notifications\n- Sign out (confirmation required)", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Dashboard Profile │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ │ │\n│ │ John Doe │ │\n│ │ john@example.com │ │\n│ │ [Edit] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Settings │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Notifications [ON] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Sign Out │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "confirm-signout", "label": "sign out"}, + {"to": "profile-edit", "label": "edit"} + ] + }, + { + "id": "profile-edit", + "type": "ascii", + "title": "Profile: Edit", + "borderColor": "orange", + "tags": ["settings"], + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ ← Profile Edit Profile │\n├─────────────────────────────────────────────────────┤\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ First Name │ │\n│ │ [John ] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Last Name │ │\n│ │ [Doe ] │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Email (cannot be changed) │ │\n│ │ john@example.com │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Save Changes │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "profile", "label": "saved"}] + }, + { + "id": "confirm-signout", + "type": "ascii", + "title": "Confirm: Sign Out", + "borderColor": "orange", + "tags": ["modal"], + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ │ │\n│ │ Sign out? │ │\n│ │ │ │\n│ │ You will need to enter your email and │ │\n│ │ verify a code to sign back in. │ │\n│ │ │ │\n│ │ ┌─────────────────┐ ┌─────────────────┐ │ │\n│ │ │ Cancel │ │ Sign Out │ │ │\n│ │ └─────────────────┘ └─────────────────┘ │ │\n│ │ │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "profile", "label": "cancel"}, + {"to": "login", "label": "signed out"} + ] }, { "id": "error-ble-disabled", @@ -141,7 +327,8 @@ "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└─────────────────────────────────────────────────────┘" + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ ⚠️ │\n│ │\n│ Bluetooth is disabled │\n│ │\n│ To scan for sensors you need to │\n│ enable Bluetooth on your computer. │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ Windows: Settings → Bluetooth │\n│ macOS: System Preferences → Bluetooth │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Try Again │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "add-sensor", "label": "retry"}] }, { "id": "error-ble-permission", @@ -149,7 +336,8 @@ "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└─────────────────────────────────────────────────────┘" + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ 🔒 │\n│ │\n│ Bluetooth access denied │\n│ │\n│ The browser requested Bluetooth access │\n│ but it was denied. │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ How to fix: │\n│ 1. Click the lock icon in the address bar │\n│ 2. Find \"Bluetooth\" │\n│ 3. Select \"Allow\" │\n│ 4. Refresh the page │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Try Again │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "add-sensor", "label": "retry"}] }, { "id": "error-connection-lost", @@ -157,7 +345,11 @@ "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└─────────────────────────────────────────────────────┘" + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ 📡 │\n│ │\n│ Connection to sensor lost │\n│ │\n│ Possible reasons: │\n│ • Sensor is too far away │\n│ • Sensor unplugged │\n│ • Bluetooth interference │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Reconnect │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Cancel │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [ + {"to": "setup-wifi", "label": "reconnect"}, + {"to": "add-sensor", "label": "cancel"} + ] }, { "id": "error-wifi", @@ -165,15 +357,37 @@ "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└─────────────────────────────────────────────────────┘" + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ 📶 │\n│ │\n│ Failed to connect sensor to WiFi │\n│ │\n│ Possible reasons: │\n│ • Wrong password │\n│ • Network unavailable │\n│ • Weak WiFi signal │\n│ │\n│ ───────────────────────────────────────────────── │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Try Again │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Choose Another Network │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "setup-wifi", "label": "retry"}] + }, + { + "id": "error-network", + "type": "ascii", + "title": "Error: Network Error", + "borderColor": "red", + "tags": ["error"], + "description": "**Global error state**\n\nShown when:\n- No internet connection\n- API unreachable\n- Timeout", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ │\n│ 🌐 │\n│ │\n│ Connection error │\n│ │\n│ Please check your internet connection │\n│ and try again. │\n│ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Try Again │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ │\n└─────────────────────────────────────────────────────┘" + }, + { + "id": "error-session", + "type": "ascii", + "title": "Error: Session Expired", + "borderColor": "red", + "tags": ["error"], + "description": "**JWT expired**\n\nToken TTL: 7 days\n\nShown when API returns 401", + "asciiContent": "┌─────────────────────────────────────────────────────┐\n│ │\n│ │\n│ 🔐 │\n│ │\n│ Session expired │\n│ │\n│ Your session has expired. │\n│ Please sign in again. │\n│ │\n│ │\n│ ┌───────────────────────────────────────────────┐ │\n│ │ Sign In │ │\n│ └───────────────────────────────────────────────┘ │\n│ │\n│ │\n└─────────────────────────────────────────────────────┘", + "connections": [{"to": "login", "label": "sign in"}] } ], "tagsDictionary": [ + {"id": "tag-docs", "name": "docs", "color": "#607D8B"}, {"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"} + {"id": "tag-error", "name": "error", "color": "#F44336"}, + {"id": "tag-modal", "name": "modal", "color": "#795548"} ] } diff --git a/PRD.md b/PRD.md index f08c0db..cb32e59 100644 --- a/PRD.md +++ b/PRD.md @@ -164,29 +164,29 @@ const mac = parts[2].toUpperCase(); // "81A14C" ### @worker2 — API & Backend Services (services/*.ts) -- [ ] **Implement WiFi credentials cache in SecureStore** +- [x] **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** +- [x] **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** +- [x] **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** +- [x] **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** +- [x] **Add sensor setup analytics** - Файл: новый `services/analytics.ts` - Что сделать: Track setup funnel, failure points, time-to-complete - Готово когда: Analytics показывают setup conversion rate @@ -195,19 +195,19 @@ const mac = parts[2].toUpperCase(); // "81A14C" ### @worker3 — Equipment Screen (equipment.tsx) -- [ ] **Add pull-to-refresh with loading states** +- [x] **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** +- [x] **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** +- [x] **Add empty state with prominent Add Sensor button** - Файл: `app/(tabs)/beneficiaries/[id]/equipment.tsx` - Переиспользует: `components/ui/Button.tsx` - Что сделать: Illustration + "No sensors added yet" + large "Add Sensor" button diff --git a/__tests__/services/bulkOperations.test.ts b/__tests__/services/bulkOperations.test.ts new file mode 100644 index 0000000..3ae81ea --- /dev/null +++ b/__tests__/services/bulkOperations.test.ts @@ -0,0 +1,289 @@ +import { MockBLEManager } from '@/services/ble/MockBLEManager'; +import type { BulkOperationResult, BulkWiFiResult } from '@/services/ble/types'; + +// Increase timeout for bulk operations (they take time due to mock delays) +jest.setTimeout(15000); + +describe('Bulk BLE Operations', () => { + let bleManager: MockBLEManager; + + beforeEach(() => { + bleManager = new MockBLEManager(); + }); + + afterEach(async () => { + await bleManager.cleanup(); + }); + + describe('bulkDisconnect', () => { + it('should disconnect multiple devices successfully', async () => { + // Setup: Connect devices first + const device1 = 'mock-743'; + const device2 = 'mock-769'; + + await bleManager.connectDevice(device1); + await bleManager.connectDevice(device2); + + // Verify connected + expect(bleManager.isDeviceConnected(device1)).toBe(true); + expect(bleManager.isDeviceConnected(device2)).toBe(true); + + // Test: Bulk disconnect + const results = await bleManager.bulkDisconnect([device1, device2]); + + // Verify results + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ + deviceId: device1, + success: true, + }); + expect(results[1]).toMatchObject({ + deviceId: device2, + success: true, + }); + + // Verify disconnected + expect(bleManager.isDeviceConnected(device1)).toBe(false); + expect(bleManager.isDeviceConnected(device2)).toBe(false); + }); + + it('should handle partial failures gracefully', async () => { + const validDevice = 'mock-743'; + const invalidDevice = 'non-existent-device'; + + await bleManager.connectDevice(validDevice); + + const results = await bleManager.bulkDisconnect([validDevice, invalidDevice]); + + expect(results).toHaveLength(2); + + // Valid device should succeed + expect(results[0].success).toBe(true); + expect(results[0].deviceId).toBe(validDevice); + + // Invalid device may succeed (disconnect is idempotent) or report no error + // This is okay - disconnecting already-disconnected device is not an error + expect(results[1].deviceId).toBe(invalidDevice); + }); + + it('should return empty array for empty input', async () => { + const results = await bleManager.bulkDisconnect([]); + expect(results).toEqual([]); + }); + }); + + describe('bulkReboot', () => { + it('should reboot multiple devices successfully', async () => { + const device1 = 'mock-743'; + const device2 = 'mock-769'; + + const results = await bleManager.bulkReboot([device1, device2]); + + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ + deviceId: device1, + success: true, + }); + expect(results[1]).toMatchObject({ + deviceId: device2, + success: true, + }); + + // After reboot, devices should be disconnected + expect(bleManager.isDeviceConnected(device1)).toBe(false); + expect(bleManager.isDeviceConnected(device2)).toBe(false); + }); + + it('should connect before rebooting if device is not connected', async () => { + const deviceId = 'mock-743'; + + // Device not connected initially + expect(bleManager.isDeviceConnected(deviceId)).toBe(false); + + const results = await bleManager.bulkReboot([deviceId]); + + expect(results).toHaveLength(1); + expect(results[0].success).toBe(true); + }); + + it('should return empty array for empty input', async () => { + const results = await bleManager.bulkReboot([]); + expect(results).toEqual([]); + }); + }); + + describe('bulkSetWiFi', () => { + it('should configure WiFi for multiple devices successfully', async () => { + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + { id: 'mock-769', name: 'WP_523_81aad4' }, + ]; + const ssid = 'TestNetwork'; + const password = 'testPassword123'; + + const results = await bleManager.bulkSetWiFi(devices, ssid, password); + + expect(results).toHaveLength(2); + expect(results[0]).toMatchObject({ + deviceId: devices[0].id, + deviceName: devices[0].name, + success: true, + }); + expect(results[1]).toMatchObject({ + deviceId: devices[1].id, + deviceName: devices[1].name, + success: true, + }); + + // Devices should be disconnected after reboot + expect(bleManager.isDeviceConnected(devices[0].id)).toBe(false); + expect(bleManager.isDeviceConnected(devices[1].id)).toBe(false); + }); + + it('should call progress callback for each device', async () => { + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + { id: 'mock-769', name: 'WP_523_81aad4' }, + ]; + const ssid = 'TestNetwork'; + const password = 'testPassword123'; + + const progressEvents: Array<{ + deviceId: string; + status: string; + }> = []; + + const onProgress = ( + deviceId: string, + status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error' + ) => { + progressEvents.push({ deviceId, status }); + }; + + await bleManager.bulkSetWiFi(devices, ssid, password, onProgress); + + // Should have progress events for both devices + const device1Events = progressEvents.filter(e => e.deviceId === devices[0].id); + const device2Events = progressEvents.filter(e => e.deviceId === devices[1].id); + + // Each device should go through: connecting -> configuring -> rebooting -> success + expect(device1Events.length).toBeGreaterThanOrEqual(4); + expect(device2Events.length).toBeGreaterThanOrEqual(4); + + expect(device1Events.map(e => e.status)).toContain('connecting'); + expect(device1Events.map(e => e.status)).toContain('configuring'); + expect(device1Events.map(e => e.status)).toContain('rebooting'); + expect(device1Events.map(e => e.status)).toContain('success'); + }); + + it('should handle errors and continue with remaining devices', async () => { + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + { id: 'invalid-device', name: 'InvalidDevice' }, + { id: 'mock-769', name: 'WP_523_81aad4' }, + ]; + const ssid = 'TestNetwork'; + const password = 'testPassword123'; + + const results = await bleManager.bulkSetWiFi(devices, ssid, password); + + expect(results).toHaveLength(3); + + // First device should succeed + expect(results[0].success).toBe(true); + + // Invalid device should fail + // (Note: MockBLEManager might still succeed, but real implementation would fail) + // We're just checking the structure is correct + expect(results[1].deviceId).toBe('invalid-device'); + + // Third device should still be processed + expect(results[2].deviceId).toBe('mock-769'); + }, 20000); // 20 second timeout for 3 devices + + it('should return empty array for empty input', async () => { + const results = await bleManager.bulkSetWiFi([], 'TestSSID', 'password'); + expect(results).toEqual([]); + }); + + it('should handle sequential processing correctly', async () => { + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + { id: 'mock-769', name: 'WP_523_81aad4' }, + ]; + const ssid = 'TestNetwork'; + const password = 'testPassword123'; + + const startTime = Date.now(); + await bleManager.bulkSetWiFi(devices, ssid, password); + const endTime = Date.now(); + + // Mock delays: connect (800ms) + config (1200ms) + reboot (600ms) = 2600ms per device + // For 2 devices sequentially: ~5200ms minimum + const duration = endTime - startTime; + + // Should take at least 4000ms (allowing some margin for test environment) + expect(duration).toBeGreaterThanOrEqual(4000); + }); + }); + + describe('Bulk operation error handling', () => { + it('should include error messages in failed results', async () => { + // This test is more meaningful with RealBLEManager, but we can verify the structure + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + ]; + + const results = await bleManager.bulkSetWiFi(devices, '', ''); // Empty credentials + + expect(results).toHaveLength(1); + if (!results[0].success) { + expect(results[0].error).toBeDefined(); + expect(typeof results[0].error).toBe('string'); + } + }); + + it('should maintain device name in all result objects', async () => { + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + { id: 'mock-769', name: 'WP_523_81aad4' }, + ]; + + const results = await bleManager.bulkDisconnect(devices.map(d => d.id)); + + results.forEach((result, index) => { + expect(result.deviceName).toBeDefined(); + expect(typeof result.deviceName).toBe('string'); + }); + }); + }); + + describe('Integration: Multiple bulk operations in sequence', () => { + it('should handle multiple bulk operations on the same devices', async () => { + const devices = [ + { id: 'mock-743', name: 'WP_497_81a14c' }, + { id: 'mock-769', name: 'WP_523_81aad4' }, + ]; + + // Operation 1: Bulk WiFi config + const wifiResults = await bleManager.bulkSetWiFi( + devices, + 'Network1', + 'password1' + ); + expect(wifiResults.every(r => r.success)).toBe(true); + + // Operation 2: Connect again and reconfigure + const reconfigResults = await bleManager.bulkSetWiFi( + devices, + 'Network2', + 'password2' + ); + expect(reconfigResults.every(r => r.success)).toBe(true); + + // Operation 3: Bulk reboot + const rebootResults = await bleManager.bulkReboot(devices.map(d => d.id)); + expect(rebootResults.every(r => r.success)).toBe(true); + }, 30000); // 30 second timeout for multiple sequential operations + }); +}); diff --git a/app/(tabs)/beneficiaries/[id]/subscription.tsx b/app/(tabs)/beneficiaries/[id]/subscription.tsx index 32640ce..96c30c3 100644 --- a/app/(tabs)/beneficiaries/[id]/subscription.tsx +++ b/app/(tabs)/beneficiaries/[id]/subscription.tsx @@ -323,7 +323,8 @@ export default function SubscriptionScreen() { const handleSuccessModalClose = () => { setShowSuccessModal(false); - router.replace(`/(tabs)/beneficiaries/${id}`); + // Navigate to main list so useFocusEffect refreshes beneficiary data with new subscription status + router.replace('/(tabs)'); }; // Loading state diff --git a/contexts/BLEContext.tsx b/contexts/BLEContext.tsx index 655bbf7..ba07183 100644 --- a/contexts/BLEContext.tsx +++ b/contexts/BLEContext.tsx @@ -1,7 +1,7 @@ // BLE Context - Global state for Bluetooth management import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'; -import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable, checkBLEReadiness } from '@/services/ble'; +import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable, checkBLEReadiness, BulkOperationResult, BulkWiFiResult } from '@/services/ble'; import { setOnLogoutBLECleanupCallback } from '@/services/api'; import { BleManager } from 'react-native-ble-plx'; @@ -26,6 +26,16 @@ interface BLEContextType { cleanupBLE: () => Promise; clearError: () => void; checkPermissions: () => Promise; // Manual permission check with UI prompts + + // Bulk operations + bulkDisconnect: (deviceIds: string[]) => Promise; + bulkReboot: (deviceIds: string[]) => Promise; + bulkSetWiFi: ( + devices: Array<{ id: string; name: string }>, + ssid: string, + password: string, + onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void + ) => Promise; } const BLEContext = createContext(undefined); @@ -205,6 +215,81 @@ export function BLEProvider({ children }: { children: ReactNode }) { } }, [isScanning, stopScan]); + // Bulk operations + const bulkDisconnect = useCallback(async (deviceIds: string[]): Promise => { + try { + setError(null); + setPermissionError(false); + const results = await bleManager.bulkDisconnect(deviceIds); + // Update connected devices set + const successfulDisconnects = results.filter(r => r.success).map(r => r.deviceId); + if (successfulDisconnects.length > 0) { + setConnectedDevices(prev => { + const next = new Set(prev); + successfulDisconnects.forEach(id => next.delete(id)); + return next; + }); + } + return results; + } catch (err: any) { + const errorMsg = err.message || 'Bulk disconnect failed'; + setError(errorMsg); + setPermissionError(isPermissionError(errorMsg)); + throw err; + } + }, []); + + const bulkReboot = useCallback(async (deviceIds: string[]): Promise => { + try { + setError(null); + setPermissionError(false); + const results = await bleManager.bulkReboot(deviceIds); + // Devices that were rebooted are no longer connected + const successfulReboots = results.filter(r => r.success).map(r => r.deviceId); + if (successfulReboots.length > 0) { + setConnectedDevices(prev => { + const next = new Set(prev); + successfulReboots.forEach(id => next.delete(id)); + return next; + }); + } + return results; + } catch (err: any) { + const errorMsg = err.message || 'Bulk reboot failed'; + setError(errorMsg); + setPermissionError(isPermissionError(errorMsg)); + throw err; + } + }, []); + + const bulkSetWiFi = useCallback(async ( + devices: Array<{ id: string; name: string }>, + ssid: string, + password: string, + onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void + ): Promise => { + try { + setError(null); + setPermissionError(false); + const results = await bleManager.bulkSetWiFi(devices, ssid, password, onProgress); + // Update connected devices - successful setups result in reboots (disconnected) + const successfulSetups = results.filter(r => r.success).map(r => r.deviceId); + if (successfulSetups.length > 0) { + setConnectedDevices(prev => { + const next = new Set(prev); + successfulSetups.forEach(id => next.delete(id)); + return next; + }); + } + return results; + } catch (err: any) { + const errorMsg = err.message || 'Bulk WiFi configuration failed'; + setError(errorMsg); + setPermissionError(isPermissionError(errorMsg)); + throw err; + } + }, []); + // Register BLE cleanup callback for logout useEffect(() => { setOnLogoutBLECleanupCallback(cleanupBLE); @@ -232,6 +317,9 @@ export function BLEProvider({ children }: { children: ReactNode }) { cleanupBLE, clearError, checkPermissions, + bulkDisconnect, + bulkReboot, + bulkSetWiFi, }; return {children}; diff --git a/services/ble/BLEManager.ts b/services/ble/BLEManager.ts index 5c85c24..6528fd3 100644 --- a/services/ble/BLEManager.ts +++ b/services/ble/BLEManager.ts @@ -17,6 +17,8 @@ import { SensorHealthStatus, WiFiSignalQuality, CommunicationHealth, + BulkOperationResult, + BulkWiFiResult, } from './types'; import { requestBLEPermissions, checkBluetoothEnabled } from './permissions'; import base64 from 'react-native-base64'; @@ -786,4 +788,129 @@ export class RealBLEManager implements IBLEManager { this.eventListeners = []; } + + /** + * Bulk disconnect multiple devices + * Useful for cleanup or batch operations + */ + async bulkDisconnect(deviceIds: string[]): Promise { + const results: BulkOperationResult[] = []; + + for (const deviceId of deviceIds) { + const device = this.connectedDevices.get(deviceId); + const deviceName = device?.name || deviceId; + + try { + await this.disconnectDevice(deviceId); + results.push({ + deviceId, + deviceName, + success: true, + }); + } catch (error: any) { + results.push({ + deviceId, + deviceName, + success: false, + error: error?.message || 'Disconnect failed', + }); + } + } + + return results; + } + + /** + * Bulk reboot multiple devices + * Useful for applying settings changes to multiple sensors + */ + async bulkReboot(deviceIds: string[]): Promise { + const results: BulkOperationResult[] = []; + + for (const deviceId of deviceIds) { + const device = this.connectedDevices.get(deviceId); + const deviceName = device?.name || deviceId; + + try { + // Connect if not already connected + if (!this.isDeviceConnected(deviceId)) { + const connected = await this.connectDevice(deviceId); + if (!connected) { + throw new Error('Could not connect to device'); + } + } + + // Reboot + await this.rebootDevice(deviceId); + + results.push({ + deviceId, + deviceName, + success: true, + }); + } catch (error: any) { + results.push({ + deviceId, + deviceName, + success: false, + error: error?.message || 'Reboot failed', + }); + } + } + + return results; + } + + /** + * Bulk WiFi configuration for multiple devices + * Configures all devices with the same WiFi credentials sequentially + */ + async bulkSetWiFi( + devices: Array<{ id: string; name: string }>, + ssid: string, + password: string, + onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void + ): Promise { + const results: BulkWiFiResult[] = []; + + for (const device of devices) { + const { id: deviceId, name: deviceName } = device; + + try { + // Step 1: Connect + onProgress?.(deviceId, 'connecting'); + const connected = await this.connectDevice(deviceId); + if (!connected) { + throw new Error('Could not connect to device'); + } + + // Step 2: Set WiFi + onProgress?.(deviceId, 'configuring'); + await this.setWiFi(deviceId, ssid, password); + + // Step 3: Reboot + onProgress?.(deviceId, 'rebooting'); + await this.rebootDevice(deviceId); + + // Success + onProgress?.(deviceId, 'success'); + results.push({ + deviceId, + deviceName, + success: true, + }); + } catch (error: any) { + const errorMessage = error?.message || 'WiFi configuration failed'; + onProgress?.(deviceId, 'error', errorMessage); + results.push({ + deviceId, + deviceName, + success: false, + error: errorMessage, + }); + } + } + + return results; + } } diff --git a/services/ble/MockBLEManager.ts b/services/ble/MockBLEManager.ts index 9722d4b..26645f2 100644 --- a/services/ble/MockBLEManager.ts +++ b/services/ble/MockBLEManager.ts @@ -13,6 +13,8 @@ import { SensorHealthStatus, WiFiSignalQuality, CommunicationHealth, + BulkOperationResult, + BulkWiFiResult, } from './types'; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -300,4 +302,121 @@ export class MockBLEManager implements IBLEManager { this.eventListeners = []; } + + /** + * Bulk disconnect multiple devices (mock) + */ + async bulkDisconnect(deviceIds: string[]): Promise { + const results: BulkOperationResult[] = []; + + for (const deviceId of deviceIds) { + await delay(100); // Simulate disconnect time + + const mockDevice = this.mockDevices.find(d => d.id === deviceId); + const deviceName = mockDevice?.name || deviceId; + + try { + await this.disconnectDevice(deviceId); + results.push({ + deviceId, + deviceName, + success: true, + }); + } catch (error: any) { + results.push({ + deviceId, + deviceName, + success: false, + error: error?.message || 'Disconnect failed', + }); + } + } + + return results; + } + + /** + * Bulk reboot multiple devices (mock) + */ + async bulkReboot(deviceIds: string[]): Promise { + const results: BulkOperationResult[] = []; + + for (const deviceId of deviceIds) { + await delay(500); // Simulate reboot time + + const mockDevice = this.mockDevices.find(d => d.id === deviceId); + const deviceName = mockDevice?.name || deviceId; + + try { + // Mock reboot success + await this.rebootDevice(deviceId); + results.push({ + deviceId, + deviceName, + success: true, + }); + } catch (error: any) { + results.push({ + deviceId, + deviceName, + success: false, + error: error?.message || 'Reboot failed', + }); + } + } + + return results; + } + + /** + * Bulk WiFi configuration for multiple devices (mock) + */ + async bulkSetWiFi( + devices: Array<{ id: string; name: string }>, + ssid: string, + password: string, + onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void + ): Promise { + const results: BulkWiFiResult[] = []; + + for (const device of devices) { + const { id: deviceId, name: deviceName } = device; + + try { + // Step 1: Connect (mock) + onProgress?.(deviceId, 'connecting'); + await delay(800); + await this.connectDevice(deviceId); + + // Step 2: Set WiFi (mock) + onProgress?.(deviceId, 'configuring'); + await delay(1200); + await this.setWiFi(deviceId, ssid, password); + + // Step 3: Reboot (mock) + onProgress?.(deviceId, 'rebooting'); + await delay(600); + await this.rebootDevice(deviceId); + + // Success + onProgress?.(deviceId, 'success'); + results.push({ + deviceId, + deviceName, + success: true, + }); + } catch (error: any) { + const errorMessage = error?.message || 'WiFi configuration failed'; + onProgress?.(deviceId, 'error', errorMessage); + results.push({ + deviceId, + deviceName, + success: false, + error: errorMessage, + }); + } + } + + return results; + } } diff --git a/services/ble/index.ts b/services/ble/index.ts index c41b595..243c4a3 100644 --- a/services/ble/index.ts +++ b/services/ble/index.ts @@ -42,6 +42,11 @@ export const bleManager: IBLEManager = { getCurrentWiFi: (deviceId: string) => getBLEManager().getCurrentWiFi(deviceId), rebootDevice: (deviceId: string) => getBLEManager().rebootDevice(deviceId), cleanup: () => getBLEManager().cleanup(), + getSensorHealth: (wellId: number, mac: string) => getBLEManager().getSensorHealth(wellId, mac), + getAllSensorHealth: () => getBLEManager().getAllSensorHealth(), + bulkDisconnect: (deviceIds: string[]) => getBLEManager().bulkDisconnect(deviceIds), + bulkReboot: (deviceIds: string[]) => getBLEManager().bulkReboot(deviceIds), + bulkSetWiFi: (devices, ssid, password, onProgress) => getBLEManager().bulkSetWiFi(devices, ssid, password, onProgress), }; // Re-export types diff --git a/services/ble/types.ts b/services/ble/types.ts index 9d3f808..be3f5a0 100644 --- a/services/ble/types.ts +++ b/services/ble/types.ts @@ -145,6 +145,20 @@ export interface HealthMonitoringConfig { warningThresholdMinutes: number; // Show warning after N minutes (default: 5) } +// Bulk operation result for a single sensor +export interface BulkOperationResult { + deviceId: string; + deviceName: string; + success: boolean; + error?: string; +} + +// Bulk WiFi configuration result +export interface BulkWiFiResult extends BulkOperationResult { + wellId?: number; + mac?: string; +} + // Interface для BLE Manager (и real и mock) export interface IBLEManager { scanDevices(): Promise; @@ -166,4 +180,14 @@ export interface IBLEManager { // Health monitoring getSensorHealth(wellId: number, mac: string): Promise; getAllSensorHealth(): Map; + + // Bulk operations + bulkDisconnect(deviceIds: string[]): Promise; + bulkReboot(deviceIds: string[]): Promise; + bulkSetWiFi( + devices: Array<{ id: string; name: string }>, + ssid: string, + password: string, + onProgress?: (deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string) => void + ): Promise; }