Skip to content
Merged

Dev #52

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
01da7fb
add status to batch/maintenance , modify asset event that show old/ne…
RockyDuy Apr 10, 2026
a45c240
fix conflict
RockyDuy Apr 10, 2026
6a67950
Merge pull request #39 from Management-System-for-Rental-SEP490/creat…
RockyDuy Apr 10, 2026
fd3beeb
Add power cut scheduling and job handling features
hoangtuzami Apr 10, 2026
9709622
Merge pull request #40 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 10, 2026
d0927cc
add images into dto
RockyDuy Apr 11, 2026
7b62ae9
add images to get latest event
RockyDuy Apr 11, 2026
3965879
add image to assetitem
RockyDuy Apr 11, 2026
4c4ea62
Merge pull request #41 from Management-System-for-Rental-SEP490/creat…
RockyDuy Apr 11, 2026
abb461b
modify method batch and upload assetImage
RockyDuy Apr 11, 2026
4010c22
mofidy controller batch-maintenance
RockyDuy Apr 11, 2026
d1ca093
Merge pull request #42 from Management-System-for-Rental-SEP490/creat…
RockyDuy Apr 11, 2026
06984e4
Introduce Area Power Control feature with IoT integration
hoangtuzami Apr 11, 2026
1e3041a
Merge pull request #43 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 11, 2026
47ff221
Add node capabilities sync and update during provisioning
hoangtuzami Apr 12, 2026
3965653
Merge pull request #44 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 12, 2026
385d5f4
Replace `jakarta.ws.rs.NotFoundException` with custom `NotFoundExcept…
hoangtuzami Apr 12, 2026
a77c913
Merge pull request #45 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 12, 2026
215cc25
Update IoT APIs and security configurations
hoangtuzami Apr 12, 2026
275a131
Merge pull request #46 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 12, 2026
f2a470a
Fix MQTT command constants and IoT device join column mapping
hoangtuzami Apr 12, 2026
8ccc46e
Merge pull request #47 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 12, 2026
1d449fd
add method upload assetEventImage and modify batch
RockyDuy Apr 12, 2026
97cc4a0
Merge pull request #48 from Management-System-for-Rental-SEP490/creat…
RockyDuy Apr 12, 2026
82ad6a7
Add support for fetching previous asset events and mapping old images…
hoangtuzami Apr 12, 2026
bf6ab10
Merge pull request #49 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 12, 2026
4affddb
Add unit tests for service implementations and critical listeners
hoangtuzami Apr 13, 2026
041285f
Merge pull request #50 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 13, 2026
14cbd59
Add logging for IoTDeviceServiceImpl and annotate NotFoundException w…
hoangtuzami Apr 14, 2026
65a9334
Add internationalized message support and refactor hardcoded responses
hoangtuzami Apr 16, 2026
4deea2d
Add internationalization support and IoT forecast trigger feature
hoangtuzami Apr 16, 2026
600db30
Merge pull request #51 from Management-System-for-Rental-SEP490/featu…
hoangtuzami Apr 16, 2026
9bb9337
Add TranslationAutoFillService to handle language translations and in…
hoangtuzami Apr 20, 2026
e8b3145
Introduce utility alerts dashboard: backend logic, IAM, and documenta…
hoangtuzami May 6, 2026
8f6cb90
Add IoT safety config versioning: controller, DTOs, entities, reposit…
hoangtuzami May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(JAVA_HOME=\"/c/Program Files/Eclipse Adoptium/jdk-25.0.1.8-hotspot\" ./gradlew compileJava --no-daemon -q)"
]
}
}
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true

[*.properties]
charset = utf-8
27 changes: 24 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ configurations {
extendsFrom annotationProcessor
}
}
configurations.configureEach {
resolutionStrategy {
force 'com.google.protobuf:protobuf-java:4.34.0'
force 'com.google.protobuf:protobuf-java-util:4.34.0'
eachDependency { details ->
if (details.requested.group == 'com.google.protobuf' && (details.requested.name == 'protobuf-java' || details.requested.name == 'protobuf-java-util')) {
details.useVersion '4.34.0'
details.because 'Keep protobuf runtime aligned with generated proto-common classes'
}
}
}
}

repositories {
mavenCentral()
Expand All @@ -43,6 +55,12 @@ ext {
}

dependencies {
implementation 'net.logstash.logback:logstash-logback-encoder:8.1'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'com.isums:isums-observability-common:1.0-SNAPSHOT'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand All @@ -57,12 +75,13 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation "org.springframework.grpc:spring-grpc-client-spring-boot-starter"
implementation "org.springframework.grpc:spring-grpc-spring-boot-starter"
implementation 'com.google.protobuf:protobuf-java-util:4.33.5'
implementation 'com.google.protobuf:protobuf-java:4.34.0-RC2'
implementation 'com.google.protobuf:protobuf-java-util:4.34.0'
implementation 'com.google.protobuf:protobuf-java:4.34.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'software.amazon.awssdk:dynamodb:2.41.30'
implementation 'software.amazon.awssdk:lambda:2.42.25'
implementation 'software.amazon.awssdk:translate:2.42.29'
implementation "com.isums:proto-common:1.0-SNAPSHOT"
implementation 'io.awspring.cloud:spring-cloud-aws-starter:4.0.0'
implementation 'software.amazon.awssdk:iot:2.42.8'
Expand Down Expand Up @@ -116,4 +135,6 @@ tasks.named('bootBuildImage') {

tasks.named('test') {
useJUnitPlatform()
}
}

tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
69 changes: 69 additions & 0 deletions docs/iam-policy-utility-alerts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# IAM policy — utility-alerts endpoint

The `GET /api/assets/utility-alerts` endpoint reads three DynamoDB
tables owned by the EIF forecast pipeline (`Z:\EIF\h2o_training`).
asset-service already has write access to `esp32_alerts` and `esp32_thresholds`
and `esp32_asset_map`; this feature only adds **read** on two more:

- `esp32_usage_agg` — daily consumption aggregates (future: 30-day chart drawer)
- `esp32_forecast` — monthly Prophet output (`usedSoFar`, `totalEstimate`, `daysLeft`)

## Additive policy statement

Attach the following statement to the asset-service IAM role
(`isums-asset-service` in ECS / EC2 instance profile, or the IAM user
for dev). Do NOT replace existing statements — merge with the current
policy.

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UtilityAlertsReadEifTables",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:BatchGetItem",
"dynamodb:DescribeTable"
],
"Resource": [
"arn:aws:dynamodb:ap-southeast-1:*:table/esp32_usage_agg",
"arn:aws:dynamodb:ap-southeast-1:*:table/esp32_usage_agg/index/*",
"arn:aws:dynamodb:ap-southeast-1:*:table/esp32_forecast",
"arn:aws:dynamodb:ap-southeast-1:*:table/esp32_forecast/index/*"
]
}
]
}
```

