A senior monitoring system consisting of a .NET 10 + SQL Server backend (Docker) and a React Native mobile application (Android/iOS).
SasiedzkAI is a support system for seniors where a senior shares location and shopping history with an assigned guardian. The backend automatically detects anomalies in the senior's daily activity and sends alerts to the guardian.
Senior (phone) Backend (.NET) Guardian (phone/emulator)
| | |
|-- registration -------------->| |
|<- userId (UUID) --------------| |
| | <-- registration -------|
| | --> userId (UUID) ------>|
| | |
| shares their UUID with guardian (outside the app) |
| | <-- assign-guardian ----|
| | (with senior UUID) |
| | |
|-- GPS every 5 min ----------->| |
|-- save purchases (manual) --->| |
| |-- hourly analysis ----------->|
| | (background service) |-- alerts -->|
| | |
On first launch, the user chooses a role and data-sharing consents:
| Option | Senior | Guardian |
|---|---|---|
isSenior |
true |
false |
sharesLocation |
can be enabled | not used |
sharesPurchases |
can be enabled | not used |
The backend assigns a persistent userId (UUID) based on the phone's deviceId. Reinstalling on the same device returns the same userId.
- Hook runs every 5 minutes in the background (if consent = true)
- Reads GPS (
ACCESS_FINE_LOCATION, high accuracy, 15s timeout) - Sends
POST /location/{userId}with coordinates and optional place category - Backend stores a
LocationEventonly ifSharesLocationData = true
Locations can be tagged with category: cafe β, store π, pharmacy π, park π³, restaurant π½οΈ, hospital π₯
Senior manually adds a product with:
- name and category (Dairy, Bread, Meat, Vegetables, Fruit, Drinks, Other)
- amount (decimal)
- optional estimated number of days until empty -> backend calculates
ExpectedEmptyDate = PurchasedAt + N days
GET /purchases/{userId}/insights returns products with ExpectedEmptyDate within 2 days - displayed with a warning
Screen displays the senior's userId (UUID) to share with guardian - no API call.
- Guardian displays their own
userIdon screen - Enters senior's
userId(UUID) shared by the senior POST /users/{seniorId}/assign-guardian- backend validates:- senior must exist and have
IsSenior = true - guardian must exist and have
IsSenior = false
- senior must exist and have
- Sets
senior.GuardianUserId = guardianId
GET /users/{guardianId}/seniors - returns all seniors assigned to the guardian with sharesLocation and sharesPurchases flags.
GET /location/{userId}/history?lastDays=30 - returns 30 days of senior location history sorted by descending date.
GET /senioralerts/{guardianId} - unacknowledged alerts for this guardian's seniors:
| Alert type | Color | Meaning |
|---|---|---|
NoMovementToday |
warning π | No GPS entries recorded today |
NoOutdoorActivity |
warning π | All GPS events are within a 200 m radius (senior did not leave home) |
RoutineAnomaly |
warning π | Deviation from typical activity |
LongImmobility |
main π | Senior has not moved for a long time |
EmergencyDetected |
dangerous π¨ | Critical situation |
ActivityAlertBackgroundService runs on the backend every hour between 10:00 and 18:00 (local time).
Algorithm for each senior with location sharing enabled:
- Fetch today's GPS events
- If empty ->
NoMovementTodayalert - If all points are within 200 m of the first point (Haversine formula) ->
NoOutdoorActivityalert - Duplicate protection: checks
ExistsForSeniorTodayAsync()- one alert per type per day
HTTP Controller
|
|-- Command (write) --> Repository --> SQL Server
| RegisterUserCommand
| AssignGuardianCommand
| RecordLocationCommand
| RecordPurchaseCommand
|
\-- Query (read) --> Repository --> SQL Server
GetUserProfileQuery
GetGuardianSeniorsQuery
GetLocationHistoryQuery
GetCrowdHeatmapQuery
GetShoppingInsightsQuery
GetPurchaseHistoryQuery
GetSeniorAlertsQuery
Consent validation: RecordLocation and RecordPurchase commands check SharesLocationData/SharesPurchaseData before writing. If consent is missing, request still succeeds (204), but data is not saved.
Users
|-- Id (PK, UUID)
|-- DeviceId (UNIQUE)
|-- IsSenior
|-- GuardianUserId (FK -> Users.Id, nullable)
|-- SharesLocationData
\-- SharesPurchaseData
LocationEvents
|-- Id (PK)
|-- UserId (FK -> Users.Id) <- INDEX (UserId, RecordedAt)
|-- Latitude / Longitude
|-- RecordedAt
\-- PlaceCategory (nullable)
Purchases
|-- Id (PK)
|-- UserId (FK -> Users.Id)
|-- ProductName / Category / Amount
|-- PurchasedAt
|-- EstimatedDaysUntilEmpty (nullable)
\-- ExpectedEmptyDate (computed, nullable)
SeniorAlerts
|-- Id (PK)
|-- SeniorUserId (FK -> Users.Id)
|-- GuardianUserId (FK -> Users.Id, nullable)
|-- Type (enum)
|-- Message
|-- TriggeredAt
\-- IsAcknowledged
| Program | Version | Link |
|---|---|---|
| Docker Desktop | latest | https://www.docker.com/products/docker-desktop |
| Node.js | >= 22.11.0 | https://nodejs.org |
| Android Studio (Android only) | latest | https://developer.android.com/studio |
| Xcode (iOS only, macOS) | latest | App Store |
| JDK 17 (Android only) | 17 | https://adoptium.net |
| .NET SDK 10 (optional - only for backend without Docker) | 10.0 | https://dot.net |
Windows: iOS is not supported - Android only.
# clone repo and enter root folder
cd "Programowanie AI"
# build and run API + SQL Server
docker-compose up --buildThis starts:
- API at
http://localhost:8080 - SQL Server 2022 on port
1433
Database (SasiedzkAI) is created automatically on first start.
GET http://localhost:8080/api/users/{deviceId}
docker-compose down| Parameter | Value |
|---|---|
| Server | localhost,1433 |
| Database | SasiedzkAI |
| Login | sa |
| Password | SasiedzkAI_Pass1! |
cd SasiedzkAIMobile
npm install- Install Android Studio and in SDK Manager install:
- Android SDK Platform 34 (or newer)
- Android SDK Build-Tools
- Android Emulator
- Create a virtual device in AVD Manager (Device Manager -> Create Device)
- Set environment variables (add to
~/.bashrcor Windows system variables):
ANDROID_HOME=C:\Users\<YourUser>\AppData\Local\Android\Sdk
PATH=$PATH:$ANDROID_HOME\emulator
PATH=$PATH:$ANDROID_HOME\platform-toolsStart your selected emulator from Android Studio (AVD Manager) or via terminal:
emulator -avd <emulator_name># in SasiedzkAIMobile folder
npx react-native run-android# install CocoaPods (one-time)
sudo gem install cocoapods
# install iOS dependencies
cd ios && pod install && cd ..
# run app
npx react-native run-iosnpx react-native startFile: SasiedzkAIMobile/src/api/client.ts - line 1
Default URL: http://127.0.0.1:8080/api
| Environment | URL |
|---|---|
| Android Emulator | http://10.0.2.2:8080/api |
| Physical device (USB) | http://<COMPUTER_IP>:8080/api |
| iOS Simulator | http://127.0.0.1:8080/api |
For Android Emulator, change
127.0.0.1to10.0.2.2- this is a special address mapped by emulator to computerlocalhost.
App supports two roles: Senior and Guardian. To test both simultaneously:
| Role | Device | Why |
|---|---|---|
| Senior | Physical Android phone (USB) | Native GPS and reliable background location |
| Guardian | Android Emulator (Android Studio) | No GPS needed, convenient desktop preview |
Senior (physical phone with adb reverse) - after adb reverse, you can use same URL as default:
const BASE_URL = 'http://127.0.0.1:8080/api'; // works thanks to adb reverseAlternative without adb reverse - use your computer LAN IP:
const BASE_URL = 'http://192.168.1.XXX:8080/api'; // check via: ipconfigGuardian (emulator) - set BASE_URL to emulator special address:
const BASE_URL = 'http://10.0.2.2:8080/api';Both devices must target the same backend (Docker). Physical phone connects via Wi-Fi, emulator via Android virtual network bridge.
- Enable Developer options on phone (Settings -> About phone -> tap build number 7 times)
- Enable USB debugging
- Connect phone via USB cable and accept trust prompt
- Verify device detection:
adb devices
# sample output:
# List of devices attached
# R5CT21ABXYZ device <- phone ID
# emulator-5554 device <- emulator (if running)- Redirect port 8080 from phone to computer localhost (
adb reverse):
adb -s <PHONE_ID> reverse tcp:8080 tcp:8080
# e.g. adb -s R5CT21ABXYZ reverse tcp:8080 tcp:8080Why needed? Without
adb reverse, physical phone cannot reach API running on computerlocalhost. This command creates a tunnel: request to127.0.0.1:8080on phone reaches port8080on computer (where Docker runs).After configuring
adb reverse, you can usehttp://127.0.0.1:8080/apifor both devices - no need to type computer IP.
- Run app on phone:
npx react-native run-android1. Start Docker Desktop
2. docker-compose up --build <- backend + database
3. Configure BASE_URL in src/api/client.ts:
- physical phone: http://192.168.1.XXX:8080/api
- emulator: http://10.0.2.2:8080/api
4. Connect phone (Senior) via USB with USB debugging enabled
5. Start Android Studio emulator (Guardian)
6. (new terminal) cd SasiedzkAIMobile && npm install
7. npx react-native run-android <- builds and installs on both devices
Programowanie AI/
|-- src/
| |-- SasiedzkAI.API/ API (controllers, configuration)
| |-- SasiedzkAI.Application/ Business logic (CQRS)
| |-- SasiedzkAI.Domain/ Domain entities
| \-- SasiedzkAI.Infrastructure/ EF Core, repositories, background services
|-- SasiedzkAIMobile/ React Native app
|-- docker-compose.yml
|-- Dockerfile
\-- SasiedzkAI.slnx
| Service | Port |
|---|---|
| API | 8080 |
| SQL Server | 1433 |
| Resource | Path |
|---|---|
| Users | /api/users |
| Location | /api/location |
| Purchases | /api/purchases |
| Senior alerts | /api/senioralerts |