- ADC (17-byte) now emits 'adc' + 'battery' events, not 'status'
- Fixes CRITICAL timer jump: remaining_ms:0 was resetting countdown
- Drop ADC packets with bad XOR checksum
- treating.js + index.js subscribe to 'battery' for live battery updates
- onStatus only updates remaining_ms when > 0
Protocol update adds FFE4 Status format: 7×PD sensors (little-endian)
+ VBAT battery voltage (mV) + XOR checksum = 17 bytes.
- parseVendorStatus: handles 1-byte heartbeat, 17-byte ADC, 33-byte params
- handleValueChange: ADC type extracts PD array + battery % from VBAT
- Battery calculated as linear 3000mV(0%) to 4200mV(100%)
- PD data passed through status event for treatment pages
- Updated protocol doc with full byte table and old/new comparison
- 01-快速开始: local setup for all 3 modules, common issues
- 02-配置说明: all env vars, WeChat/Pay/DB/COS config details
- 03-架构说明: system overview, directory structure, data flows
- 04-部署指南: Tencent Cloud SCF/COS deployment, launch checklist
- 05-API接口文档: all 42 endpoints with params and response format
- README index with audience guide and quick links
- Retry button disconnects then re-scans for device (no page navigation)
- Prevents duplicate scan with checking guard
- Disconnect before scan with 500ms delay for adapter recovery
- Clears reconnect timer on success/error/unload
- Error text updated to guide user to retry button
- vendor_33 mode skips BLE wear detection (protocol doesn't support it)
and passes check immediately if device is connected
- Show error message if device is not connected
- Retry button navigates back if disconnected instead of re-checking
- onUnload disconnects BLE if not in 'done' state (prevents connection leak)
- onUnload clears retry timer to prevent setData on dead page
- Store retry setTimeout ref for proper cleanup
- onRetry clears bind_result listener before disconnect
- Add 1s delay after closeBluetoothAdapter before re-scanning
- Fix binding state text from '点击连接' to '正在配对...'
Heartbeat (1-byte FFE4 notify) now emits 'heartbeat' event only.
Only 33-byte state responses emit 'status' with parsed IO data.
Fixes timer jumping to 0 every second during treatment.
- handleValueChange now calls parseVendorStatus and emits 'status'
with legacy-compatible shape (mode_state, battery, remaining_ms, etc.)
- 1-byte heartbeat mapped to mode_state, 33-byte state parsed for
active IO detection
- Fixes regression where treating, index, wear-check, auto-scan pages
never received status updates in vendor_33 mode
- Also emits 'vendor_raw' with full parsed data for future use
- discoverChars uses properties (write/notify) to distinguish FFE1
command characteristic from FFE1 service UUID
- handleValueChange: vendor_33 mode emits vendor_status (FFE4 heartbeat)
and vendor_data separately, no longer tries parseFrame on raw bytes
- parseVendorStatus handles 1-byte heartbeat and 33-byte state readback
with proper IO1-IO5 field parsing
- RecordView passes user_id as separate filter param, not as keyword
- Admin records endpoint forwards user_id to DAO
- treatmentDao.listAdmin supports exact user_id match filter
- Added visible filter tag with clear button in RecordView
- Always sync treatment end (not just early stop), pass start/end times
- treatment-done uses api.syncTreatment instead of raw http.post
- Move getApp() from module level to onLoad in treatment-done
- Avatar upload only returns URL, no longer updates profile directly
- COS CDN domain configurable via COS_CDN_DOMAIN env var
- Fix operator precedence in request.js token expiry check
- Remove unused result variable from COS putObject
- Treatment start immediately syncs record with full 10min duration
- Early stop re-syncs with actual elapsed time (upsert by session_id)
- treatment-done page uses same session_id from treating page
- Replace chooseAvatar button with chooseMedia (more reliable, works on all versions)
- COS avatar URL uses tx.vsai.net.cn instead of generated bucket domain
- Phone shows '已授权' only (no actual number displayed)
- Keep body as Buffer instead of utf8 string for multipart/form-data
- Set content-length header for multer compatibility
- Phone authorization shows '已授权' instead of actual number
- loadProfile() now returns Promise (was missing return)
- Avatar upload via COS instead of storing WeChat temp path
- Add POST /user/avatar endpoint with multer + COS SDK
- Incomplete registration detection: redirect to register if phone is empty
- New register page with chooseAvatar, nickname input (random default),
and required phone number authorization via getPhoneNumber
- Login simplified: removed deprecated wx.getUserProfile, checks
is_new_user flag to redirect new users to registration
- Server login response now includes is_new_user field
- Add expire_time > NOW() filter to findActive() preventing stale subscriptions
- Add express-rate-limit on login endpoints (user: 10/15min, admin: 5/15min)
- Add production guard for default admin credentials
- Fix BLE bindDevice userId encoding (uint32 instead of hexToBytes on numeric)
- Wrap adminCreate in transaction to prevent race condition
- Add settings cache invalidation after admin saves
- Read trial_days from settings instead of hardcoding 7
- Fix double JSON.stringify in commandDao.finish call
- Cancel stale pending bindings before creating new ones
- Reduce token refresh grace period from 3 days to 1 day
- Fix subscribe-success to fetch expiry from server (correct for renewals)
- Add keep-alive name property to DashboardView and SettingsView
- Fix BLE disconnect() to preserve listener registrations across reconnects
Server:
- Add settings-cache with 60s TTL for feature toggle checks
- Enforce maintenance_mode on login, enable_binding on device bind
Admin console:
- Remove dead "发送通知" button from user detail
- Firmware check calls real API and compares versions
Miniprogram:
- Wear-check: dynamic battery/connected from BLE state
- Login: hide non-functional phone auth button
- Treating: show actual selected regions instead of hardcoded "全脸"
- Index: display subscription status with tap to manage
- Auto-scan: show "待检测" instead of "--" for PD data
- Agreements: tap shows "内容建设中" modal
- Subscription purchase now extends expire_time when user has active
subscription, instead of cancelling and replacing
- Admin subscription creation uses same extend logic
- Subscribe page shows "续费" button and extend message for renewals
- Help and contact pages: add pink header, centered icon + text
POST /api/v1/subscription/purchase only creates an order without
activating. Add POST /api/v1/subscription/mock-purchase that does
purchase + verify in one step (non-production only). Miniprogram
subscribe page now calls mock-purchase so subscriptions take effect.
- Add GET /api/v1/subscription/plans public endpoint for pricing
- Subscription API now returns trial_used field
- Subscribe-plans page shows trial card, greyed out with "已使用" tag
when trial has been used
- Trial activation calls dedicated trial API, not purchase
- Prices fetched from server settings, fallback to defaults
- Add GET /api/v1/subscription/plans public endpoint (no auth needed)
that reads prices from system_settings table
- Subscribe-plans page now loads prices from server on show
- Falls back to hardcoded defaults if API fails
- Add api.getPlans() to miniprogram API module
- Profile: show "未订阅" card when subscription inactive/0 days
- Profile: subscription management navigates to page instead of modal
- Subscribe-plans: data-driven plan cards with mock payment flow
- Subscribe-plans: show current subscription status at top
- Subscribe-prompt: "先使用普通模式" goes to wear-check directly
- Add help and contact placeholder pages
- Fix unbindDevice to work without explicit deviceId
WeChat's require() does not support directory imports — require('./ble')
looks for ble.js, not ble/index.js. Add a proxy file that re-exports
from ble/index.js so all existing imports continue to work.
- utils.js: force UTC+8 output to match MySQL timezone on SCF
- admin subscriptions: return stats (plan counts + revenue) in response
- admin devices: implement keyword search filter
- device-detail: fix binding history field names (bind_time/unbind_time/bind_status)
- user-detail: fix treatment time field (start_time not started_at)
- dashboard: show actual mode per treatment, use sub_stats from dashboard API
Miniprogram:
- Profile "我的设备": show unbind option when device bound, scan when not
- Add device status indicator (已绑定/未绑定) to profile menu
Admin console:
- Login: add @confirm to inputs so Enter key submits the form
- Record detail: wire up 详情 link with modal showing full record info
- Dashboard: add subscription stats row (月卡/年卡/试用/收入)
- Subscription: add 取消 action for active subscriptions
- Format: fix -8h timezone display for UTC ISO date strings
Server:
- POST /api/v1/admin/subscriptions/cancel: cancel active subscriptions
- Dashboard API: include sub_stats (plan counts + monthly revenue)
- IP extraction: add X-Forwarded-For fallback for logging
The bind + confirm two-step flow has timezone issues between Node.js
(UTC on SCF) and MySQL (timezone +08:00) that cause bind_token to
fail validation. Add POST /api/v1/device/mock-bind that does bind +
confirm in one atomic step, skipping the token entirely. Only
available when NODE_ENV != production.
Update ble-connect and index page mock buttons to use mock-bind.
SCF runs in UTC but MySQL connection uses timezone +08:00, causing
bind_expires computed in JS to mismatch NOW() in SQL queries.
Use DATE_ADD(NOW(), INTERVAL ...) directly in SQL to guarantee
consistent timezone for both storage and comparison.
- nav-back: add text-align:left to override page-header's center
- ble-connect mock: call bind API first to get fresh token before
confirm, instead of relying on potentially stale URL params
- utils.js: use local time instead of UTC for toMysqlDate, fixing
timezone mismatch with MySQL NOW() that caused bind tokens to
appear immediately expired
Server:
- POST /api/v1/admin/password: admin password change with bcrypt migration
- POST /api/v1/subscription/trial: user trial activation, one per user
- POST /api/v1/admin/devices/batch: bulk device import (up to 500)
- Add trial plan (7 days, free) to PLANS constant
Admin console:
- Settings page: add password change form with validation
- Device page: add batch import modal with textarea input
Miniprogram:
- Treating page: add back button with stop-treatment confirmation
- Index page: add mock device bind button (dev mode only)
- Login bcrypt migration: write password_salt = '' instead of NULL,
which violated the NOT NULL constraint and caused 500 errors
- Schema: widen password_hash from CHAR(64) to VARCHAR(100) for bcrypt
compatibility, add DEFAULT '' to password_salt
- init-db: add ALTER TABLE migrations for existing databases
- app.js: enrich error logging with method, path, SQL error details
- request.js: also remove admin_token_expiry when clearing auth on
1001/1002 response, preventing stale expiry value in storage
- 01-服务端架构.md: fix table count from 10 to 11
- 03-管理后台架构.md: add /api/v1 prefix to all API endpoint paths