## Why no write access?

The EIF pipeline is the sole writer of both tables — asset-service
reading them directly keeps ownership clear and avoids the classic
"who invalidates the cache" ambiguity. If asset-service ever needs to
trigger a forecast refresh, use the existing `LambdaClient` invoke
against `esp32-forecast-dispatcher` (already wired in `IotForecastServiceImpl`).

## Credential source

asset-service resolves credentials via the standard AWS SDK provider
chain (`DefaultCredentialsProvider` in `DynamoConfig.java`), which
means:

- **Prod (ECS)**: task role — no env vars, no rotation needed.
- **Dev (workstation)**: `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`
env vars, or `~/.aws/credentials` default profile.

Region is pinned to `ap-southeast-1` in `DynamoConfig.java` — same
region as the EIF pipeline.

## Verification

After rollout, tail CloudWatch metrics for
`AWS/DynamoDB:ConsumedReadCapacityUnits` on both tables. Expected
profile: small burst on cache-miss (every 5 min per landlord), ~0
otherwise. If reads exceed the table's on-demand baseline, either
(a) bump the cache TTL, or (b) move to a DAX accelerator.
193 changes: 193 additions & 0 deletions docs/utility-alerts-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Utility Alerts — production feature handoff

## What shipped

Replaces the mock `/utilities` dashboard with a real fleet-wide alerts
surface driven by the EIF forecast pipeline + `iot_thresholds`. Landlord
and manager see every house they're responsible for, with the ones
crossing their monthly electricity/water cap surfaced first — and get
an email when a house crosses the threshold.

## End-to-end flow

```
ESP32 → MQTT → AWS IoT → Lambda → esp32_raw (DDB)
EIF Xeon job (daily) → esp32_usage_agg + esp32_forecast
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
READ PATH EMIT PATH ALERT PATH
UtilityAlertsService UtilityThresholdCrossover UtilityAlertEventListener
(on landlord hit) Scheduler (hourly) (notification-service)
│ │ │
▼ ▼ ▼
Redis cache 5min Kafka utility.consumption.alert email vi/en/ja
│ │
▼ ▼
FE Utilities.jsx Redis state store (48h TTL, debounce)
```

## Files added

### asset-service (BE)
| File | Purpose |
|---|---|
| `domains/enums/UtilityMetric.java` | ELECTRICITY / WATER enum + forecast/threshold/unit keys |
| `domains/enums/UtilityStatus.java` | GOOD / WARNING / CRITICAL / NO_DATA |
| `domains/dtos/UtilityAlertItem.java` | One tile |
| `domains/dtos/UtilityAlertSummary.java` | Top-of-dashboard KPIs |
| `domains/dtos/UtilityAlertsResponse.java` | Envelope |
| `domains/events/UtilityThresholdExceededEvent.java` | Kafka payload |
| `infrastructures/abstracts/UtilityAlertsService.java` | Interface |
| `services/UtilityAlertsServiceImpl.java` | Read-side orchestrator + @Cacheable |
| `controllers/UtilityAlertsController.java` | `GET /api/assets/utility-alerts` |
| `schedulers/UtilityThresholdCrossoverScheduler.java` | Hourly cron + Redis debounce + Kafka emit |
| `infrastructures/grpcs/HouseGrpcImpl.java` | **extended** with listHousesByLandlord/Manager |

### asset-service config + docs
| File | Change |
|---|---|
| `src/main/resources/application.properties` | `app.ddb.usageAggTable`, `app.ddb.forecastTable`, `app.cache.utility-alerts.ttl-seconds`, `app.kafka.topic.utility-alert`, `app.utility-alerts.scheduler.enabled` |
| `src/main/resources/messages*.properties` | `utility_alerts.retrieved` key in vi/en/ja |
| `docs/iam-policy-utility-alerts.md` | IAM read-only policy for `esp32_usage_agg` + `esp32_forecast` |

### notification-service (BE)
| File | Purpose |
|---|---|
| `domains/events/UtilityThresholdExceededEvent.java` | Mirror of asset-service event |
| `infrastructures/listeners/UtilityAlertEventListener.java` | Kafka consumer + idempotency + email dispatch |
| `infrastructures/seeders/UtilityAlertTemplateSeeder.java` | Seeds `utility_threshold_exceeded` email templates vi/en/ja |

### web-application (FE)
| File | Purpose |
|---|---|
| `features/utilities/api/utilities.api.js` | REST client (getUtilityAlerts, getHouseAlerts, upsertMonthlyLimit) |
| `features/utilities/hooks/useUtilityAlerts.js` | Module-scope cached hook, 60s TTL + manual refresh |
| `features/utilities/components/HouseAlertDrawer.jsx` | Per-house drawer with alerts + action strip |
| `app/layout/Utilities.jsx` | **rewritten** — real fetch, skeleton/empty/error, tiles, summary KPIs, drawer |
| `lib/api-endpoints.js` | **extended** with `UTILITY_ENDPOINTS` |
| `locales/{vi,en,ja}/common.json` | `utilitiesPage.*` extended — gas removed, drawer/status/states keys added |

### tests
| File | Coverage |
|---|---|
| `services/UtilityAlertsServiceImplTest.java` | Role scoping (LANDLORD / MANAGER / TECH_STAFF), status bands (GOOD / WARNING / CRITICAL), NO_DATA degradation, DDB alert-count happy + failure paths |

## Request / response

```
GET /api/assets/utility-alerts?metric=electricity
Authorization: Bearer <JWT>

200 OK
{
"data": {
"metric": "ELECTRICITY",
"unit": "kWh",
"month": "2026-04",
"items": [
{
"houseId": "…",
"houseName": "Nhà Q1 #12",
"currentUsage": 850.0,
"forecastTotal": 1020.0,
"monthlyLimit": 1000.0,
"usagePercent": 85.0,
"unit": "kWh",
"status": "WARNING",
"activeAlertCount": 2,
"daysLeft": 8,
"lastAlertAt": 1714020930000
}
],
"summary": {
"totalUsage": 3490.0,
"avgPerHouse": 872.5,
"houseCount": 4,
"housesOverThreshold": 2
}
},
"message": "Lấy danh sách cảnh báo tiện ích thành công"
}
```

## Metric names

New rows in `iot_thresholds` table (one per house + utility):
- `metric = 'electricity_monthly_kwh'`, `max_val = <kWh cap>`
- `metric = 'water_monthly_m3'`, `max_val = <m³ cap>`
- `area_id = NULL` (house-level)

Existing per-sensor thresholds (`voltage`, `current`, `power_W`, etc.) are
unaffected — they continue to power the `esp32_alerts` stream which the
drawer renders, but they're not what gates the tile status.

## Kafka event

**Topic**: `utility.consumption.alert`
**Key**: `houseId` (preserves ordering per house)
**Value** (JSON):
```json
{
"eventId": "uuid-v4",
"houseId": "…",
"houseName": "Nhà Q1 #12",
"landlordUserId": "…",
"metric": "ELECTRICITY",
"previousStatus": "GOOD",
"currentStatus": "WARNING",
"currentUsage": 850.0,
"monthlyLimit": 1000.0,
"usagePercent": 85.0,
"unit": "kWh",
"month": "2026-04",
"occurredAt": 1714020930000
}
```

Consumer: notification-service → email to landlord (template
`utility_threshold_exceeded`, default locale vi_VN).

## Rollout checklist

1. Merge branch.
2. **AWS**: attach IAM policy from `docs/iam-policy-utility-alerts.md` to
the asset-service ECS task role (or the dev IAM user).
3. **Kafka**: ensure topic `utility.consumption.alert` exists with RF=1
(dev) or RF=3 (prod). Auto-created on first send if broker has
`auto.create.topics.enable=true`; otherwise create manually via
Kafka UI on `:9091`.
4. Restart asset-service and notification-service (IDE Run Dashboard).
5. Hard-refresh FE browser.
6. Seed a threshold via
`PUT /api/assets/houses/{id}/iot/thresholds/electricity_monthly_kwh`
with body `{"maxVal": 1000, "enabled": true, "severity": "WARNING"}`.
7. Wait ~5 minutes for Redis cache cold path; load `/utilities` in the
landlord session — tile should show real data.
8. For push notification smoke test: set `maxVal` very low (e.g. 10 kWh)
on a house that already has forecast data, wait for the `:07` past
hour cron (or call the scheduler method manually via an admin
endpoint in a future hardening pass), check the landlord's inbox
and Kafka UI for the event.

## Observability

- Micrometer counters: `utility_alerts_requests_total{metric}`,
`utility_alerts_scheduler_sweeps_total`,
`utility_alerts_scheduler_events_total`.
- Micrometer timer: `utility_alerts_build_duration{metric}`.
- Logs: all lines prefixed `[UtilityAlerts]` or `[UtilityAlert]` in
asset-service and notification-service respectively.

## Known deferred items

| Item | Why deferred | Ticket |
|---|---|---|
| `preferredLanguage` on UserResponse (proto) → landlord locale for email | Needs cross-team proto change | next sprint |
| Integration test with Testcontainers + LocalStack DDB | Requires LocalStack image pull + test infra; unit covers the orchestration logic | next sprint |
| Flyway baseline migration for `iot_thresholds` (replace `ddl-auto=update`) | Requires downtime coordination | hardening pass |
| Drawer 30-day chart | Needs a `esp32_usage_agg` → daily-series endpoint | next sprint |
| Region name enrichment (`regionName` on item) | Requires extra house-service gRPC call per row | next sprint |
| Gas metric | Not ingested today | when hardware lands |
Loading
Loading