From 6272ece15d6f9f8798392b9da00b84be9dafdfff Mon Sep 17 00:00:00 2001 From: Wowieee4 Date: Thu, 28 May 2026 11:27:06 +0700 Subject: [PATCH] im so tired man --- .gitignore | 3 +- Exam Guide.md | 411 --- README.md | 22 +- ooad-docs/03_Facade_Pattern.puml | 16 +- ooad-docs/04_Repository_Proxy_Pattern.puml | 10 +- ooad-docs/05_Observer_Pattern.puml | 23 +- walkguide-backend/demo/backend-run.err.log | 7 - walkguide-backend/demo/backend-run.log | 2867 ----------------- .../demo/secrets.properties.example | 10 + .../dto/request/LocationUpdateRequest.java | 1 + .../dto/response/DashboardResponse.java | 3 + .../dto/response/LocationResponse.java | 1 + .../com/walkguide/entity/LocationHistory.java | 3 + .../exception/GlobalExceptionHandler.java | 7 +- .../repository/ObstacleLogRepository.java | 4 + .../java/com/walkguide/security/JwtUtil.java | 32 +- .../service/GuardianDashboardService.java | 23 + .../walkguide/service/LocationService.java | 2 + .../demo/src/main/resources/.env.example | 44 +- .../src/main/resources/application-dev.yml | 18 +- .../src/main/resources/application.properties | 12 +- .../V18__add_battery_to_location_history.sql | 2 + .../android/app/src/main/AndroidManifest.xml | 9 +- .../assets/icons/walkguide_launcher.svg | 36 + .../walkguide_app/lib/app/app.dart | 239 +- .../walkguide_app/lib/app/app_cubit.dart | 18 +- .../lib/app/injection_container.dart | 9 - .../lib/core/constants/app_constants.dart | 8 +- .../lib/core/errors/friendly_error.dart | 2 + .../lib/core/i18n/app_strings.dart | 30 +- .../lib/core/network/api_client.dart | 13 +- .../lib/core/services/call_service.dart | 14 + .../lib/core/services/fcm_service.dart | 12 +- .../services/location_reporter_service.dart | 12 + .../core/services/voice_command_handler.dart | 31 + .../activity_log/application/README.md | 3 + .../lib/features/activity_log/data/README.md | 3 + .../features/activity_log/domain/README.md | 3 + .../ai_benchmark/application/README.md | 3 + .../lib/features/ai_benchmark/data/README.md | 3 + .../features/ai_benchmark/domain/README.md | 3 + .../lib/features/auth/application/README.md | 3 + .../lib/features/auth/login_screen.dart | 297 +- .../lib/features/auth/register_screen.dart | 242 +- .../lib/features/call/application/README.md | 3 + .../lib/features/call/call_screen.dart | 126 +- .../lib/features/call/data/README.md | 3 + .../lib/features/call/domain/README.md | 3 + .../guardian_dashboard/application/README.md | 3 + .../guardian_dashboard/data/README.md | 3 + .../guardian_dashboard/domain/README.md | 3 + .../lib/features/home/application/README.md | 3 + .../lib/features/home/data/README.md | 3 + .../lib/features/home/domain/README.md | 3 + .../guardian_dashboard_screen.dart | 18 +- .../presentation/user_dashboard_screen.dart | 39 +- .../lib/features/manual/application/README.md | 3 + .../lib/features/manual/data/README.md | 3 + .../lib/features/manual/domain/README.md | 3 + .../features/manual/presentation/README.md | 3 + .../navigation_mode/application/README.md | 3 + .../features/navigation_mode/data/README.md | 3 + .../features/navigation_mode/domain/README.md | 3 + .../navigation_mode_screen.dart | 208 +- .../features/pairing/application/README.md | 3 + .../lib/features/pairing/data/README.md | 3 + .../lib/features/pairing/domain/README.md | 3 + .../server_connect/application/README.md | 3 + .../features/server_connect/data/README.md | 3 + .../features/server_connect/domain/README.md | 3 + .../server_connect/presentation/README.md | 3 + .../server_connect/server_connect_server.dart | 89 +- .../features/settings/application/README.md | 3 + .../lib/features/settings/data/README.md | 3 + .../lib/features/settings/domain/README.md | 3 + .../settings/user_settings_screen.dart | 1 + .../lib/features/sos/sos_screen.dart | 74 +- .../walk_guide/walk_guide_screen.dart | 24 +- walkguide-mobile/walkguide_app/lib/main.dart | 205 +- .../lib/shared/widgets/app_shells.dart | 274 +- .../lib/shared/widgets/feature_page.dart | 161 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + walkguide-mobile/walkguide_app/pubspec.lock | 29 + walkguide-mobile/walkguide_app/pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 86 files changed, 1759 insertions(+), 4090 deletions(-) delete mode 100644 Exam Guide.md delete mode 100644 walkguide-backend/demo/backend-run.err.log delete mode 100644 walkguide-backend/demo/backend-run.log create mode 100644 walkguide-backend/demo/secrets.properties.example create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V18__add_battery_to_location_history.sql create mode 100644 walkguide-mobile/walkguide_app/assets/icons/walkguide_launcher.svg create mode 100644 walkguide-mobile/walkguide_app/lib/features/activity_log/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/activity_log/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/activity_log/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/ai_benchmark/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/ai_benchmark/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/ai_benchmark/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/auth/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/call/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/call/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/call/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/home/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/home/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/home/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/manual/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/manual/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/manual/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/manual/presentation/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/navigation_mode/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/navigation_mode/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/navigation_mode/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/pairing/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/pairing/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/pairing/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/server_connect/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/server_connect/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/server_connect/domain/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/server_connect/presentation/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/settings/application/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/settings/data/README.md create mode 100644 walkguide-mobile/walkguide_app/lib/features/settings/domain/README.md diff --git a/.gitignore b/.gitignore index ed227fa..7938a44 100644 --- a/.gitignore +++ b/.gitignore @@ -43,8 +43,9 @@ build/ walkguide-backend/demo/secrets.properties walkguide-backend/demo/hs_err_pid*.log +walkguide-backend/demo/backend-run*.log walkguide-backend/demo/src/main/resources/firebase/*.json -walkguide-mobile/walkguide_app/android/app/google-services.json +walkguide-mobile/walkguide_app/android/app/google-services*.json walkguide-mobile/walkguide_app/ios/Runner/GoogleService-Info.plist # Android SDK path (generated by Android Studio) diff --git a/Exam Guide.md b/Exam Guide.md deleted file mode 100644 index 2cb48ca..0000000 --- a/Exam Guide.md +++ /dev/null @@ -1,411 +0,0 @@ -# 📱 Final Exam: Integrated Mobile Application Project -### Flutter × Spring Boot × Object-Oriented Analysis and Design -#### Group Assignment (3 Members) — Industry-Grade Level - ---- - -## Overview - -This final exam requires your group to design, develop, and deliver a **fully integrated mobile application system** consisting of: - -- A **Flutter mobile frontend** that consumes a RESTful API -- A **Spring Boot backend** that exposes the API with proper layering, security, and persistence -- A rigorous **OOAD process** — designed before coding, then verified against the final implementation - -The project is evaluated across three distinct dimensions with different grade weights. This is intentional: **design thinking (OOAD) is the foundation**, engineering quality (Flutter + Spring Boot) is the execution. - ---- - -## Group Formation & Role Distribution - -Each group consists of **exactly 3 members**. Each member owns one primary pillar but all members must understand and contribute to all three. - -| Role | Primary Pillar | Core Responsibilities | -|---|---|---| -| **OOAD Lead** | Object-Oriented Analysis & Design | Leads all design artifacts (use case, class, sequence, state diagrams), enforces design pattern compliance across both codebases, owns the design traceability matrix | -| **Flutter Engineer** | Mobile Frontend | Clean Architecture implementation, state management, UI/UX, performance benchmarking | -| **Backend Engineer** | Spring Boot API | REST API design, service/repository layers, database schema, security (JWT), API documentation, backend testing | - -> **Important:** Role ownership means accountability, not isolation. All members must commit code to both repositories. The OOAD Lead reviews both codebases for pattern compliance — this is a graded responsibility. - ---- - -## Project Topic - -Your group is free to choose any application domain, provided it: - -- Models a real-world problem with identifiable actors, use cases, and entities -- Requires meaningful backend logic (not just CRUD — include at least one business rule or workflow) -- Has a clear primary user and at least one secondary actor (admin, system, or external service) - -**Example domains** *(create your own — do not copy)*: -- Hospital appointment and queue management -- Campus asset borrowing and return tracking -- Community marketplace with seller verification flow -- Event ticketing with seat allocation logic -- Employee attendance with approval workflow - ---- - -## Pillar 1 — Object-Oriented Analysis & Design (OOAD) - -OOAD is evaluated in **two phases**: design artifacts produced before coding, and a traceability audit conducted against the final code. - -### Phase 1A: Pre-Development Design Artifacts - -All diagrams must be produced using **PlantUML, draw.io, or StarUML** and submitted as part of the Week 2–3 checkpoint. Diagrams drawn by hand are not accepted. - -| Artifact | Diagram Type | Requirement | -|---|---|---| -| Requirements model | Use Case Diagram | All actors, use cases, include/extend relationships | -| Structural model | Class Diagram | All domain classes with attributes, methods, visibility, and relationships (association, aggregation, composition, inheritance) | -| Behavioral model | Sequence Diagrams | At least 3 key interactions (e.g., login, create resource, approval flow) showing object collaboration | -| State model | State Machine Diagram | At least 1 entity with meaningful state transitions (e.g., Order: PENDING → CONFIRMED → COMPLETED → CANCELLED) | -| Data model | ERD (Crow's Foot notation) | All entities, PKs/FKs, cardinality — must align with the class diagram | -| Architecture model | Component Diagram | Flutter app, Spring Boot layers, database, and external services as components with interfaces | - -### Phase 1B: Design Pattern Compliance - -Your system must implement **at least 4 GoF design patterns** across the full stack, with at least 1 from each category: - -| Category | Required Count | Examples | -|---|---|---| -| Creational | ≥ 1 | Factory Method, Builder, Singleton | -| Structural | ≥ 1 | Adapter, Facade, Decorator, Proxy | -| Behavioral | ≥ 2 | Strategy, Observer, Command, Template Method, Chain of Responsibility | - -Each pattern must be documented with: -1. Pattern name and category -2. Which class/component implements it (with file path) -3. UML class diagram showing the pattern in context -4. Justification — why this pattern was chosen over alternatives - -### Phase 1C: Design Traceability Audit (Post-Development) - -After development is complete, the OOAD Lead conducts a **traceability audit** comparing the pre-development design to the final code: - -- For each class in the original class diagram: does it exist in code? If not, explain why. -- For each design pattern: show the exact code that implements it (file + line reference). -- For each sequence diagram: trace the method call chain in the actual code. -- Document all **design deviations** — cases where implementation diverged from design — with a written rationale for each deviation. - -> A perfect match between design and code is not required. Thoughtful, documented deviations are acceptable. Undocumented deviations are penalized. - ---- - -## Pillar 2 — Flutter Mobile Frontend - -### Technical Requirements - -| Category | Requirement | -|---|---| -| **Flutter Version** | Flutter 3.x (Stable channel) | -| **Architecture** | Clean Architecture — strict 4-layer separation: `domain / data / application / presentation` | -| **State Management** | BLoC or Riverpod (consistent throughout; mixing is not allowed) | -| **Navigation** | Go Router with at least 6 distinct screens and route guards for authenticated routes | -| **API Communication** | `Dio` with interceptors for JWT token injection, refresh token handling, and error normalization | -| **Local Persistence** | Hive or SQLite for offline caching of at least one core data entity | -| **Authentication** | JWT-based login/register consuming the Spring Boot auth endpoint | -| **UI/UX** | Custom widget library (min. 5 reusable widgets), responsive layout, consistent design system | -| **Error Handling** | Typed failure classes using `Either` (dartz) or equivalent; no raw `try/catch` in UI layer | - -### Folder Structure (Enforced) - -``` -lib/ -├── core/ # shared utilities, theme, constants, error types -├── features/ -│ └── [feature]/ -│ ├── domain/ # entities, repository interfaces, use cases -│ ├── data/ # repository implementations, DTOs, data sources -│ └── presentation/ # BLoC/Cubit, pages, widgets -└── main.dart -``` - -Any structure deviating from feature-first modular layout must be approved in writing before Week 4. - -### Advanced Features (Choose at least 2) - -- Real-time updates via WebSocket or Server-Sent Events from Spring Boot -- Push notifications triggered by backend events (FCM) -- Offline-first with background sync to Spring Boot API -- Animated transitions using custom `PageRouteBuilder` or Lottie -- Internationalization (i18n) with at least 2 languages -- Biometric authentication (fingerprint/face ID) as second factor - -### Flutter Testing & Benchmarking - -#### Functional Testing - -| Type | Tool | Minimum | -|---|---|---| -| Unit Testing | `flutter_test` | All use cases and repository implementations | -| Widget Testing | `flutter_test` | At least 5 core UI components | -| Integration Testing | `integration_test` | At least 3 end-to-end flows against the live Spring Boot API | - -#### Performance Benchmarking - -Run all benchmarks on a **physical Android device in profile mode** (`flutter run --profile`). Emulator results alone are not accepted. - -| Metric | Tool | Pass Threshold | -|---|---|---| -| Memory — baseline | DevTools → Memory tab | Report heap at launch (MB) | -| Memory — leak check | DevTools → Memory tab | No steady growth over 10 repeated navigations | -| Frame rate / jank | DevTools → Performance tab | ≥ 90% frames < 16ms (60fps target) | -| CPU profile | DevTools → CPU Profiler | Flame graph for top 3 CPU-heavy operations | -| API latency (client-side) | Dio interceptor logs | All core endpoints < 1500ms | -| Cold start time | `--trace-startup --profile` | `timeToFirstFrame` < 3000ms | -| APK size | `flutter build apk --analyze-size` | Release APK < 50MB | - -Each benchmark must be reported with: objective, tool, method, results table, threshold comparison, and DevTools screenshot. - -**Regression requirement:** Run benchmarks at Week 5 (mid-sprint) and at Week 7 (final). Submit a delta table comparing both runs. Any metric that degrades > 20% must include a root cause analysis and remediation. - ---- - -## Pillar 3 — Spring Boot Backend - -### Technical Requirements - -| Category | Requirement | -|---|---| -| **Java Version** | Java 17+ | -| **Framework** | Spring Boot 3.x | -| **Architecture** | Layered: `Controller → Service → Repository` (no logic in Controller, no DB calls in Service) | -| **Database** | PostgreSQL or MySQL with JPA/Hibernate; schema migrations via Flyway | -| **Security** | Spring Security with JWT (access token + refresh token); role-based access control (RBAC) | -| **API Design** | RESTful conventions; versioned endpoints (`/api/v1/...`); proper HTTP status codes | -| **Validation** | Bean Validation (`@Valid`) on all request DTOs; global exception handler via `@ControllerAdvice` | -| **Documentation** | Swagger/OpenAPI 3.0 via `springdoc-openapi`; all endpoints documented with schemas | -| **Configuration** | Environment-separated configs (`application-dev.yml`, `application-prod.yml`); no hardcoded secrets | - -### API Contract Requirements - -- Minimum **10 distinct REST endpoints** covering the full application domain -- All endpoints must return a **consistent response envelope**: - -```json -{ - "success": true, - "data": {}, - "message": "Operation successful", - "timestamp": "2025-01-01T00:00:00Z" -} -``` - -- Error responses must include: `success: false`, `errorCode`, `message`, and `timestamp` -- API contract must be defined as an **OpenAPI 3.0 YAML file** committed to the repository before development begins (design-first) - -### Backend Testing & Benchmarking - -#### Functional Testing - -| Type | Tool | Minimum | -|---|---|---| -| Unit Testing | JUnit 5 + Mockito | All service classes; mock repository layer | -| Integration Testing | `@SpringBootTest` + MockMvc | All controller endpoints; test DB via Testcontainers | -| Code Coverage | JaCoCo | ≥ 70% line coverage on `service` and `controller` packages | - -#### Load Benchmarking - -| Metric | Tool | Pass Threshold | -|---|---|---| -| API throughput | Apache JMeter or k6 | ≥ 100 req/s under 50 concurrent users | -| p95 latency | JMeter or k6 | < 500ms under load | -| Error rate under load | JMeter or k6 | < 1% at 50 concurrent users | -| DB query performance | Spring Actuator + slow query log | No query > 200ms for standard operations | -| JVM memory under load | Actuator `/actuator/metrics` | No heap exhaustion during 5-min load test | - -Load test scenario: simulate 50 concurrent users performing a realistic user journey (login → fetch list → create resource → logout) for 5 minutes. Export JMeter `.jtl` report or k6 summary as evidence. - ---- - -## Deliverables - -### 1. 📁 GitHub Repositories (2 repos) - -**Flutter Repository** (`[GroupName]-[AppName]-mobile-final`): -- Feature-first clean architecture folder structure -- GitHub Actions workflow (`.github/workflows/flutter.yml`) — green at submission -- `README.md`: setup instructions, environment variables, APK download link -- All 3 members must have commits; branching strategy enforced - -**Spring Boot Repository** (`[GroupName]-[AppName]-backend`): -- Layered package structure (`controller`, `service`, `repository`, `domain`, `dto`, `config`) -- Flyway migration scripts in `resources/db/migration/` -- OpenAPI YAML committed before Week 4 -- `README.md`: setup instructions, environment variables, how to run locally -- JaCoCo HTML coverage report committed or published via CI - -### 2. 📦 APK File - -- Release build named `[GroupName]_[AppName]_FinalExam.apk` -- Must connect to a **live, publicly deployed** Spring Boot backend (not localhost) -- Acceptable deployment platforms: Railway, Render, Fly.io, or any public URL - -### 3. 📄 Written Report - -Format: PDF, minimum **25 pages** (excluding cover and references). Language: English or Bahasa Indonesia. - -| # | Section | Description | -|---|---|---| -| 1 | Cover Page | System name, group name, member names & student IDs, course name, date | -| 2 | Abstract | 200–250 words covering the system, tech stack, and key findings | -| 3 | Introduction | Problem background, objectives, target users, scope and limitations | -| 4 | OOAD — Pre-Development | All design artifacts (use case, class, sequence, state, ERD, component diagrams) | -| 5 | OOAD — Design Patterns | Documentation of all 4+ patterns with UML and code references | -| 6 | OOAD — Traceability Audit | Design-to-code mapping table; documented deviations with rationale | -| 7 | System Architecture | Flutter Clean Architecture, Spring Boot layers, API communication flow diagram | -| 8 | API Contract | OpenAPI summary, endpoint table, request/response examples | -| 9 | Flutter Implementation | Key features, state management flow, custom widgets, advanced features | -| 10 | Spring Boot Implementation | Service layer logic, security config, DB schema, Flyway migrations | -| 11 | Flutter Testing & Benchmarking | Test results, all 7 benchmark metrics with evidence and delta table | -| 12 | Backend Testing & Benchmarking | JUnit/integration test results, JMeter/k6 load test report | -| 13 | Team Contribution | Per-member task table with percentage, cross-verified with Git commit history | -| 14 | Conclusion | Achievements, design lessons learned, challenges, future improvements | -| 15 | References | IEEE format | - -### 4. Presentation - -- Duration: **15–20 minutes** -- Structure: - - Team introduction + system overview (2 min) - - OOAD design walkthrough — diagrams and pattern explanation (4–5 min) - - Flutter app live demo — all major flows (5–6 min) - - Spring Boot API demo — Swagger UI + one live API call (3 min) - - Benchmark results summary (2 min) -- All 3 members must present a section -- Upload to YouTube (unlisted) or Google Drive - -> **Live Session:** Each member will be questioned individually on the section they presented and on OOAD concepts. Individual scores may differ from the group score. - ---- - -## Timeline - -| Phase | Activity | Deadline | -|---|---|---| -| Week 1 | Group registration + topic proposal + actor/use case list | Day 7 | -| Week 2–3 | All OOAD Phase 1A artifacts submitted + OpenAPI YAML drafted | Day 21 | -| Week 4 | Architecture approved; development sprint begins | Day 28 | -| Week 5 | Mid-sprint benchmark run (Flutter + Backend) submitted | Day 35 | -| Week 6–7 | Feature freeze; Flutter ↔ Spring Boot integration testing | Day 49 | -| Week 7 | Final benchmark run; delta table completed | Day 49 | -| Week 8 | OOAD traceability audit completed; report writing + video | Day 56 | -| **Final** | **All deliverables submitted** | **Day 60, 23:59** | -| Final+1 | Live presentation & individual Q&A | Scheduled by lecturer | - ---- - -## Grading Rubric - -Each pillar is graded **independently out of 100 points**. Students receive three separate scores — one per pillar. There is no combined final grade: each score stands on its own and is recorded separately. - ---- - -### 🥇 Pillar 1 — OOAD Score (/ 100) - -| Component | Points | Criteria | -|---|---|---| -| Pre-development design artifacts | 35 | Completeness of all 6 required diagrams, correctness of notation, diagram tool used (no hand-drawn), submitted before coding begins | -| Design pattern implementation | 25 | Correct application of ≥ 4 GoF patterns (min 1 per category), UML documented per pattern, each traceable to code with file path | -| Traceability audit | 25 | Coverage of class-to-code mapping, quality and honesty of deviation documentation, sequence diagram trace accuracy | -| Cross-pillar design consistency | 15 | Alignment between class diagram, ERD, Flutter domain layer entities, and Spring Boot domain/entity classes | - -**OOAD Penalty:** - -| Violation | Deduction | -|---|---| -| OOAD artifacts submitted after Week 3 (after coding begins) | −20 points | -| Diagram produced with unpermitted tool (e.g., hand-drawn, screenshot of AI output) | −15 points | -| Design pattern claimed but not traceable in code | −8 points per pattern | -| Traceability audit missing for > 30% of class diagram classes | −10 points | - ---- - -### 🥈 Pillar 2 — Flutter Mobile Score (/ 100) - -| Component | Points | Criteria | -|---|---|---| -| Clean Architecture compliance | 25 | Strict 4-layer separation enforced, no cross-layer violations, feature-first folder structure correct, dependency direction correct | -| Features & UX quality | 20 | All required screens functional, JWT auth flow works against live API, custom widget library present, error states handled | -| Testing — unit & widget | 15 | All use cases and repositories covered, at least 5 widget tests, test quality (meaningful assertions, not just coverage padding) | -| Testing — integration | 10 | At least 3 end-to-end flows tested against live Spring Boot API, not mocked | -| Performance benchmarking | 20 | All 7 metrics reported on physical device in profile mode, DevTools screenshots provided, delta table (mid vs final), root cause for any regression > 20% | -| Report clarity | 10 | Report is complete and have clear explanation | - -**Flutter Penalty:** - -| Violation | Deduction | -|---|---| -| Responsive Design fail | −10 points | -| Benchmarks run on emulator only (no physical device) | −10 points | -| Missing delta table (mid-sprint vs final benchmark) | −8 points | -| State management inconsistency (mixing BLoC and Riverpod) | −10 points | -| Raw `try/catch` found in presentation layer | −5 points per occurrence (max −15) | -| APK connects to localhost instead of deployed backend | −15 points | - ---- - -### 🥉 Pillar 3 — Spring Boot Score (/ 100) - -| Component | Points | Criteria | -|---|---|---| -| API design & OpenAPI contract | 25 | ≥ 10 endpoints, consistent response envelope, versioned routes, OpenAPI 3.0 YAML committed before Week 4, Swagger UI accessible | -| Layered architecture & security | 25 | Strict Controller → Service → Repository separation, JWT with access + refresh token, RBAC with at least 2 roles, no hardcoded secrets | -| Testing — unit & integration | 25 | JUnit 5 + Mockito for all service classes, MockMvc + Testcontainers for all controllers, JaCoCo ≥ 70% on `service` and `controller` packages | -| Load benchmarking | 25 | All 5 metrics reported (throughput, p95 latency, error rate, DB query time, JVM heap), JMeter `.jtl` or k6 summary exported, analysis against pass thresholds | - -**Spring Boot Penalty:** - -| Violation | Deduction | -|---|---| -| Hardcoded secrets (API keys, DB passwords) in any file | −15 points | -| JaCoCo coverage below 70% | −10 points | -| No Flyway migrations (schema managed manually) | −8 points | -| OpenAPI YAML committed after Week 4 | −10 points | -| Load test run with < 50 concurrent users | −10 points | -| Business logic found directly in Controller class | −8 points per occurrence (max −16) | - ---- - -### Universal Penalty (Applied to All Three Pillar Scores) - -| Violation | Deduction | -|---|---| -| Late submission (per day, applied to all pillars) | −5 points per pillar | -| Missing deliverable | −15 points from the relevant pillar | -| Plagiarized code (any source) | 0 on all three pillars | -| Member with < 10% commits and no other contribution evidence | That member's individual pillar scores reduced by 20 points each | - ---- - -## Academic Integrity - -- All code must be original. Open-source libraries are permitted with proper attribution in both READMEs and the report. -- Use of AI coding assistants is **permitted but must be disclosed** in a dedicated "AI Tool Usage" section in the report, listing which tools were used, for which tasks, and how outputs were reviewed and understood. -- Design artifacts must be produced by the group. AI-generated diagrams submitted without annotation will be identified during the live Q&A. -- Plagiarism between groups or from public repositories results in **zero marks for all involved groups**. - ---- - -## Submission Checklist - -- [ ] Flutter GitHub repository (Actions pipeline green, branch protection active, 3+ merged PRs) -- [ ] Spring Boot GitHub repository (JaCoCo report committed, OpenAPI YAML present, Flyway migrations included) -- [ ] APK file connecting to live deployed backend (`[GroupName]_FinalExam.apk`) -- [ ] Written report PDF (≥ 25 pages, all 16 sections complete, benchmark delta table included) -- [ ] Demo video link (YouTube unlisted or Google Drive, all 3 members presenting) -- [ ] OOAD traceability matrix (Section 6 of report) -- [ ] Mid-sprint benchmark results (Sections 11 & 12 of report) -- [ ] JMeter/k6 load test export (appendix or Drive link) - ---- - -## Contact & Questions - -All questions must be submitted through the official course channel. Questions submitted at least **48 hours before any deadline** are guaranteed a response. Design artifact reviews (Week 2–3) require a scheduled appointment — contact the lecturer by Week 1 to book a slot. - ---- - -*Build systems you can defend, designs you can explain, and code that reflects your thinking. 🚀* diff --git a/README.md b/README.md index ae89426..7d46583 100644 --- a/README.md +++ b/README.md @@ -138,17 +138,11 @@ The `ooad-docs/` folder contains traceability and diagrams, including: ### Backend -Local/dev config has fallback values so the project can run from an IDE without manually setting every environment variable. +Local/dev config imports an optional gitignored file at `walkguide-backend/demo/secrets.properties`. +Copy `walkguide-backend/demo/secrets.properties.example` to `secrets.properties` and fill it locally. +Tracked config files do not contain DB passwords, JWT secrets, Agora certificates, or Firebase keys. -`application.properties` and `application-dev.yml` currently default to: - -```properties -spring.datasource.url=jdbc:postgresql://202.46.28.160:2002/uas_5803024001 -spring.datasource.username=5803024001 -spring.datasource.password=pw5803024001 -``` - -JWT also has a dev fallback secret. Production config remains strict in `application-prod.yml` and expects environment variables: +Both dev and production expect these values from environment variables or `secrets.properties`: ```text DB_URL @@ -447,12 +441,12 @@ Swagger UI: http://localhost:8080/swagger-ui.html ``` -The local/dev profile has fallback DB and JWT values. If you want to override them: +Local/dev configuration reads database and secret values from environment variables or from the gitignored `secrets.properties` file: ```powershell -$env:DB_URL="jdbc:postgresql://202.46.28.160:2002/uas_5803024001" -$env:DB_USERNAME="5803024001" -$env:DB_PASSWORD="pw5803024001" +$env:DB_URL="jdbc:postgresql://:/" +$env:DB_USERNAME="" +$env:DB_PASSWORD="" $env:JWT_SECRET="your-base64-secret" ``` diff --git a/ooad-docs/03_Facade_Pattern.puml b/ooad-docs/03_Facade_Pattern.puml index 13cc0c7..fbce94e 100644 --- a/ooad-docs/03_Facade_Pattern.puml +++ b/ooad-docs/03_Facade_Pattern.puml @@ -46,16 +46,14 @@ package "③ Facade Pattern [Structural]" #FFF8E1 { class "VoiceCommandHandler\n<>" as VoiceCommandHandler { - _ttsService : TtsService - _sttService : SttService - - _router : GoRouter - - _walkGuideBloc : WalkGuideBloc - - _sosBloc : SosBloc - - _notifBloc : NotificationBloc + - _router : CommandRouter + - _actions : Map + processText(String command) : void - _matchCommand(String) : VoiceCommandKey? - _executeCommand(VoiceCommandKey) : void } - class "WalkGuideBloc\n<>" as WalkGuideBlocFacade { + class "WalkGuideCubit\n<>" as WalkGuideCubitFacade { + onVoiceCommand(String text) } @@ -69,8 +67,8 @@ package "③ Facade Pattern [Structural]" #FFF8E1 { class "SttService " as SttServiceFacade <> class "TtsService " as TtsServiceFacade <> - class "GoRouter\n<>" as GoRouterFacade <> - class "SosBloc " as SosBlocFacade <> + class "CommandRouter\n<>" as GoRouterFacade <> + class "CommandAction\n<>" as CommandActionFacade <> class "LocationService\n<>" as LocationService <> class "ActivityLogService\n<>" as ActivityService <> @@ -82,11 +80,11 @@ package "③ Facade Pattern [Structural]" #FFF8E1 { ' GET /api/v1/guardian/dashboard } - WalkGuideBlocFacade --> VoiceCommandHandler : processText() + WalkGuideCubitFacade --> VoiceCommandHandler : processText() VoiceCommandHandler --> SttServiceFacade : delegates VoiceCommandHandler --> TtsServiceFacade : delegates VoiceCommandHandler --> GoRouterFacade : delegates - VoiceCommandHandler --> SosBlocFacade : delegates + VoiceCommandHandler --> CommandActionFacade : delegates GuardianDashboardController --> GuardianDashboardService : getDashboard() GuardianDashboardService --> LocationService : aggregates diff --git a/ooad-docs/04_Repository_Proxy_Pattern.puml b/ooad-docs/04_Repository_Proxy_Pattern.puml index 8ef1443..8b34198 100644 --- a/ooad-docs/04_Repository_Proxy_Pattern.puml +++ b/ooad-docs/04_Repository_Proxy_Pattern.puml @@ -57,8 +57,8 @@ package "④ Repository Pattern [Structural — Proxy]" #FFF3E0 { } class "WalkGuideRepositoryImpl\n<>" as WalkGuideRepoImpl { - - _remoteDataSource : WalkGuideRemoteDataSource - - _localDataSource : WalkGuideLocalDataSource + - _apiClient : ApiClient + - _offlineQueue : OfflineQueueService - _connectivity : ConnectivityPlus + startSession() : Either + logObstacle(req) : Either @@ -72,16 +72,16 @@ package "④ Repository Pattern [Structural — Proxy]" #FFF3E0 { + syncPending() : Either } - class "WalkGuideRemoteDataSource\n<>" as RemoteDSWalk { + class "ApiClient\n<>" as RemoteDSWalk { + startSession() : void + logObstacle(req) : void ' POST /api/v1/user/obstacle } - class "WalkGuideLocalDataSource\n<>" as LocalDSWalk { + class "OfflineQueueService + LocalDatabase\n<>" as LocalDSWalk { + cacheObstacle(ObstacleLog) : void + getPendingLogs() : List - ' Drift ORM — offline first + ' SQLite-backed offline first } WalkGuideRepo <|.. WalkGuideRepoImpl : implements diff --git a/ooad-docs/05_Observer_Pattern.puml b/ooad-docs/05_Observer_Pattern.puml index f5f2263..9d2d8f6 100644 --- a/ooad-docs/05_Observer_Pattern.puml +++ b/ooad-docs/05_Observer_Pattern.puml @@ -43,30 +43,27 @@ skinparam note { package "⑤ Observer Pattern [Behavioral]" #F3E5F5 { - abstract class "Bloc\n<>" as BlocSubject { + abstract class "Cubit\n<>" as BlocSubject { # stateController : StreamController - + {abstract} on(EventHandler) - + add(Event event) + emit(State state) + stream : Stream } - class "WalkGuideBloc\n<>" as WalkGuideBlocObs { - + on(_onStart) - + on(_onStop) - + on(_onFrame) - + on(_onObstacle) + class "WalkGuideCubit\n<>" as WalkGuideCubitObs { + + start() + + stop() + + logObstacle() - _yoloDetector : YoloDetector - _ttsService : TtsService - _hapticService : HapticService } - class "BlocBuilder\n<>" as BlocBuilderWidget { + class "BlocBuilder\n<>" as BlocBuilderWidget { + builder(ctx, state) : Widget ' Rebuilds UI on every state emission } - class "BlocListener\n<>" as BlocListenerWidget { + class "BlocListener\n<>" as BlocListenerWidget { + listener(ctx, state) : void ' Side effects: TTS, haptic, navigation } @@ -84,9 +81,9 @@ package "⑤ Observer Pattern [Behavioral]" #F3E5F5 { ' Updates flutter_map markers in real-time } - BlocSubject <|-- WalkGuideBlocObs : extends - WalkGuideBlocObs --> BlocBuilderWidget : emits state\n(rebuilds UI) - WalkGuideBlocObs --> BlocListenerWidget : emits state\n(side effects) + BlocSubject <|-- WalkGuideCubitObs : extends + WalkGuideCubitObs --> BlocBuilderWidget : emits state\n(rebuilds UI) + WalkGuideCubitObs --> BlocListenerWidget : emits state\n(side effects) WebSocketObs --> GuardianMapObs : notifies\nlive location } diff --git a/walkguide-backend/demo/backend-run.err.log b/walkguide-backend/demo/backend-run.err.log deleted file mode 100644 index a7fbef3..0000000 --- a/walkguide-backend/demo/backend-run.err.log +++ /dev/null @@ -1,7 +0,0 @@ -Set-Location : A positional parameter cannot be found that accepts argument 'PROGRAM'. -At line:1 char:1 -+ Set-Location C:\COBA PROGRAM SEMESTER 4\UAS FLUTTER FT. SPRINGBOOT da ... -+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + CategoryInfo : InvalidArgument: (:) [Set-Location], ParameterBindingException - + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.SetLocationCommand - diff --git a/walkguide-backend/demo/backend-run.log b/walkguide-backend/demo/backend-run.log deleted file mode 100644 index 4af7315..0000000 --- a/walkguide-backend/demo/backend-run.log +++ /dev/null @@ -1,2867 +0,0 @@ -[INFO] Scanning for projects... -[INFO] -[INFO] -------------------------< com.walkguide:demo >------------------------- -[INFO] Building walkguide 0.0.1-SNAPSHOT -[INFO] from pom.xml -[INFO] --------------------------------[ jar ]--------------------------------- -[INFO] -[INFO] >>> spring-boot:3.2.5:run (default-cli) > test-compile @ demo >>> -[INFO] -[INFO] --- jacoco:0.8.11:prepare-agent (default) @ demo --- -[INFO] argLine set to "-javaagent:C:\\Users\\Robertus\\.m2\\repository\\org\\jacoco\\org.jacoco.agent\\0.8.11\\org.jacoco.agent-0.8.11-runtime.jar=destfile=C:\\COBA PROGRAM SEMESTER 4\\UAS FLUTTER FT. SPRINGBOOT dan OOD\\Final-08-Evan-Jap-Bambang-WalkGuide-AI\\walkguide-backend\\demo\\target\\jacoco.exec" -[INFO] -[INFO] --- resources:3.3.1:resources (default-resources) @ demo --- -[INFO] Copying 3 resources from src\main\resources to target\classes -[INFO] Copying 20 resources from src\main\resources to target\classes -[INFO] -[INFO] --- compiler:3.11.0:compile (default-compile) @ demo --- -[INFO] Nothing to compile - all classes are up to date -[INFO] -[INFO] --- resources:3.3.1:testResources (default-testResources) @ demo --- -[INFO] skip non existing resourceDirectory C:\COBA PROGRAM SEMESTER 4\UAS FLUTTER FT. SPRINGBOOT dan OOD\Final-08-Evan-Jap-Bambang-WalkGuide-AI\walkguide-backend\demo\src\test\resources -[INFO] -[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ demo --- -[INFO] Changes detected - recompiling the module! :source -[INFO] Compiling 28 source files with javac [debug release 21] to target\test-classes -[INFO] -[INFO] <<< spring-boot:3.2.5:run (default-cli) < test-compile @ demo <<< -[INFO] -[INFO] -[INFO] --- spring-boot:3.2.5:run (default-cli) @ demo --- -[INFO] Attaching agents: [] -Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts - - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ -( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: (v3.2.5) - -2026-05-27T19:15:04.478+07:00 INFO 14960 --- [ main] com.walkguide.WalkGuideApplication : Starting WalkGuideApplication using Java 21.0.9 with PID 14960 (C:\COBA PROGRAM SEMESTER 4\UAS FLUTTER FT. SPRINGBOOT dan OOD\Final-08-Evan-Jap-Bambang-WalkGuide-AI\walkguide-backend\demo\target\classes started by Robertus in C:\COBA PROGRAM SEMESTER 4\UAS FLUTTER FT. SPRINGBOOT dan OOD\Final-08-Evan-Jap-Bambang-WalkGuide-AI\walkguide-backend\demo) -2026-05-27T19:15:04.481+07:00 DEBUG 14960 --- [ main] com.walkguide.WalkGuideApplication : Running with Spring Boot v3.2.5, Spring v6.1.6 -2026-05-27T19:15:04.482+07:00 INFO 14960 --- [ main] com.walkguide.WalkGuideApplication : The following 1 profile is active: "dev" -2026-05-27T19:15:05.632+07:00 INFO 14960 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. -2026-05-27T19:15:05.781+07:00 INFO 14960 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 127 ms. Found 13 JPA repository interfaces. -2026-05-27T19:15:06.394+07:00 INFO 14960 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) -2026-05-27T19:15:06.419+07:00 INFO 14960 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] -2026-05-27T19:15:06.419+07:00 INFO 14960 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20] -2026-05-27T19:15:06.507+07:00 INFO 14960 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext -2026-05-27T19:15:06.509+07:00 INFO 14960 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1964 ms -Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts -2026-05-27T19:15:06.564+07:00 DEBUG 14960 --- [ main] com.walkguide.security.JwtAuthFilter : Filter 'jwtAuthFilter' configured for use -2026-05-27T19:15:06.683+07:00 INFO 14960 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] -2026-05-27T19:15:06.741+07:00 INFO 14960 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final -2026-05-27T19:15:06.774+07:00 INFO 14960 --- [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled -2026-05-27T19:15:07.001+07:00 INFO 14960 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer -2026-05-27T19:15:07.029+07:00 INFO 14960 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... -2026-05-27T19:15:09.789+07:00 INFO 14960 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. -2026-05-27T19:15:18.903+07:00 WARN 14960 --- [ main] org.hibernate.orm.deprecation : HHH90000025: PostgreSQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) -2026-05-27T19:15:20.462+07:00 INFO 14960 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) -2026-05-27T19:15:22.089+07:00 INFO 14960 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' -2026-05-27T19:15:22.497+07:00 INFO 14960 --- [ main] com.walkguide.config.FirebaseConfig : [FIREBASE] Firebase Admin initialized from classpath:firebase/google-services-admin.json -2026-05-27T19:15:23.016+07:00 WARN 14960 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning -2026-05-27T19:15:23.047+07:00 WARN 14960 --- [ main] .s.s.UserDetailsServiceAutoConfiguration : - -Using generated security password: 95518d96-3ca0-4c5c-b99a-94ed7eb4c8e2 - -This generated password is for development use only. Your security configuration must be updated before running your application in production. - -2026-05-27T19:15:23.171+07:00 DEBUG 14960 --- [ main] o.s.w.s.s.s.WebSocketHandlerMapping : Patterns [/ws/**] in 'stompWebSocketHandlerMapping' -2026-05-27T19:15:23.336+07:00 INFO 14960 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@63b4a896, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3feeab43, org.springframework.security.web.context.SecurityContextHolderFilter@5f59b6c5, org.springframework.security.web.header.HeaderWriterFilter@566dc0c3, org.springframework.web.filter.CorsFilter@6178ac9d, org.springframework.security.web.authentication.logout.LogoutFilter@5096be74, com.walkguide.security.JwtAuthFilter@113ee1ce, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1872a7fe, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@70720b78, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7413c41, org.springframework.security.web.session.SessionManagementFilter@535b016, org.springframework.security.web.access.ExceptionTranslationFilter@50c462b8, org.springframework.security.web.access.intercept.AuthorizationFilter@6cb59aa] -2026-05-27T19:15:23.407+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - c.w.c.AuthController: - -2026-05-27T19:15:23.408+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - c.w.c.CallController: - -2026-05-27T19:15:23.409+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - c.w.c.GuardianController: - -2026-05-27T19:15:23.410+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - c.w.c.PairingController: - -2026-05-27T19:15:23.410+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - c.w.c.UserController: - -2026-05-27T19:15:23.411+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - o.s.b.a.w.s.e.BasicErrorController: - -2026-05-27T19:15:23.413+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - o.s.w.a.OpenApiWebMvcResource: - -2026-05-27T19:15:23.414+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - o.s.w.u.SwaggerWelcomeWebMvc: - -2026-05-27T19:15:23.415+07:00 DEBUG 14960 --- [ main] .WebSocketAnnotationMethodMessageHandler : - o.s.w.u.SwaggerConfigResource: - -2026-05-27T19:15:23.710+07:00 INFO 14960 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' -2026-05-27T19:15:23.724+07:00 DEBUG 14960 --- [ main] o.s.m.s.ExecutorSubscribableChannel : clientOutboundChannel added SubProtocolWebSocketHandler[StompSubProtocolHandler[v10.stomp, v11.stomp, v12.stomp]] -2026-05-27T19:15:23.725+07:00 DEBUG 14960 --- [ main] o.s.m.s.ExecutorSubscribableChannel : clientInboundChannel added WebSocketAnnotationMethodMessageHandler[prefixes=[/app/]] -2026-05-27T19:15:23.726+07:00 INFO 14960 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Starting... -2026-05-27T19:15:23.726+07:00 DEBUG 14960 --- [ main] o.s.m.s.ExecutorSubscribableChannel : clientInboundChannel added SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@61ee9672] -2026-05-27T19:15:23.726+07:00 DEBUG 14960 --- [ main] o.s.m.s.ExecutorSubscribableChannel : brokerChannel added SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@61ee9672] -2026-05-27T19:15:23.728+07:00 INFO 14960 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@61ee9672]] -2026-05-27T19:15:23.729+07:00 INFO 14960 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Started. -2026-05-27T19:15:23.730+07:00 DEBUG 14960 --- [ main] o.s.m.s.ExecutorSubscribableChannel : clientInboundChannel added UserDestinationMessageHandler[DefaultUserDestinationResolver[prefix=/user/]] -2026-05-27T19:15:23.730+07:00 DEBUG 14960 --- [ main] o.s.m.s.ExecutorSubscribableChannel : brokerChannel added UserDestinationMessageHandler[DefaultUserDestinationResolver[prefix=/user/]] -2026-05-27T19:15:23.737+07:00 INFO 14960 --- [ main] com.walkguide.WalkGuideApplication : Started WalkGuideApplication in 19.764 seconds (process running for 20.253) -Hibernate: - select - count(*) - from - users u1_0 -DataSeeder: Database sudah ada data, skip seeding. -2026-05-27T19:15:38.814+07:00 INFO 14960 --- [0.0-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' -2026-05-27T19:15:38.814+07:00 INFO 14960 --- [0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' -2026-05-27T19:15:38.815+07:00 INFO 14960 --- [0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms -2026-05-27T19:16:23.704+07:00 INFO 14960 --- [MessageBroker-1] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0] -2026-05-27T19:18:42.664+07:00 WARN 14960 --- [0.0-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 08001 -2026-05-27T19:18:42.676+07:00 ERROR 14960 --- [0.0-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper : HikariPool-1 - Connection is not available, request timed out after 10011ms. -2026-05-27T19:18:42.676+07:00 ERROR 14960 --- [0.0-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper : The connection attempt failed. -Hibernate: - select - rt1_0.id, - rt1_0.created_at, - rt1_0.expires_at, - rt1_0.token, - rt1_0.user_id - from - refresh_tokens rt1_0 - where - rt1_0.user_id is null -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.email=? -Hibernate: - select - rt1_0.id, - rt1_0.created_at, - rt1_0.expires_at, - rt1_0.token, - rt1_0.user_id - from - refresh_tokens rt1_0 - where - rt1_0.user_id=? -Hibernate: - insert - into - activity_logs - (created_at, description, log_type, metadata, user_id) - values - (?, ?, ?, ?, ?) -Hibernate: - insert - into - refresh_tokens - (created_at, expires_at, token, user_id) - values - (?, ?, ?, ?) -Hibernate: - delete - from - refresh_tokens - where - id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - update - users - set - display_name=?, - email=?, - fcm_token=?, - pairing_code=?, - pairing_code_expires_at=?, - password=?, - role=?, - unique_user_id=?, - updated_at=? - where - id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - lh1_0.id, - lh1_0.accuracy, - lh1_0.created_at, - lh1_0.heading, - lh1_0.lat, - lh1_0.lng, - lh1_0.speed, - lh1_0.user_id - from - location_history lh1_0 - where - lh1_0.user_id=? - order by - lh1_0.created_at desc - fetch - first ? rows only -2026-05-27T19:28:59.450+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:28:59.456+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(gn1_0.id) - from - guardian_notifications gn1_0 - where - gn1_0.user_id=? - and not(gn1_0.is_read) -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -2026-05-27T19:29:04.606+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:04.607+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:09.779+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:09.786+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:14.951+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:14.951+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.email=? -Hibernate: - select - rt1_0.id, - rt1_0.created_at, - rt1_0.expires_at, - rt1_0.token, - rt1_0.user_id - from - refresh_tokens rt1_0 - where - rt1_0.user_id=? -Hibernate: - insert - into - activity_logs - (created_at, description, log_type, metadata, user_id) - values - (?, ?, ?, ?, ?) -Hibernate: - insert - into - refresh_tokens - (created_at, expires_at, token, user_id) - values - (?, ?, ?, ?) -Hibernate: - delete - from - refresh_tokens - where - id=? -2026-05-27T19:29:20.100+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:20.113+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - vcc1_0.id, - vcc1_0.command_key, - vcc1_0.enabled, - vcc1_0.guardian_id, - vcc1_0.trigger_phrase, - vcc1_0.updated_at, - vcc1_0.user_id - from - voice_command_configs vcc1_0 - where - vcc1_0.user_id=? -Hibernate: - select - hs1_0.id, - hs1_0.button_code, - hs1_0.button_name, - hs1_0.enabled, - hs1_0.guardian_id, - hs1_0.shortcut_key, - hs1_0.updated_at, - hs1_0.user_id - from - hardware_shortcuts hs1_0 - where - hs1_0.user_id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:29:22.109+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:22.110+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:29:27.233+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:27.234+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:32.402+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:32.414+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:37.567+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:37.579+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:42.732+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:42.744+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:47.899+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:47.911+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:29:53.062+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:53.063+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.email=? -Hibernate: - select - rt1_0.id, - rt1_0.created_at, - rt1_0.expires_at, - rt1_0.token, - rt1_0.user_id - from - refresh_tokens rt1_0 - where - rt1_0.user_id=? -Hibernate: - insert - into - activity_logs - (created_at, description, log_type, metadata, user_id) - values - (?, ?, ?, ?, ?) -Hibernate: - insert - into - refresh_tokens - (created_at, expires_at, token, user_id) - values - (?, ?, ?, ?) -Hibernate: - delete - from - refresh_tokens - where - id=? -2026-05-27T19:29:58.228+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:29:58.241+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - update - users - set - display_name=?, - email=?, - fcm_token=?, - pairing_code=?, - pairing_code_expires_at=?, - password=?, - role=?, - unique_user_id=?, - updated_at=? - where - id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - lh1_0.id, - lh1_0.accuracy, - lh1_0.created_at, - lh1_0.heading, - lh1_0.lat, - lh1_0.lng, - lh1_0.speed, - lh1_0.user_id - from - location_history lh1_0 - where - lh1_0.user_id=? - order by - lh1_0.created_at desc - fetch - first ? rows only -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -2026-05-27T19:30:00.506+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:00.507+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(gn1_0.id) - from - guardian_notifications gn1_0 - where - gn1_0.user_id=? - and not(gn1_0.is_read) -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -2026-05-27T19:30:03.397+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:03.408+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -2026-05-27T19:30:05.610+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:05.622+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:07.814+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2068 expires=1779888607 -2026-05-27T19:30:07.817+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.controller.CallController : [CALL] Token generated | caller=2068 receiver=2067 channel=call_2067_2068 -2026-05-27T19:30:08.564+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:08.565+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:10.773+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:10.785+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:13.728+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:13.739+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:15.944+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:15.956+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -2026-05-27T19:30:18.908+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:18.919+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:21.106+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:21.117+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:24.059+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:24.098+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:26.273+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:26.274+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -2026-05-27T19:30:29.237+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:29.238+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - lh1_0.id, - lh1_0.accuracy, - lh1_0.created_at, - lh1_0.heading, - lh1_0.lat, - lh1_0.lng, - lh1_0.speed, - lh1_0.user_id - from - location_history lh1_0 - where - lh1_0.user_id=? - order by - lh1_0.created_at desc - fetch - first ? rows only -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(gn1_0.id) - from - guardian_notifications gn1_0 - where - gn1_0.user_id=? - and not(gn1_0.is_read) -2026-05-27T19:30:31.440+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:31.441+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:34.390+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:34.391+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:36.101+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2067 expires=1779888636 -2026-05-27T19:30:36.112+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.controller.CallController : [CALL] Token generated | caller=2067 receiver=2068 channel=call_2067_2068 -2026-05-27T19:30:36.606+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:36.618+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:39.554+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:39.555+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:41.778+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:41.790+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:44.718+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:44.724+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:46.928+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2068 expires=1779888646 -2026-05-27T19:30:46.939+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.controller.CallController : [CALL] Token generated | caller=2068 receiver=2067 channel=call_2067_2068 -2026-05-27T19:30:46.939+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:46.939+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:48.914+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"receiverId":"2067","channelName":"call_2067_2068","callerId":"2068","type":"IN...(truncated) -2026-05-27T19:30:48.920+07:00 INFO 14960 --- [0.0-8080-exec-1] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=INCOMING_CALL status=RINGING channel=call_2067_2068 -2026-05-27T19:30:49.898+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:49.899+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:52.103+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:52.115+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:53.589+07:00 INFO 14960 --- [0.0-8080-exec-1] com.walkguide.service.FcmService : [FCM] Sent high-priority notification successfully: projects/walkguide-549b3/messages/0:1779885052504510%084e7484084e7484 -2026-05-27T19:30:55.060+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:55.062+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:30:55.909+07:00 WARN 14960 --- [0.0-8080-exec-1] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -2026-05-27T19:30:55.909+07:00 INFO 14960 --- [0.0-8080-exec-1] c.w.service.CallNotificationService : [CALL] Incoming call notification sent | caller=2068 receiver=2067 channel=call_2067_2068 -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:57.011+07:00 INFO 14960 --- [0.0-8080-exec-7] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2067 expires=1779888657 -2026-05-27T19:30:57.023+07:00 INFO 14960 --- [0.0-8080-exec-7] c.walkguide.controller.CallController : [CALL] Token generated | caller=2067 receiver=2068 channel=call_2067_2068 -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:57.281+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:30:57.282+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:30:58.242+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"receiverId":"2067","receiverName":"suami kobo","channelName":"call_2067_2068",...(truncated) -2026-05-27T19:30:58.253+07:00 INFO 14960 --- [0.0-8080-exec-8] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ACCEPTED status=ACCEPTED channel=call_2067_2068 -2026-05-27T19:30:58.254+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"receiverId":"2067","receiverName":"suami kobo","channelName":"call_2067_2068",...(truncated) -2026-05-27T19:30:58.254+07:00 INFO 14960 --- [0.0-8080-exec-8] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ACCEPTED status=ACCEPTED channel=call_2067_2068 -2026-05-27T19:30:58.254+07:00 INFO 14960 --- [0.0-8080-exec-8] c.w.service.CallNotificationService : [CALL] Call accepted | caller=2068 receiver=2067 channel=call_2067_2068 -2026-05-27T19:31:00.222+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:00.224+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:02.443+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:02.453+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:05.389+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:05.401+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:07.637+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:07.648+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:10.554+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:10.566+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:12.771+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:12.782+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:15.726+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:15.737+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:17.935+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:17.947+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:20.889+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:20.901+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:23.105+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:23.112+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:26.058+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:26.070+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:28.271+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:28.283+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:31.219+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:31.231+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:33.436+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:33.437+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:36.382+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:36.393+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:38.600+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:38.601+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:41.551+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:41.564+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:43.769+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:43.781+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:46.718+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:46.719+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:48.933+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:48.935+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:51.884+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:51.885+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:52.614+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"otherId":"2068","receiverName":"suami kobo","endedBy":"2067","type":"CALL_ENDE...(truncated) -2026-05-27T19:31:52.626+07:00 INFO 14960 --- [0.0-8080-exec-4] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:31:52.627+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"otherId":"2068","receiverName":"suami kobo","endedBy":"2067","type":"CALL_ENDE...(truncated) -2026-05-27T19:31:52.628+07:00 INFO 14960 --- [0.0-8080-exec-4] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:31:54.122+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:54.135+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:31:56.658+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"otherId":"2068","receiverName":"suami kobo","endedBy":"2067","type":"CALL_ENDE...(truncated) -2026-05-27T19:31:56.670+07:00 INFO 14960 --- [0.0-8080-exec-1] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:31:56.671+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"otherId":"2068","receiverName":"suami kobo","endedBy":"2067","type":"CALL_ENDE...(truncated) -2026-05-27T19:31:56.671+07:00 INFO 14960 --- [0.0-8080-exec-1] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:31:57.050+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:57.052+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:59.268+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:31:59.280+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:31:59.991+07:00 INFO 14960 --- [0.0-8080-exec-4] com.walkguide.service.FcmService : [FCM] Sent normal notification successfully: projects/walkguide-549b3/messages/0:1779885120460350%084e7484084e7484 -2026-05-27T19:32:00.045+07:00 WARN 14960 --- [0.0-8080-exec-4] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:00.576+07:00 INFO 14960 --- [0.0-8080-exec-1] com.walkguide.service.FcmService : [FCM] Sent normal notification successfully: projects/walkguide-549b3/messages/0:1779885121120572%084e7484084e7484 -2026-05-27T19:32:00.777+07:00 WARN 14960 --- [0.0-8080-exec-1] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -Hibernate: - select - vcc1_0.id, - vcc1_0.command_key, - vcc1_0.enabled, - vcc1_0.guardian_id, - vcc1_0.trigger_phrase, - vcc1_0.updated_at, - vcc1_0.user_id - from - voice_command_configs vcc1_0 - where - vcc1_0.user_id=? -Hibernate: - select - hs1_0.id, - hs1_0.button_code, - hs1_0.button_name, - hs1_0.enabled, - hs1_0.guardian_id, - hs1_0.shortcut_key, - hs1_0.updated_at, - hs1_0.user_id - from - hardware_shortcuts hs1_0 - where - hs1_0.user_id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:02.214+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:02.226+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:04.463+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:04.465+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:07.397+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:07.399+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:09.446+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"otherId":"2067","receiverName":"suami kobo","endedBy":"2068","type":"CALL_ENDE...(truncated) -2026-05-27T19:32:09.447+07:00 INFO 14960 --- [0.0-8080-exec-4] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:32:09.447+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"otherId":"2067","receiverName":"suami kobo","endedBy":"2068","type":"CALL_ENDE...(truncated) -2026-05-27T19:32:09.447+07:00 INFO 14960 --- [0.0-8080-exec-4] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:32:09.599+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:09.600+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:11.061+07:00 INFO 14960 --- [0.0-8080-exec-4] com.walkguide.service.FcmService : [FCM] Sent normal notification successfully: projects/walkguide-549b3/messages/0:1779885131535053%084e7484084e7484 -2026-05-27T19:32:11.118+07:00 WARN 14960 --- [0.0-8080-exec-4] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -2026-05-27T19:32:12.550+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:12.551+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:14.805+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:14.817+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - lh1_0.id, - lh1_0.accuracy, - lh1_0.created_at, - lh1_0.heading, - lh1_0.lat, - lh1_0.lng, - lh1_0.speed, - lh1_0.user_id - from - location_history lh1_0 - where - lh1_0.user_id=? - order by - lh1_0.created_at desc - fetch - first ? rows only -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(gn1_0.id) - from - guardian_notifications gn1_0 - where - gn1_0.user_id=? - and not(gn1_0.is_read) -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:17.715+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:17.728+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -2026-05-27T19:32:19.928+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:19.929+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:22.887+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:22.926+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:25.333+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:25.345+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:27.296+07:00 INFO 14960 --- [0.0-8080-exec-2] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2068 expires=1779888747 -2026-05-27T19:32:27.297+07:00 INFO 14960 --- [0.0-8080-exec-2] c.walkguide.controller.CallController : [CALL] Token generated | caller=2068 receiver=2067 channel=call_2067_2068 -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:28.047+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:28.048+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:28.525+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"receiverId":"2067","channelName":"call_2067_2068","callerId":"2068","type":"IN...(truncated) -2026-05-27T19:32:28.526+07:00 INFO 14960 --- [0.0-8080-exec-7] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=INCOMING_CALL status=RINGING channel=call_2067_2068 -2026-05-27T19:32:28.775+07:00 INFO 14960 --- [0.0-8080-exec-7] com.walkguide.service.FcmService : [FCM] Sent high-priority notification successfully: projects/walkguide-549b3/messages/0:1779885149223672%084e7484084e7484 -2026-05-27T19:32:28.822+07:00 WARN 14960 --- [0.0-8080-exec-7] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -2026-05-27T19:32:28.823+07:00 INFO 14960 --- [0.0-8080-exec-7] c.w.service.CallNotificationService : [CALL] Incoming call notification sent | caller=2068 receiver=2067 channel=call_2067_2068 -2026-05-27T19:32:30.510+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:30.511+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:33.210+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:33.211+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:35.696+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:35.697+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:37.382+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2067 expires=1779888757 -2026-05-27T19:32:37.394+07:00 INFO 14960 --- [0.0-8080-exec-4] c.walkguide.controller.CallController : [CALL] Token generated | caller=2067 receiver=2068 channel=call_2067_2068 -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:32:38.370+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:38.379+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:38.861+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"receiverId":"2067","receiverName":"suami kobo","channelName":"call_2067_2068",...(truncated) -2026-05-27T19:32:38.861+07:00 INFO 14960 --- [0.0-8080-exec-5] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ACCEPTED status=ACCEPTED channel=call_2067_2068 -2026-05-27T19:32:38.862+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"receiverId":"2067","receiverName":"suami kobo","channelName":"call_2067_2068",...(truncated) -2026-05-27T19:32:38.862+07:00 INFO 14960 --- [0.0-8080-exec-5] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ACCEPTED status=ACCEPTED channel=call_2067_2068 -2026-05-27T19:32:38.862+07:00 INFO 14960 --- [0.0-8080-exec-5] c.w.service.CallNotificationService : [CALL] Call accepted | caller=2068 receiver=2067 channel=call_2067_2068 -2026-05-27T19:32:40.845+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:40.857+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:43.541+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:43.554+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:46.009+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:46.021+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:48.719+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:48.721+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:51.268+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:51.270+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:53.878+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:53.879+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:56.431+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:56.433+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:32:59.043+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:32:59.044+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:01.751+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:01.752+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:04.236+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:04.238+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:06.926+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:06.928+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:09.387+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:09.389+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:10.594+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"otherId":"2067","receiverName":"suami kobo","endedBy":"2068","type":"CALL_ENDE...(truncated) -2026-05-27T19:33:10.595+07:00 INFO 14960 --- [0.0-8080-exec-7] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:33:10.596+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"otherId":"2067","receiverName":"suami kobo","endedBy":"2068","type":"CALL_ENDE...(truncated) -2026-05-27T19:33:10.596+07:00 INFO 14960 --- [0.0-8080-exec-7] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ENDED status=ENDED channel=call_2067_2068 -2026-05-27T19:33:12.096+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:12.097+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - vcc1_0.id, - vcc1_0.command_key, - vcc1_0.enabled, - vcc1_0.guardian_id, - vcc1_0.trigger_phrase, - vcc1_0.updated_at, - vcc1_0.user_id - from - voice_command_configs vcc1_0 - where - vcc1_0.user_id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - hs1_0.id, - hs1_0.button_code, - hs1_0.button_name, - hs1_0.enabled, - hs1_0.guardian_id, - hs1_0.shortcut_key, - hs1_0.updated_at, - hs1_0.user_id - from - hardware_shortcuts hs1_0 - where - hs1_0.user_id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -2026-05-27T19:33:13.056+07:00 INFO 14960 --- [0.0-8080-exec-7] com.walkguide.service.FcmService : [FCM] Sent normal notification successfully: projects/walkguide-549b3/messages/0:1779885193529669%084e7484084e7484 -2026-05-27T19:33:13.110+07:00 WARN 14960 --- [0.0-8080-exec-7] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:33:14.632+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:14.637+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users g1_0 - on g1_0.id=pr1_0.guardian_id - where - g1_0.id=? - and pr1_0.status=? -Hibernate: - select - lh1_0.id, - lh1_0.accuracy, - lh1_0.created_at, - lh1_0.heading, - lh1_0.lat, - lh1_0.lng, - lh1_0.speed, - lh1_0.user_id - from - location_history lh1_0 - where - lh1_0.user_id=? - order by - lh1_0.created_at desc - fetch - first ? rows only -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(gn1_0.id) - from - guardian_notifications gn1_0 - where - gn1_0.user_id=? - and not(gn1_0.is_read) -2026-05-27T19:33:17.259+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:17.260+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - al1_0.id, - al1_0.created_at, - al1_0.description, - al1_0.log_type, - al1_0.metadata, - al1_0.user_id - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? - order by - al1_0.created_at desc - offset - ? rows - fetch - first ? rows only -Hibernate: - select - count(al1_0.id) - from - activity_logs al1_0 - left join - users u1_0 - on u1_0.id=al1_0.user_id - where - u1_0.id=? -Hibernate: - select - se1_0.id, - se1_0.acknowledged_at, - se1_0.created_at, - se1_0.lat, - se1_0.lng, - se1_0.status, - se1_0.trigger_type, - se1_0.user_id - from - sos_events se1_0 - where - se1_0.user_id=? - order by - se1_0.created_at desc - offset - ? rows - fetch - first ? rows only -2026-05-27T19:33:19.819+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:19.822+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:22.428+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:22.431+07:00 DEBUG 14960 --- [0.0-8080-exec-7] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:24.965+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:24.966+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:27.589+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:27.594+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:30.077+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:30.081+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - pr1_0.id, - pr1_0.guardian_id, - pr1_0.invited_at, - pr1_0.responded_at, - pr1_0.status, - pr1_0.user_id - from - pairing_relations pr1_0 - left join - users u1_0 - on u1_0.id=pr1_0.user_id - where - u1_0.id=? -2026-05-27T19:33:32.755+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:32.756+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:33:35.214+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:35.217+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:35.438+07:00 INFO 14960 --- [0.0-8080-exec-3] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2067 expires=1779888815 -2026-05-27T19:33:35.441+07:00 INFO 14960 --- [0.0-8080-exec-3] c.walkguide.controller.CallController : [CALL] Token generated | caller=2067 receiver=2068 channel=call_2067_2068 -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:33:36.919+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"receiverId":"2068","channelName":"call_2067_2068","callerId":"2067","type":"IN...(truncated) -2026-05-27T19:33:36.920+07:00 INFO 14960 --- [0.0-8080-exec-5] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=INCOMING_CALL status=RINGING channel=call_2067_2068 -2026-05-27T19:33:37.163+07:00 INFO 14960 --- [0.0-8080-exec-5] com.walkguide.service.FcmService : [FCM] Sent high-priority notification successfully: projects/walkguide-549b3/messages/0:1779885217624637%084e7484084e7484 -2026-05-27T19:33:37.220+07:00 WARN 14960 --- [0.0-8080-exec-5] com.walkguide.service.FcmService : [FIRESTORE] Notification audit skipped: com.google.api.gax.rpc.PermissionDeniedException: io.grpc.StatusRuntimeException: PERMISSION_DENIED: Cloud Firestore API has not been used in project walkguide-549b3 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firestore.googleapis.com/overview?project=walkguide-549b3 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. -2026-05-27T19:33:37.221+07:00 INFO 14960 --- [0.0-8080-exec-5] c.w.service.CallNotificationService : [CALL] Incoming call notification sent | caller=2067 receiver=2068 channel=call_2067_2068 -2026-05-27T19:33:37.919+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:37.920+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:40.376+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:40.379+07:00 DEBUG 14960 --- [0.0-8080-exec-2] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:33:43.099+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:43.100+07:00 DEBUG 14960 --- [0.0-8080-exec-3] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:43.804+07:00 INFO 14960 --- [.0-8080-exec-10] c.walkguide.service.AgoraTokenService : [AGORA] Token generated untuk channel=call_2067_2068 uid=2068 expires=1779888823 -2026-05-27T19:33:43.804+07:00 INFO 14960 --- [.0-8080-exec-10] c.walkguide.controller.CallController : [CALL] Token generated | caller=2068 receiver=2067 channel=call_2067_2068 -2026-05-27T19:33:45.613+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:45.615+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:48.293+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:48.295+07:00 DEBUG 14960 --- [0.0-8080-exec-8] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:49.690+07:00 WARN 14960 --- [0.0-8080-exec-9] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@2f6912a6 (This connection has been closed.). Possibly consider using a shorter maxLifetime value. -2026-05-27T19:33:50.729+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:50.731+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -Hibernate: - select - u1_0.id, - u1_0.created_at, - u1_0.display_name, - u1_0.email, - u1_0.fcm_token, - u1_0.pairing_code, - u1_0.pairing_code_expires_at, - u1_0.password, - u1_0.role, - u1_0.unique_user_id, - u1_0.updated_at - from - users u1_0 - where - u1_0.id=? -2026-05-27T19:33:53.427+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:53.429+07:00 DEBUG 14960 --- [0.0-8080-exec-6] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:53.890+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2067 session=null payload={"receiverId":"2068","receiverName":"GuardianRobert","channelName":"call_2067_20...(truncated) -2026-05-27T19:33:53.890+07:00 INFO 14960 --- [0.0-8080-exec-9] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2067 | type=CALL_ACCEPTED status=ACCEPTED channel=call_2067_2068 -2026-05-27T19:33:53.891+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/call/2068 session=null payload={"receiverId":"2068","receiverName":"GuardianRobert","channelName":"call_2067_20...(truncated) -2026-05-27T19:33:53.892+07:00 INFO 14960 --- [0.0-8080-exec-9] c.w.websocket.LocationBroadcaster : [WS] Call broadcast -> /queue/call/2068 | type=CALL_ACCEPTED status=ACCEPTED channel=call_2067_2068 -2026-05-27T19:33:53.893+07:00 INFO 14960 --- [0.0-8080-exec-9] c.w.service.CallNotificationService : [CALL] Call accepted | caller=2067 receiver=2068 channel=call_2067_2068 -2026-05-27T19:33:55.880+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:55.880+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:33:58.584+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:33:58.585+07:00 DEBUG 14960 --- [.0-8080-exec-10] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:01.039+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:01.042+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:03.749+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:03.751+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:06.209+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:06.210+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:08.918+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:08.919+07:00 DEBUG 14960 --- [0.0-8080-exec-9] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:11.369+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:11.371+07:00 DEBUG 14960 --- [0.0-8080-exec-1] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:14.082+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:14.083+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:16.535+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:16.535+07:00 DEBUG 14960 --- [0.0-8080-exec-4] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws -2026-05-27T19:34:19.253+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@268eefe7 -2026-05-27T19:34:19.258+07:00 DEBUG 14960 --- [0.0-8080-exec-5] o.s.w.s.s.t.h.DefaultSockJsService : Processing transport request: GET http://192.168.1.68:8080/ws diff --git a/walkguide-backend/demo/secrets.properties.example b/walkguide-backend/demo/secrets.properties.example new file mode 100644 index 0000000..210cca7 --- /dev/null +++ b/walkguide-backend/demo/secrets.properties.example @@ -0,0 +1,10 @@ +# Copy this file to walkguide-backend/demo/secrets.properties. +# secrets.properties is gitignored and is imported by application.properties. + +DB_URL=jdbc:postgresql://:/ +DB_USERNAME= +DB_PASSWORD= +JWT_SECRET= +AGORA_APP_ID= +AGORA_APP_CERTIFICATE= +FIREBASE_CREDENTIALS_PATH=classpath:firebase/google-services-admin.json diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java index db61c41..d59dbd8 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java @@ -8,4 +8,5 @@ public class LocationUpdateRequest { private Double accuracy; private Double speed; private Double heading; + private Integer batteryLevel; } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java index 2e40183..66d3e8b 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java @@ -4,6 +4,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import java.util.List; +import java.util.Map; @Data @NoArgsConstructor @@ -22,6 +23,8 @@ public class DashboardResponse { // Status private long unreadSosCount; private long unreadNotifCount; + private long obstaclesToday; + private Map userStatus; // Recent activity (5 terbaru) private List recentActivity; diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java index 8c5144e..cea1826 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java @@ -16,5 +16,6 @@ public class LocationResponse { private Double accuracy; private Double speed; private Double heading; + private Integer batteryLevel; private LocalDateTime createdAt; } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java index 79d971d..15afa0d 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java @@ -29,6 +29,9 @@ public class LocationHistory { private Double speed; // m/s private Double heading; // derajat 0-360 + @Column(name = "battery_level") + private Integer batteryLevel; + @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java b/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java index 11740ba..0bf54bc 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java @@ -39,8 +39,13 @@ public class GlobalExceptionHandler { } @ExceptionHandler(RuntimeException.class) public ResponseEntity> handleRuntime(RuntimeException ex) { + String message = ex.getMessage(); + if ("Email tidak terdaftar".equals(message) || "Password salah".equals(message)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ApiResponse.error("AUTH_INVALID", message)); + } return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("INTERNAL_ERROR", ex.getMessage())); + .body(ApiResponse.error("INTERNAL_ERROR", message)); } @ExceptionHandler(Exception.class) diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java index c8c9b74..2c9d076 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java @@ -4,7 +4,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + @Repository public interface ObstacleLogRepository extends JpaRepository { Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); + long countByUserIdAndCreatedAtAfter(Long userId, LocalDateTime createdAt); } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java b/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java index 031b6ea..49cc6d3 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java @@ -8,7 +8,10 @@ import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -82,7 +85,34 @@ public class JwtUtil { } private Key getSignInKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); + byte[] keyBytes = decodeSecret(secretKey); return Keys.hmacShaKeyFor(keyBytes); } + + private byte[] decodeSecret(String configuredSecret) { + String trimmed = configuredSecret == null ? "" : configuredSecret.trim(); + if (trimmed.isEmpty()) { + throw new IllegalStateException("JWT secret must not be empty"); + } + + byte[] keyBytes; + try { + keyBytes = Decoders.BASE64.decode(trimmed); + } catch (RuntimeException base64Error) { + try { + keyBytes = Decoders.BASE64URL.decode(trimmed); + } catch (RuntimeException base64UrlError) { + keyBytes = trimmed.getBytes(StandardCharsets.UTF_8); + } + } + + if (keyBytes.length >= 32) { + return keyBytes; + } + try { + return MessageDigest.getInstance("SHA-256").digest(keyBytes); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not available", e); + } + } } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java index 4344538..4aded85 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java @@ -8,6 +8,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; + @Service @RequiredArgsConstructor public class GuardianDashboardService { @@ -17,6 +22,7 @@ public class GuardianDashboardService { private final ActivityLogService activityLogService; private final SosEventRepository sosEventRepository; private final GuardianNotificationRepository notifRepository; + private final ObstacleLogRepository obstacleLogRepository; public DashboardResponse getDashboard(Long guardianId) { var pairing = pairingRelationRepository @@ -40,6 +46,21 @@ public class GuardianDashboardService { long unreadSos = sosEventRepository.findByUserIdOrderByCreatedAtDesc(userId, PageRequest.of(0, 100)) .stream().filter(s -> s.getStatus().name().equals("TRIGGERED")).count(); long unreadNotif = notifRepository.countByUserIdAndIsReadFalse(userId); + long obstaclesToday = obstacleLogRepository.countByUserIdAndCreatedAtAfter( + userId, + LocalDateTime.of(LocalDateTime.now().toLocalDate(), LocalTime.MIN) + ); + + Map userStatus = new HashMap<>(); + userStatus.put("displayName", user.getDisplayName()); + userStatus.put("email", user.getEmail()); + userStatus.put("online", lastLocation != null + && lastLocation.getCreatedAt() != null + && lastLocation.getCreatedAt().isAfter(LocalDateTime.now().minusMinutes(2))); + userStatus.put("lastSeenAt", lastLocation != null ? lastLocation.getCreatedAt() : null); + userStatus.put("battery", lastLocation != null ? lastLocation.getBatteryLevel() : null); + userStatus.put("lastSpeed", lastLocation != null ? lastLocation.getSpeed() : null); + userStatus.put("obstaclesToday", obstaclesToday); return DashboardResponse.builder() .pairedUserId(userId) @@ -49,6 +70,8 @@ public class GuardianDashboardService { .lastLocation(lastLocation) .unreadSosCount(unreadSos) .unreadNotifCount(unreadNotif) + .obstaclesToday(obstaclesToday) + .userStatus(userStatus) .recentActivity(recentActivity) .build(); } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java index 6db75fd..71624ad 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java @@ -42,6 +42,7 @@ public class LocationService { .accuracy(req.getAccuracy()) .speed(req.getSpeed()) .heading(req.getHeading()) + .batteryLevel(req.getBatteryLevel()) .build(); loc = locationHistoryRepository.save(loc); @@ -136,6 +137,7 @@ public class LocationService { return LocationResponse.builder() .id(l.getId()).lat(l.getLat()).lng(l.getLng()) .accuracy(l.getAccuracy()).speed(l.getSpeed()).heading(l.getHeading()) + .batteryLevel(l.getBatteryLevel()) .createdAt(l.getCreatedAt()).build(); } } diff --git a/walkguide-backend/demo/src/main/resources/.env.example b/walkguide-backend/demo/src/main/resources/.env.example index 11acc7a..a11d62e 100644 --- a/walkguide-backend/demo/src/main/resources/.env.example +++ b/walkguide-backend/demo/src/main/resources/.env.example @@ -1,35 +1,9 @@ -# =================================================== -# Profile: prod (production) -# Aktifkan dengan: --spring.profiles.active=prod -# Semua nilai WAJIB diisi via environment variable -# Tidak ada default value — akan gagal start jika kosong -# =================================================== - -spring: - datasource: - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - - jpa: - show-sql: false - properties: - hibernate: - format_sql: false - -server: - port: ${PORT:8080} - -jwt: - secret: ${JWT_SECRET} - expiration: ${JWT_EXPIRATION:86400000} - -agora: - app-id: ${AGORA_APP_ID} - app-certificate: ${AGORA_APP_CERTIFICATE} - -logging: - level: - com.walkguide: INFO - org.springframework.messaging: WARN - org.springframework.web.socket: WARN \ No newline at end of file +DB_URL=jdbc:postgresql://:/ +DB_USERNAME= +DB_PASSWORD= +JWT_SECRET= +JWT_EXPIRATION=86400000 +AGORA_APP_ID= +AGORA_APP_CERTIFICATE= +FIREBASE_CREDENTIALS_PATH=classpath:firebase/google-services-admin.json +FIREBASE_NOTIFICATIONS_COLLECTION=notifications diff --git a/walkguide-backend/demo/src/main/resources/application-dev.yml b/walkguide-backend/demo/src/main/resources/application-dev.yml index 58e9101..6859f47 100644 --- a/walkguide-backend/demo/src/main/resources/application-dev.yml +++ b/walkguide-backend/demo/src/main/resources/application-dev.yml @@ -6,9 +6,9 @@ spring: datasource: - url: ${DB_URL:jdbc:postgresql://202.46.28.160:2002/uas_5803024001} - username: ${DB_USERNAME:5803024001} - password: ${DB_PASSWORD:pw5803024001} + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} hikari: maximum-pool-size: ${DB_POOL_MAX:1} minimum-idle: ${DB_POOL_MIN_IDLE:0} @@ -16,8 +16,8 @@ spring: idle-timeout: ${DB_IDLE_TIMEOUT:30000} max-lifetime: ${DB_MAX_LIFETIME:120000} - flyway: - enabled: ${FLYWAY_ENABLED:false} + flyway: + enabled: ${FLYWAY_ENABLED:true} jpa: show-sql: true @@ -26,12 +26,12 @@ spring: format_sql: true jwt: - secret: ${JWT_SECRET:d2Fsa2d1aWRlLWRldi1qd3Qtc2VjcmV0LWtleS0zMi1ieXRlcy1taW5pbXVt} + secret: ${JWT_SECRET} expiration: ${JWT_EXPIRATION:86400000} -agora: - app-id: ${AGORA_APP_ID:e36c2b6592e34cfda1f6ea6432a5e68d} - app-certificate: ${AGORA_APP_CERTIFICATE:70a4288475734a8c92ff8686c66cbc77} +agora: + app-id: ${AGORA_APP_ID:} + app-certificate: ${AGORA_APP_CERTIFICATE:} logging: level: diff --git a/walkguide-backend/demo/src/main/resources/application.properties b/walkguide-backend/demo/src/main/resources/application.properties index 3fc8d3b..f1d4753 100644 --- a/walkguide-backend/demo/src/main/resources/application.properties +++ b/walkguide-backend/demo/src/main/resources/application.properties @@ -1,12 +1,12 @@ # ===== SERVER ===== -spring.config.import=optional:file:./secrets.properties +spring.config.import=optional:file:./secrets.properties,optional:file:walkguide-backend/demo/secrets.properties server.port=${SERVER_PORT:8080} server.address=${SERVER_ADDRESS:0.0.0.0} # ===== POSTGRESQL CONNECTION ===== -spring.datasource.url=${DB_URL:jdbc:postgresql://202.46.28.160:2002/uas_5803024001} -spring.datasource.username=${DB_USERNAME:5803024001} -spring.datasource.password=${DB_PASSWORD:pw5803024001} +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # ===== HIKARI POOL (keep DB classroom slots low) ===== spring.datasource.hikari.maximum-pool-size=${DB_POOL_MAX:1} @@ -27,7 +27,7 @@ spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true # ===== JWT ===== -jwt.secret=${JWT_SECRET:d2Fsa2d1aWRlLWRldi1qd3Qtc2VjcmV0LWtleS0zMi1ieXRlcy1taW5pbXVt} +jwt.secret=${JWT_SECRET} jwt.expiration=${JWT_EXPIRATION:86400000} # ===== SWAGGER ===== @@ -35,7 +35,7 @@ springdoc.swagger-ui.path=/swagger-ui.html springdoc.api-docs.path=/v3/api-docs # ===== AGORA RTC ===== -agora.app-id=${AGORA_APP_ID:e36c2b6592e34cfda1f6ea6432a5e68d} +agora.app-id=${AGORA_APP_ID:} agora.app-certificate=${AGORA_APP_CERTIFICATE:} # ===== FIREBASE ===== diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V18__add_battery_to_location_history.sql b/walkguide-backend/demo/src/main/resources/db/migration/V18__add_battery_to_location_history.sql new file mode 100644 index 0000000..2c06be3 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V18__add_battery_to_location_history.sql @@ -0,0 +1,2 @@ +ALTER TABLE location_history + ADD COLUMN IF NOT EXISTS battery_level INTEGER; diff --git a/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml b/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml index 3ac360a..2a004e9 100644 --- a/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml +++ b/walkguide-mobile/walkguide_app/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + @@ -13,7 +16,8 @@ + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> + diff --git a/walkguide-mobile/walkguide_app/assets/icons/walkguide_launcher.svg b/walkguide-mobile/walkguide_app/assets/icons/walkguide_launcher.svg new file mode 100644 index 0000000..141a19f --- /dev/null +++ b/walkguide-mobile/walkguide_app/assets/icons/walkguide_launcher.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/walkguide-mobile/walkguide_app/lib/app/app.dart b/walkguide-mobile/walkguide_app/lib/app/app.dart index b72fbdd..705b440 100644 --- a/walkguide-mobile/walkguide_app/lib/app/app.dart +++ b/walkguide-mobile/walkguide_app/lib/app/app.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:google_fonts/google_fonts.dart'; import 'app_cubit.dart'; import 'router.dart'; +import '../core/i18n/app_strings.dart'; import '../core/theme/app_colors.dart'; class WalkGuideApp extends StatelessWidget { @@ -15,126 +17,151 @@ class WalkGuideApp extends StatelessWidget { return BlocProvider( create: (_) => AppCubit(), - child: MaterialApp.router( - title: 'WalkGuide', - debugShowCheckedModeBanner: false, - routerConfig: appRouter, - theme: ThemeData( - useMaterial3: true, - colorScheme: ColorScheme.fromSeed( - seedColor: seed, - brightness: Brightness.light, - primary: seed, - secondary: AppColors.accent, - error: AppColors.danger, - ), - scaffoldBackgroundColor: AppColors.surface, - textTheme: GoogleFonts.interTextTheme().apply( - bodyColor: AppColors.text, - displayColor: AppColors.text, - ), - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), - }, - ), - appBarTheme: const AppBarTheme( - centerTitle: false, - backgroundColor: AppColors.surface, - foregroundColor: AppColors.text, - elevation: 0, - surfaceTintColor: Colors.transparent, - ), - cardTheme: CardThemeData( - elevation: 0, - color: AppColors.surfaceRaised, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide(color: AppColors.border), + child: BlocBuilder( + builder: (context, state) => MaterialApp.router( + title: 'WalkGuide', + debugShowCheckedModeBanner: false, + routerConfig: appRouter, + builder: (context, child) { + final media = MediaQuery.of(context); + return MediaQuery( + data: media.copyWith( + textScaler: media.textScaler.clamp( + minScaleFactor: 0.9, + maxScaleFactor: 1.15, + ), + ), + child: child ?? const SizedBox.shrink(), + ); + }, + locale: state.localeCode == 'en-US' + ? const Locale('en', 'US') + : const Locale('id', 'ID'), + supportedLocales: AppStrings.supportedLocales, + localizationsDelegates: const [ + AppStringsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + theme: ThemeData( + useMaterial3: true, + visualDensity: VisualDensity.adaptivePlatformDensity, + colorScheme: ColorScheme.fromSeed( + seedColor: seed, + brightness: Brightness.light, + primary: seed, + secondary: AppColors.accent, + error: AppColors.danger, ), - ), - dividerTheme: const DividerThemeData( - color: AppColors.border, - thickness: 1, - space: 1, - ), - iconButtonTheme: IconButtonThemeData( - style: IconButton.styleFrom( + scaffoldBackgroundColor: AppColors.surface, + textTheme: GoogleFonts.interTextTheme().apply( + bodyColor: AppColors.text, + displayColor: AppColors.text, + ), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), + }, + ), + appBarTheme: const AppBarTheme( + centerTitle: false, + backgroundColor: AppColors.surface, foregroundColor: AppColors.text, - backgroundColor: Colors.white, + elevation: 0, + surfaceTintColor: Colors.transparent, + ), + cardTheme: CardThemeData( + elevation: 0, + color: AppColors.surfaceRaised, + surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: const BorderSide(color: AppColors.border), ), ), - ), - navigationBarTheme: NavigationBarThemeData( - elevation: 0, - height: 76, - backgroundColor: Colors.white, - indicatorColor: const Color(0xFFDDEAFE), - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStateProperty.resolveWith( - (states) => TextStyle( - fontSize: 12, - fontWeight: states.contains(WidgetState.selected) - ? FontWeight.w800 - : FontWeight.w500, + dividerTheme: const DividerThemeData( + color: AppColors.border, + thickness: 1, + space: 1, + ), + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + foregroundColor: AppColors.text, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide(color: AppColors.border), + ), ), ), - ), - filledButtonTheme: FilledButtonThemeData( - style: FilledButton.styleFrom( - backgroundColor: seed, - foregroundColor: Colors.white, - minimumSize: const Size(0, 50), - textStyle: const TextStyle(fontWeight: FontWeight.w800), + navigationBarTheme: NavigationBarThemeData( + elevation: 0, + height: 76, + backgroundColor: Colors.white, + indicatorColor: const Color(0xFFDDEAFE), + surfaceTintColor: Colors.transparent, + labelTextStyle: WidgetStateProperty.resolveWith( + (states) => TextStyle( + fontSize: 12, + fontWeight: states.contains(WidgetState.selected) + ? FontWeight.w800 + : FontWeight.w500, + ), + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: seed, + foregroundColor: Colors.white, + minimumSize: const Size(0, 50), + textStyle: const TextStyle(fontWeight: FontWeight.w800), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + minimumSize: const Size(0, 50), + foregroundColor: seed, + textStyle: const TextStyle(fontWeight: FontWeight.w800), + side: const BorderSide(color: AppColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + backgroundColor: AppColors.text, + contentTextStyle: GoogleFonts.inter( + color: Colors.white, + fontWeight: FontWeight.w600, + ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(8), ), ), - ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - minimumSize: const Size(0, 50), - foregroundColor: seed, - textStyle: const TextStyle(fontWeight: FontWeight.w800), - side: const BorderSide(color: AppColors.border), - shape: RoundedRectangleBorder( + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: seed, width: 1.5), ), - ), - ), - snackBarTheme: SnackBarThemeData( - behavior: SnackBarBehavior.floating, - backgroundColor: AppColors.text, - contentTextStyle: GoogleFonts.inter( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: Colors.white, - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: seed, width: 1.5), ), ), ), diff --git a/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart b/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart index c775192..8f33802 100644 --- a/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart +++ b/walkguide-mobile/walkguide_app/lib/app/app_cubit.dart @@ -4,14 +4,26 @@ class AppState { final bool online; final String? role; final String? serverUrl; + final String localeCode; - const AppState({required this.online, this.role, this.serverUrl}); + const AppState({ + required this.online, + this.role, + this.serverUrl, + this.localeCode = 'id-ID', + }); - AppState copyWith({bool? online, String? role, String? serverUrl}) { + AppState copyWith({ + bool? online, + String? role, + String? serverUrl, + String? localeCode, + }) { return AppState( online: online ?? this.online, role: role ?? this.role, serverUrl: serverUrl ?? this.serverUrl, + localeCode: localeCode ?? this.localeCode, ); } } @@ -25,5 +37,7 @@ class AppCubit extends Cubit { void setOnline(bool value) => emit(state.copyWith(online: value)); + void setLocaleCode(String value) => emit(state.copyWith(localeCode: value)); + void clearSession() => emit(const AppState(online: true)); } diff --git a/walkguide-mobile/walkguide_app/lib/app/injection_container.dart b/walkguide-mobile/walkguide_app/lib/app/injection_container.dart index 8550976..b5c1191 100644 --- a/walkguide-mobile/walkguide_app/lib/app/injection_container.dart +++ b/walkguide-mobile/walkguide_app/lib/app/injection_container.dart @@ -1,6 +1,4 @@ import 'package:get_it/get_it.dart'; -import 'package:flutter/foundation.dart'; - import '../core/constants/app_constants.dart'; import '../core/ai/obstacle_alert_strategy.dart'; import '../core/ai/obstacle_analyzer.dart'; @@ -85,12 +83,5 @@ Future initDependencies() async { } await ignoreInitFailure(() => sl().init(), label: 'TTS init'); - await sl().init(); - if (!kIsWeb) { - await ignoreInitFailure(() => sl().init(), label: 'STT init'); - } sl().loadDefaultCommands(); - if (!kIsWeb) { - await sl().init(); - } } diff --git a/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart b/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart index c6d2de0..7ae8d8b 100644 --- a/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart +++ b/walkguide-mobile/walkguide_app/lib/core/constants/app_constants.dart @@ -22,6 +22,9 @@ class AppConstants { if (!cleaned.startsWith('http://') && !cleaned.startsWith('https://')) { cleaned = 'http://$cleaned'; } + cleaned = cleaned + .replaceFirst('://localhost', '://127.0.0.1') + .replaceFirst('://0.0.0.0', '://127.0.0.1'); while (cleaned.endsWith('/')) { cleaned = cleaned.substring(0, cleaned.length - 1); } @@ -61,7 +64,6 @@ class AppConstants { await prefs.setString(_selectedYoloModelKey, path); } - // Agora App ID tetap bisa dioverride saat build: --dart-define=AGORA_APP_ID=... - static const String agoraAppId = String.fromEnvironment('AGORA_APP_ID', - defaultValue: 'e36c2b6592e34cfda1f6ea6432a5e68d'); + // Set at build time with: --dart-define=AGORA_APP_ID=your_app_id + static const String agoraAppId = String.fromEnvironment('AGORA_APP_ID'); } diff --git a/walkguide-mobile/walkguide_app/lib/core/errors/friendly_error.dart b/walkguide-mobile/walkguide_app/lib/core/errors/friendly_error.dart index 4334958..7268627 100644 --- a/walkguide-mobile/walkguide_app/lib/core/errors/friendly_error.dart +++ b/walkguide-mobile/walkguide_app/lib/core/errors/friendly_error.dart @@ -75,6 +75,8 @@ bool _looksTechnical(String message) { 'duplicate key', 'constraint', 'sql [', + 'illegal base64', + 'base64 character', ]; return blocked.any(lower.contains); } diff --git a/walkguide-mobile/walkguide_app/lib/core/i18n/app_strings.dart b/walkguide-mobile/walkguide_app/lib/core/i18n/app_strings.dart index d1bea0f..cb60b92 100644 --- a/walkguide-mobile/walkguide_app/lib/core/i18n/app_strings.dart +++ b/walkguide-mobile/walkguide_app/lib/core/i18n/app_strings.dart @@ -1,9 +1,19 @@ +import 'package:flutter/widgets.dart'; + class AppStrings { final String localeCode; const AppStrings(this.localeCode); - static const supportedLocales = ['id-ID', 'en-US']; + static const supportedLocales = [ + Locale('id', 'ID'), + Locale('en', 'US'), + ]; + + static AppStrings of(BuildContext context) { + return Localizations.of(context, AppStrings) ?? + const AppStrings('id-ID'); + } String get walkGuideStarted => _pick( id: 'WalkGuide dimulai', @@ -29,3 +39,21 @@ class AppStrings { return localeCode == 'en-US' ? en : id; } } + +class AppStringsDelegate extends LocalizationsDelegate { + const AppStringsDelegate(); + + @override + bool isSupported(Locale locale) { + return locale.languageCode == 'id' || locale.languageCode == 'en'; + } + + @override + Future load(Locale locale) async { + final code = locale.languageCode == 'en' ? 'en-US' : 'id-ID'; + return AppStrings(code); + } + + @override + bool shouldReload(covariant LocalizationsDelegate old) => false; +} diff --git a/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart b/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart index ec617ba..6df900b 100644 --- a/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart +++ b/walkguide-mobile/walkguide_app/lib/core/network/api_client.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import '../constants/app_constants.dart'; import '../storage/secure_storage.dart'; @@ -24,8 +25,15 @@ class ApiClient { _dio.interceptors.addAll([ _AuthInterceptor(_secureStorage, _dio), _ErrorInterceptor(), - LogInterceptor(requestBody: true, responseBody: true), ]); + if (kDebugMode) { + _dio.interceptors.add(LogInterceptor( + requestBody: false, + responseBody: false, + requestHeader: false, + responseHeader: false, + )); + } } Dio get dio => _dio; @@ -42,7 +50,8 @@ class _AuthInterceptor extends Interceptor { _AuthInterceptor(this._storage, this._dio); @override - void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { + void onRequest( + RequestOptions options, RequestInterceptorHandler handler) async { final token = await _storage.getAccessToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; diff --git a/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart b/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart index 06b59c7..ddded54 100644 --- a/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart +++ b/walkguide-mobile/walkguide_app/lib/core/services/call_service.dart @@ -166,6 +166,11 @@ class CallService { debugPrint('Agora remote user offline: $remoteUid $reason'); _onRemoteUserOffline?.call(); }, + onRemoteAudioStateChanged: (_, remoteUid, state, reason, __) { + debugPrint( + 'Agora remote audio state: uid=$remoteUid state=$state reason=$reason', + ); + }, onError: (type, msg) { debugPrint('Agora error: $type $msg'); if (!joinCompleter.isCompleted) joinCompleter.complete(false); @@ -175,9 +180,18 @@ class CallService { await _engine!.setChannelProfile( ChannelProfileType.channelProfileCommunication, ); + await _engine!.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + await _engine!.setAudioProfile( + profile: AudioProfileType.audioProfileDefault, + scenario: AudioScenarioType.audioScenarioMeeting, + ); await _engine!.enableAudio(); await _engine!.enableLocalAudio(true); + await _engine!.muteAllRemoteAudioStreams(false); await _engine!.muteLocalAudioStream(false); + await _engine!.adjustRecordingSignalVolume(100); + await _engine!.adjustPlaybackSignalVolume(100); + await _engine!.setDefaultAudioRouteToSpeakerphone(true); await _engine!.setEnableSpeakerphone(true); await _engine!.joinChannel( token: token ?? '', diff --git a/walkguide-mobile/walkguide_app/lib/core/services/fcm_service.dart b/walkguide-mobile/walkguide_app/lib/core/services/fcm_service.dart index 8c122d9..30f89a7 100644 --- a/walkguide-mobile/walkguide_app/lib/core/services/fcm_service.dart +++ b/walkguide-mobile/walkguide_app/lib/core/services/fcm_service.dart @@ -9,7 +9,6 @@ import '../network/api_client.dart'; class FcmService { final ApiClient _apiClient; - final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin(); @@ -18,6 +17,7 @@ class FcmService { Future init() async { if (kIsWeb) return; try { + final messaging = FirebaseMessaging.instance; await _localNotifications.initialize( const InitializationSettings( android: AndroidInitializationSettings('@mipmap/ic_launcher'), @@ -31,10 +31,10 @@ class FcmService { } catch (_) {} }, ); - await _messaging.requestPermission(alert: true, badge: true, sound: true); - final token = await _messaging.getToken(); + await messaging.requestPermission(alert: true, badge: true, sound: true); + final token = await messaging.getToken(); if (token != null) await syncToken(token); - FirebaseMessaging.instance.onTokenRefresh.listen(syncToken); + messaging.onTokenRefresh.listen(syncToken); FirebaseMessaging.onMessage.listen((message) { debugPrint('FCM foreground: ${message.data}'); _showLocalNotification(message); @@ -55,6 +55,10 @@ class FcmService { Future syncToken(String token) async { try { + if (_apiClient.baseUrl == null) { + debugPrint('FCM token sync skipped: server URL is not ready.'); + return; + } await _apiClient.dio.put('/auth/fcm-token', data: {'fcmToken': token}); } catch (e) { debugPrint('FCM token sync skipped: $e'); diff --git a/walkguide-mobile/walkguide_app/lib/core/services/location_reporter_service.dart b/walkguide-mobile/walkguide_app/lib/core/services/location_reporter_service.dart index f305b7f..fbc8b13 100644 --- a/walkguide-mobile/walkguide_app/lib/core/services/location_reporter_service.dart +++ b/walkguide-mobile/walkguide_app/lib/core/services/location_reporter_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:battery_plus/battery_plus.dart'; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; @@ -10,6 +11,7 @@ import 'offline_queue_service.dart'; class LocationReporterService { final ApiClient _apiClient; final OfflineQueueService _offlineQueue; + final Battery _battery = Battery(); Timer? _timer; LocationReporterService(this._apiClient, this._offlineQueue); @@ -32,12 +34,14 @@ class LocationReporterService { try { await Geolocator.requestPermission(); final position = await Geolocator.getCurrentPosition(); + final batteryLevel = await _readBatteryLevel(); await _apiClient.dio.post('/user/location', data: { 'lat': position.latitude, 'lng': position.longitude, 'accuracy': position.accuracy, 'speed': position.speed, 'heading': position.heading, + if (batteryLevel != null) 'batteryLevel': batteryLevel, }); } on DioException catch (_) { await _offlineQueue.enqueue(OfflineRequest( @@ -50,4 +54,12 @@ class LocationReporterService { // GPS permission can be unavailable during desktop/web testing. } } + + Future _readBatteryLevel() async { + try { + return await _battery.batteryLevel; + } catch (_) { + return null; + } + } } diff --git a/walkguide-mobile/walkguide_app/lib/core/services/voice_command_handler.dart b/walkguide-mobile/walkguide_app/lib/core/services/voice_command_handler.dart index 5916c4f..5e2f9ea 100644 --- a/walkguide-mobile/walkguide_app/lib/core/services/voice_command_handler.dart +++ b/walkguide-mobile/walkguide_app/lib/core/services/voice_command_handler.dart @@ -19,6 +19,8 @@ class VoiceCommand { /// Callback yang dipanggil saat command terdeteksi /// Registered oleh router/screen yang relevan typedef CommandCallback = void Function(VoiceCommandKey key); +typedef CommandRouter = void Function(String route); +typedef CommandAction = void Function(); class VoiceCommandHandler { final SttService _stt; @@ -26,9 +28,19 @@ class VoiceCommandHandler { List _commands = []; CommandCallback? onCommand; + CommandRouter? _router; + final Map _actions = {}; VoiceCommandHandler(this._stt, this._tts); + void registerRouter(CommandRouter router) { + _router = router; + } + + void registerAction(VoiceCommandKey key, CommandAction action) { + _actions[key] = action; + } + void loadCommands(List commands) { _commands = commands; _stt.onResult = _processText; @@ -66,9 +78,28 @@ class VoiceCommandHandler { } void _handleCommand(VoiceCommandKey key) { + _routeFor(key); + _actions[key]?.call(); onCommand?.call(key); // Built-in actions for TTS-only commands if (key == VoiceCommandKey.repeatLast) _tts.repeatLast(); if (key == VoiceCommandKey.stopTts) _tts.stop(); } + + void _routeFor(VoiceCommandKey key) { + final route = switch (key) { + VoiceCommandKey.openWalkguide || VoiceCommandKey.startWalkguide => + '/user/walkguide', + VoiceCommandKey.openNotification => '/user/notifications', + VoiceCommandKey.openSos || VoiceCommandKey.sendSos => '/user/sos', + VoiceCommandKey.openActivity => '/user/activity', + VoiceCommandKey.openNavigation => '/user/navigation', + VoiceCommandKey.openSettings => '/user/settings', + VoiceCommandKey.callGuardian => '/call', + _ => null, + }; + if (route != null) { + _router?.call(route); + } + } } diff --git a/walkguide-mobile/walkguide_app/lib/features/activity_log/application/README.md b/walkguide-mobile/walkguide_app/lib/features/activity_log/application/README.md new file mode 100644 index 0000000..acd1e19 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/activity_log/application/README.md @@ -0,0 +1,3 @@ +Activity log application layer. + +This layer is reserved for Cubit/BLoC orchestration between the activity log UI and domain contracts. diff --git a/walkguide-mobile/walkguide_app/lib/features/activity_log/data/README.md b/walkguide-mobile/walkguide_app/lib/features/activity_log/data/README.md new file mode 100644 index 0000000..13d7015 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/activity_log/data/README.md @@ -0,0 +1,3 @@ +Activity log data layer. + +This layer is reserved for remote/local data sources and repository implementations. diff --git a/walkguide-mobile/walkguide_app/lib/features/activity_log/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/activity_log/domain/README.md new file mode 100644 index 0000000..06ac154 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/activity_log/domain/README.md @@ -0,0 +1,3 @@ +Activity log domain layer. + +This layer is reserved for entities, repository contracts, and use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/application/README.md b/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/application/README.md new file mode 100644 index 0000000..0fae0ad --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/application/README.md @@ -0,0 +1,3 @@ +AI benchmark application layer. + +This layer is reserved for benchmark Cubit/BLoC orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/data/README.md b/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/data/README.md new file mode 100644 index 0000000..6b410f6 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/data/README.md @@ -0,0 +1,3 @@ +AI benchmark data layer. + +This layer is reserved for benchmark result persistence and export adapters. diff --git a/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/domain/README.md new file mode 100644 index 0000000..d7f59d8 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/ai_benchmark/domain/README.md @@ -0,0 +1,3 @@ +AI benchmark domain layer. + +This layer is reserved for benchmark entities and use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/auth/application/README.md b/walkguide-mobile/walkguide_app/lib/features/auth/application/README.md new file mode 100644 index 0000000..29cd0a9 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/auth/application/README.md @@ -0,0 +1,3 @@ +Auth application layer. + +This layer is reserved for auth Cubit/BLoC orchestration between auth UI and auth domain contracts. diff --git a/walkguide-mobile/walkguide_app/lib/features/auth/login_screen.dart b/walkguide-mobile/walkguide_app/lib/features/auth/login_screen.dart index 12aa434..a62bbf1 100644 --- a/walkguide-mobile/walkguide_app/lib/features/auth/login_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/auth/login_screen.dart @@ -151,163 +151,182 @@ class _AuthFrame extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFEAF4FF), - body: Stack( - children: [ - const Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], - ), - ), - ), - ), - Positioned( - top: -90, - right: -60, - child: TweenAnimationBuilder( - tween: Tween(begin: 0.85, end: 1), - duration: const Duration(milliseconds: 900), - curve: Curves.easeOutCubic, - builder: (_, value, child) => Transform.scale( - scale: value, - child: child, - ), - child: Container( - width: 260, - height: 260, - decoration: BoxDecoration( - color: const Color(0xFF2563EB).withValues(alpha: 0.14), - shape: BoxShape.circle, - ), - ), - ), - ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 430), - child: TweenAnimationBuilder( - tween: Tween(begin: 18, end: 0), - duration: const Duration(milliseconds: 520), - curve: Curves.easeOutCubic, - builder: (_, offset, child) => Opacity( - opacity: (1 - offset / 18).clamp(0.0, 1.0), - child: Transform.translate( - offset: Offset(0, offset), - child: child, + body: LayoutBuilder( + builder: (context, constraints) { + final compact = + constraints.maxWidth < 480 || constraints.maxHeight < 720; + return Stack( + children: [ + const Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], ), ), - child: RepaintBoundary( - child: Container( - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.96), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Colors.white.withValues(alpha: 0.8)), - boxShadow: [ - BoxShadow( - color: - const Color(0xFF1E3A8A).withValues(alpha: 0.14), - blurRadius: 40, - offset: const Offset(0, 20), - ), - ], + ), + ), + Positioned( + top: compact ? -70 : -90, + right: compact ? -70 : -60, + child: Container( + width: compact ? 180 : 260, + height: compact ? 180 : 260, + decoration: BoxDecoration( + color: const Color(0xFF2563EB).withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + ), + ), + Center( + child: SingleChildScrollView( + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.fromLTRB( + compact ? 14 : 24, + compact ? 12 : 24, + compact ? 14 : 24, + 20 + MediaQuery.of(context).viewInsets.bottom, + ), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: compact ? 380 : 430), + child: TweenAnimationBuilder( + tween: Tween(begin: 18, end: 0), + duration: const Duration(milliseconds: 520), + curve: Curves.easeOutCubic, + builder: (_, offset, child) => Opacity( + opacity: (1 - offset / 18).clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, offset), + child: child, + ), ), - child: Padding( - padding: const EdgeInsets.fromLTRB(24, 26, 24, 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( + child: RepaintBoundary( + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + borderRadius: + BorderRadius.circular(compact ? 22 : 30), + border: Border.all( + color: Colors.white.withValues(alpha: 0.8)), + boxShadow: [ + BoxShadow( + color: const Color(0xFF1E3A8A) + .withValues(alpha: 0.14), + blurRadius: compact ? 24 : 40, + offset: const Offset(0, 18), + ), + ], + ), + child: Padding( + padding: EdgeInsets.fromLTRB( + compact ? 18 : 24, + compact ? 18 : 26, + compact ? 18 : 24, + compact ? 18 : 24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - Color(0xFF2563EB), - Color(0xFF0891B2) + Row( + children: [ + Container( + width: compact ? 44 : 56, + height: compact ? 44 : 56, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF2563EB), + Color(0xFF0891B2) + ], + ), + borderRadius: BorderRadius.circular(16), + ), + child: Icon(Icons.navigation_rounded, + color: Colors.white, + size: compact ? 26 : 30), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'WalkGuide', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Color(0xFF0F172A), + ), + ), + ), + ], + ), + SizedBox(height: compact ? 14 : 16), + if (!compact) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(999), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.shield_outlined, + size: 14, color: Color(0xFF1D4ED8)), + SizedBox(width: 6), + Text( + 'Secure Assistive Navigation', + style: TextStyle( + color: Color(0xFF1D4ED8), + fontSize: 11, + fontWeight: FontWeight.w800, + ), + ), ], ), - borderRadius: BorderRadius.circular(18), ), - child: const Icon(Icons.navigation_rounded, - color: Colors.white, size: 30), + if (!compact) const SizedBox(height: 18), + Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontSize: compact ? 26 : null, + fontWeight: FontWeight.w900, + color: const Color(0xFF0F172A), + ), ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'WalkGuide', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: Color(0xFF0F172A), - ), + const SizedBox(height: 6), + Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFF64748B), + height: 1.35, ), ), + SizedBox(height: compact ? 18 : 26), + child, ], ), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: const Color(0xFFEFF6FF), - borderRadius: BorderRadius.circular(999), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.shield_outlined, - size: 14, color: Color(0xFF1D4ED8)), - SizedBox(width: 6), - Text( - 'Secure Assistive Navigation', - style: TextStyle( - color: Color(0xFF1D4ED8), - fontSize: 11, - fontWeight: FontWeight.w800, - ), - ), - ], - ), - ), - const SizedBox(height: 18), - Text( - title, - style: Theme.of(context) - .textTheme - .headlineMedium - ?.copyWith( - fontWeight: FontWeight.w900, - color: const Color(0xFF0F172A), - ), - ), - const SizedBox(height: 6), - Text( - subtitle, - style: const TextStyle( - color: Color(0xFF64748B), - height: 1.35, - ), - ), - const SizedBox(height: 26), - child, - ], + ), ), ), ), ), ), ), - ), - ), - ], + ], + ); + }, ), ); } diff --git a/walkguide-mobile/walkguide_app/lib/features/auth/register_screen.dart b/walkguide-mobile/walkguide_app/lib/features/auth/register_screen.dart index 7362b7c..39a1671 100644 --- a/walkguide-mobile/walkguide_app/lib/features/auth/register_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/auth/register_screen.dart @@ -299,133 +299,151 @@ class _AuthFrame extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFEAF4FF), - body: Stack( - children: [ - const Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], - ), - ), - ), - ), - Positioned( - top: -90, - right: -60, - child: TweenAnimationBuilder( - tween: Tween(begin: 0.85, end: 1), - duration: const Duration(milliseconds: 900), - curve: Curves.easeOutCubic, - builder: (_, value, child) => Transform.scale( - scale: value, - child: child, - ), - child: Container( - width: 260, - height: 260, - decoration: BoxDecoration( - color: const Color(0xFF2563EB).withValues(alpha: 0.14), - shape: BoxShape.circle, - ), - ), - ), - ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 430), - child: TweenAnimationBuilder( - tween: Tween(begin: 18, end: 0), - duration: const Duration(milliseconds: 520), - curve: Curves.easeOutCubic, - builder: (_, offset, child) => Opacity( - opacity: (1 - offset / 18).clamp(0.0, 1.0), - child: Transform.translate( - offset: Offset(0, offset), - child: child, + body: LayoutBuilder( + builder: (context, constraints) { + final compact = + constraints.maxWidth < 480 || constraints.maxHeight < 720; + return Stack( + children: [ + const Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], ), ), - child: RepaintBoundary( - child: Container( - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.96), - borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Colors.white.withValues(alpha: 0.8)), - boxShadow: [ - BoxShadow( - color: - const Color(0xFF1E3A8A).withValues(alpha: 0.14), - blurRadius: 40, - offset: const Offset(0, 20), - ), - ], + ), + ), + Positioned( + top: compact ? -70 : -90, + right: compact ? -70 : -60, + child: Container( + width: compact ? 180 : 260, + height: compact ? 180 : 260, + decoration: BoxDecoration( + color: const Color(0xFF2563EB).withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + ), + ), + Center( + child: SingleChildScrollView( + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.fromLTRB( + compact ? 14 : 24, + compact ? 12 : 24, + compact ? 14 : 24, + 20 + MediaQuery.of(context).viewInsets.bottom, + ), + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: compact ? 380 : 430), + child: TweenAnimationBuilder( + tween: Tween(begin: 18, end: 0), + duration: const Duration(milliseconds: 520), + curve: Curves.easeOutCubic, + builder: (_, offset, child) => Opacity( + opacity: (1 - offset / 18).clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, offset), + child: child, + ), ), - child: Padding( - padding: const EdgeInsets.fromLTRB(24, 26, 24, 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( + child: RepaintBoundary( + child: Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.96), + borderRadius: + BorderRadius.circular(compact ? 22 : 30), + border: Border.all( + color: Colors.white.withValues(alpha: 0.8)), + boxShadow: [ + BoxShadow( + color: const Color(0xFF1E3A8A) + .withValues(alpha: 0.14), + blurRadius: compact ? 24 : 40, + offset: const Offset(0, 18), + ), + ], + ), + child: Padding( + padding: EdgeInsets.fromLTRB( + compact ? 18 : 24, + compact ? 18 : 26, + compact ? 18 : 24, + compact ? 18 : 24, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: const Color(0xFF1D4ED8), - borderRadius: BorderRadius.circular(18), - ), - child: const Icon(Icons.navigation_rounded, - color: Colors.white, size: 30), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'WalkGuide', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: Color(0xFF0F172A), + Row( + children: [ + Container( + width: compact ? 44 : 56, + height: compact ? 44 : 56, + decoration: BoxDecoration( + color: const Color(0xFF1D4ED8), + borderRadius: BorderRadius.circular(16), + ), + child: Icon(Icons.navigation_rounded, + color: Colors.white, + size: compact ? 26 : 30), ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'WalkGuide', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Color(0xFF0F172A), + ), + ), + ), + ], + ), + SizedBox(height: compact ? 14 : 22), + Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontSize: compact ? 26 : null, + fontWeight: FontWeight.w900, + color: const Color(0xFF0F172A), + ), + ), + const SizedBox(height: 6), + Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Color(0xFF64748B), + height: 1.35, ), ), + SizedBox(height: compact ? 18 : 26), + child, ], ), - const SizedBox(height: 22), - Text( - title, - style: Theme.of(context) - .textTheme - .headlineMedium - ?.copyWith( - fontWeight: FontWeight.w900, - color: const Color(0xFF0F172A), - ), - ), - const SizedBox(height: 6), - Text( - subtitle, - style: const TextStyle( - color: Color(0xFF64748B), - height: 1.35, - ), - ), - const SizedBox(height: 26), - child, - ], + ), ), ), ), ), ), ), - ), - ), - ], + ], + ); + }, ), ); } diff --git a/walkguide-mobile/walkguide_app/lib/features/call/application/README.md b/walkguide-mobile/walkguide_app/lib/features/call/application/README.md new file mode 100644 index 0000000..d9023fc --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/call/application/README.md @@ -0,0 +1,3 @@ +Call application layer. + +This layer owns call state orchestration. The current route keeps a compatibility screen while call side effects are delegated to core services. diff --git a/walkguide-mobile/walkguide_app/lib/features/call/call_screen.dart b/walkguide-mobile/walkguide_app/lib/features/call/call_screen.dart index c53e487..553b66d 100644 --- a/walkguide-mobile/walkguide_app/lib/features/call/call_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/call/call_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../app/injection_container.dart'; +import '../../core/errors/friendly_error.dart'; import '../../core/services/call_service.dart'; import '../../core/services/haptic_service.dart'; import '../../core/services/tts_service.dart'; @@ -63,65 +64,71 @@ class _CallScreenState extends State unawaited(_finishRemoteEnded()); }); - try { - final invite = await callService.startPairedCall(); - if (!mounted) return; - if (invite == null) { + final invite = await runFriendly>( + () => callService.startPairedCall(), + onError: _failCall, + fallback: 'Panggilan gagal. Server tidak merespons.', + ); + if (!mounted) return; + if (invite == null) { + if (_phase != _CallPhase.failed) { _failCall('Panggilan gagal. Pastikan sudah pairing dan server aktif.'); - return; } - - _otherId = _asInt(invite['receiverId']); - _activeChannel = invite['channelName']?.toString(); - setState(() => _phase = _CallPhase.calling); - sl().speak( - 'Panggilan dikirim ke ${widget.targetLabel}. Menunggu jawaban.', - ); - _startAcceptedPolling(); - _ringTimeout?.cancel(); - _ringTimeout = Timer(const Duration(seconds: 45), () { - if (!mounted || _phase == _CallPhase.connected) return; - _failCall('Panggilan tidak dijawab.'); - }); - } catch (_) { - if (!mounted) return; - _failCall('Panggilan gagal. Server tidak merespons.'); + return; } + + _otherId = _asInt(invite['receiverId']); + _activeChannel = invite['channelName']?.toString(); + setState(() => _phase = _CallPhase.calling); + sl().speak( + 'Panggilan dikirim ke ${widget.targetLabel}. Menunggu jawaban.', + ); + _startAcceptedPolling(); + _ringTimeout?.cancel(); + _ringTimeout = Timer(const Duration(seconds: 45), () { + if (!mounted || _phase == _CallPhase.connected) return; + _failCall('Panggilan tidak dijawab.'); + }); } void _startAcceptedPolling() { _acceptedPoll?.cancel(); _acceptedPoll = Timer.periodic(const Duration(seconds: 2), (_) async { if (!mounted || _activeChannel == null) return; - try { - final state = await sl() + final state = await runFriendly>( + () => sl() .getCallState(_activeChannel) - .timeout(const Duration(seconds: 3)); - final status = state?['status']?.toString(); - if (status == 'ENDED') { - await _finishRemoteEnded(); - return; - } - if (status == 'ACCEPTED') { - _markRemoteConnected(); - return; - } - - final accepted = await sl() - .getAcceptedCall() - .timeout(const Duration(seconds: 3)); - if (accepted?['type']?.toString() != 'CALL_ACCEPTED') return; - final channel = accepted?['channelName']?.toString(); - if (_activeChannel != null && - channel != null && - channel.isNotEmpty && - channel != _activeChannel) { - return; - } - _markRemoteConnected(); - } catch (_) { - // Keep ringing; a short network hiccup should not cancel the call UI. + .timeout(const Duration(seconds: 3)), + onError: (_) {}, + fallback: 'Polling panggilan gagal.', + ); + if (state == null) return; + final status = state['status']?.toString(); + if (status == 'ENDED') { + await _finishRemoteEnded(); + return; } + if (status == 'ACCEPTED') { + _markRemoteConnected(); + return; + } + + final accepted = await runFriendly>( + () => sl() + .getAcceptedCall() + .timeout(const Duration(seconds: 3)), + onError: (_) {}, + fallback: 'Polling panggilan diterima gagal.', + ); + if (accepted?['type']?.toString() != 'CALL_ACCEPTED') return; + final channel = accepted?['channelName']?.toString(); + if (_activeChannel != null && + channel != null && + channel.isNotEmpty && + channel != _activeChannel) { + return; + } + _markRemoteConnected(); }); } @@ -319,7 +326,12 @@ class _IncomingCallScreenState extends State { setState(() => _responding = true); sl().speak('Menerima panggilan.'); - final joined = await _joinIncomingChannel(); + final joined = await runFriendly( + () => _joinIncomingChannel(), + onError: (_) {}, + fallback: 'Panggilan gagal tersambung.', + ) ?? + false; if (!mounted) return; if (!joined || _joinedChannel == null || widget.callerId == null) { setState(() { @@ -350,14 +362,16 @@ class _IncomingCallScreenState extends State { _statePoll?.cancel(); _statePoll = Timer.periodic(const Duration(seconds: 2), (_) async { if (!mounted || _joinedChannel == null) return; - try { - final state = await sl() + final state = await runFriendly>( + () => sl() .getCallState(_joinedChannel) - .timeout(const Duration(seconds: 3)); - if (state?['status']?.toString() == 'ENDED') { - await _finishIncomingRemoteEnded(); - } - } catch (_) {} + .timeout(const Duration(seconds: 3)), + onError: (_) {}, + fallback: 'Polling panggilan masuk gagal.', + ); + if (state?['status']?.toString() == 'ENDED') { + await _finishIncomingRemoteEnded(); + } }); } diff --git a/walkguide-mobile/walkguide_app/lib/features/call/data/README.md b/walkguide-mobile/walkguide_app/lib/features/call/data/README.md new file mode 100644 index 0000000..e7dc353 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/call/data/README.md @@ -0,0 +1,3 @@ +Call data layer. + +This layer is reserved for call remote data sources and repository implementations over `/shared/call/**`. diff --git a/walkguide-mobile/walkguide_app/lib/features/call/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/call/domain/README.md new file mode 100644 index 0000000..a8472c0 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/call/domain/README.md @@ -0,0 +1,3 @@ +Call domain layer. + +This layer is reserved for call session entities, repository contracts, and call use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/application/README.md b/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/application/README.md new file mode 100644 index 0000000..ebb5092 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/application/README.md @@ -0,0 +1,3 @@ +Guardian dashboard application layer. + +This layer is reserved for dashboard, map, SOS, notification, AI config, shortcut, and geofence Cubits. diff --git a/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/data/README.md b/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/data/README.md new file mode 100644 index 0000000..0654410 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/data/README.md @@ -0,0 +1,3 @@ +Guardian dashboard data layer. + +This layer is reserved for `/guardian/**` data sources and repository implementations. diff --git a/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/domain/README.md new file mode 100644 index 0000000..0c9aab7 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/guardian_dashboard/domain/README.md @@ -0,0 +1,3 @@ +Guardian dashboard domain layer. + +This layer is reserved for Guardian dashboard entities, repository contracts, and use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/home/application/README.md b/walkguide-mobile/walkguide_app/lib/features/home/application/README.md new file mode 100644 index 0000000..44aa481 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/home/application/README.md @@ -0,0 +1,3 @@ +Home application layer. + +This layer is reserved for role-specific home state orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/home/data/README.md b/walkguide-mobile/walkguide_app/lib/features/home/data/README.md new file mode 100644 index 0000000..74749e8 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/home/data/README.md @@ -0,0 +1,3 @@ +Home data layer. + +This layer is reserved for home/dashboard data adapters. diff --git a/walkguide-mobile/walkguide_app/lib/features/home/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/home/domain/README.md new file mode 100644 index 0000000..201473e --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/home/domain/README.md @@ -0,0 +1,3 @@ +Home domain layer. + +This layer is reserved for home/dashboard domain entities and use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/home/presentation/guardian_dashboard_screen.dart b/walkguide-mobile/walkguide_app/lib/features/home/presentation/guardian_dashboard_screen.dart index 6306255..8b7e125 100644 --- a/walkguide-mobile/walkguide_app/lib/features/home/presentation/guardian_dashboard_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/home/presentation/guardian_dashboard_screen.dart @@ -41,6 +41,7 @@ class _GuardianDashboardScreenState extends State // ── Live location (WebSocket) ──────────────────────────────────────────────── LatLng? _liveLatLng; bool _liveConnected = false; + DateTime? _lastRealtimeStatusReload; final MapController _mapController = MapController(); // ── Pulse animation for live dot ──────────────────────────────────────────── @@ -133,12 +134,13 @@ class _GuardianDashboardScreenState extends State 'User', userOnline: userStatus?['online'] as bool? ?? false, userLastSeen: userStatus?['lastSeenAt']?.toString(), - battery: userStatus?['battery'] as int?, - speed: userStatus?['lastSpeed'] as double?, - obstaclesTotal: userStatus?['obstaclesToday'] as int? ?? - dashboard?['obstaclesToday'] as int? ?? + battery: (userStatus?['battery'] as num?)?.toInt(), + speed: (userStatus?['lastSpeed'] as num?)?.toDouble(), + obstaclesTotal: (userStatus?['obstaclesToday'] as num?)?.toInt() ?? + (dashboard?['obstaclesToday'] as num?)?.toInt() ?? 0, - unreadNotif: dashboard?['unreadNotifCount'] as int? ?? 0, + unreadNotif: + (dashboard?['unreadNotifCount'] as num?)?.toInt() ?? 0, unreadSos: sosPending, lastLat: lastLoc?['lat'] != null ? (lastLoc!['lat'] as num).toDouble() @@ -247,6 +249,12 @@ class _GuardianDashboardScreenState extends State _liveConnected = true; }); _moveMapSafely(newPos); + final now = DateTime.now(); + if (_lastRealtimeStatusReload == null || + now.difference(_lastRealtimeStatusReload!).inSeconds >= 15) { + _lastRealtimeStatusReload = now; + unawaited(_loadAll(silent: true)); + } }); ws.subscribeSos((sosData) { if (!mounted) return; diff --git a/walkguide-mobile/walkguide_app/lib/features/home/presentation/user_dashboard_screen.dart b/walkguide-mobile/walkguide_app/lib/features/home/presentation/user_dashboard_screen.dart index 61000d0..7ce64fe 100644 --- a/walkguide-mobile/walkguide_app/lib/features/home/presentation/user_dashboard_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/home/presentation/user_dashboard_screen.dart @@ -3,7 +3,6 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:camera/camera.dart'; import '../../../core/secure_storage.dart'; import '../../auth/presentation/login_screen.dart'; -import '../../../../main.dart'; class UserDashboardScreen extends StatefulWidget { const UserDashboardScreen({super.key}); @@ -12,7 +11,8 @@ class UserDashboardScreen extends StatefulWidget { State createState() => _UserDashboardScreenState(); } -class _UserDashboardScreenState extends State with TickerProviderStateMixin { +class _UserDashboardScreenState extends State + with TickerProviderStateMixin { CameraController? _camCtrl; late AnimationController _radarCtrl; late Animation _radarAnim; @@ -31,8 +31,10 @@ class _UserDashboardScreenState extends State with TickerPr } Future _initCamera() async { + final cameras = await availableCameras(); if (cameras.isEmpty) return; - _camCtrl = CameraController(cameras[0], ResolutionPreset.high, enableAudio: false); + _camCtrl = CameraController(cameras[0], ResolutionPreset.medium, + enableAudio: false); await _camCtrl!.initialize(); if (mounted) setState(() {}); } @@ -85,7 +87,8 @@ class _UserDashboardScreenState extends State with TickerPr gradient: RadialGradient( colors: [ Colors.transparent, - const Color(0xFF10B981).withValues(alpha: 0.05 + _radarCtrl.value * 0.08), + const Color(0xFF10B981) + .withValues(alpha: 0.05 + _radarCtrl.value * 0.08), ], stops: const [0.5, 1.0], radius: 1.4, @@ -127,7 +130,8 @@ class _UserDashboardScreenState extends State with TickerPr ), child: Row(children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.54), borderRadius: BorderRadius.circular(20), @@ -158,7 +162,8 @@ class _UserDashboardScreenState extends State with TickerPr const Spacer(), IconButton( onPressed: _logout, - icon: const Icon(Icons.power_settings_new, color: Colors.white, size: 26), + icon: const Icon(Icons.power_settings_new, + color: Colors.white, size: 26), style: IconButton.styleFrom( backgroundColor: Colors.redAccent.withValues(alpha: 0.8), ), @@ -204,15 +209,19 @@ class _UserDashboardScreenState extends State with TickerPr color: const Color(0x33F59E0B), borderRadius: BorderRadius.circular(7), ), - child: const Icon(Icons.warning_amber_rounded, color: Color(0xFFF59E0B), size: 16), + child: const Icon(Icons.warning_amber_rounded, + color: Color(0xFFF59E0B), size: 16), ), const SizedBox(width: 10), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Obstacle ahead', style: GoogleFonts.inter( - color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500)), + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w500)), Text('2.1m — Haptic alert sent', - style: GoogleFonts.inter(color: Colors.white60, fontSize: 11)), + style: + GoogleFonts.inter(color: Colors.white60, fontSize: 11)), ]), ]), ), @@ -234,9 +243,12 @@ class _UserDashboardScreenState extends State with TickerPr ), child: Column(children: [ Row(children: [ - Expanded(child: _bigBtn(const Color(0xCC1A56DB), Icons.mic, () {})), + Expanded( + child: _bigBtn(const Color(0xCC1A56DB), Icons.mic, () {})), const SizedBox(width: 12), - Expanded(child: _bigBtn(const Color(0xCCDC2626), Icons.phone_in_talk, () {})), + Expanded( + child: _bigBtn( + const Color(0xCCDC2626), Icons.phone_in_talk, () {})), ]), const SizedBox(height: 8), Text( @@ -290,7 +302,8 @@ class _RadarPainter extends CustomPainter { ..style = PaintingStyle.stroke ..strokeWidth = 1.2; for (final r in [48.0, 34.0, 20.0]) { - paint.color = const Color(0xFF60EFAB).withValues(alpha: 0.15 + (50 - r) / 100); + paint.color = + const Color(0xFF60EFAB).withValues(alpha: 0.15 + (50 - r) / 100); canvas.drawCircle(center, r, paint); } paint @@ -301,4 +314,4 @@ class _RadarPainter extends CustomPainter { @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} \ No newline at end of file +} diff --git a/walkguide-mobile/walkguide_app/lib/features/manual/application/README.md b/walkguide-mobile/walkguide_app/lib/features/manual/application/README.md new file mode 100644 index 0000000..125a4ed --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/manual/application/README.md @@ -0,0 +1,3 @@ +Manual application layer. + +This layer is reserved for manual/TTS instruction state orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/manual/data/README.md b/walkguide-mobile/walkguide_app/lib/features/manual/data/README.md new file mode 100644 index 0000000..8927f7d --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/manual/data/README.md @@ -0,0 +1,3 @@ +Manual data layer. + +This layer is reserved for local command and shortcut documentation data sources. diff --git a/walkguide-mobile/walkguide_app/lib/features/manual/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/manual/domain/README.md new file mode 100644 index 0000000..e6f1fc2 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/manual/domain/README.md @@ -0,0 +1,3 @@ +Manual domain layer. + +This layer is reserved for manual section entities and instruction use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/manual/presentation/README.md b/walkguide-mobile/walkguide_app/lib/features/manual/presentation/README.md new file mode 100644 index 0000000..ae198dc --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/manual/presentation/README.md @@ -0,0 +1,3 @@ +Manual presentation layer. + +This layer is reserved for manual pages and widgets. diff --git a/walkguide-mobile/walkguide_app/lib/features/navigation_mode/application/README.md b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/application/README.md new file mode 100644 index 0000000..e7b57aa --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/application/README.md @@ -0,0 +1,3 @@ +Navigation mode application layer. + +This layer is reserved for navigation Cubit/BLoC orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/navigation_mode/data/README.md b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/data/README.md new file mode 100644 index 0000000..c94b019 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/data/README.md @@ -0,0 +1,3 @@ +Navigation mode data layer. + +This layer is reserved for location, OSM, and OSRM data adapters. diff --git a/walkguide-mobile/walkguide_app/lib/features/navigation_mode/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/domain/README.md new file mode 100644 index 0000000..b12bbc2 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/domain/README.md @@ -0,0 +1,3 @@ +Navigation mode domain layer. + +This layer is reserved for route, waypoint, and navigation use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/navigation_mode/navigation_mode_screen.dart b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/navigation_mode_screen.dart index 9007a26..0efbb7d 100644 --- a/walkguide-mobile/walkguide_app/lib/features/navigation_mode/navigation_mode_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/navigation_mode/navigation_mode_screen.dart @@ -61,35 +61,40 @@ class _NavState extends Cubit { StreamSubscription? _posStream; void _set(_NavPhase p, String status) { + if (isClosed) return; phase = p; statusText = status; _notify(); } - void _notify() => emit(state + 1); + void _notify() { + if (isClosed) return; + emit(state + 1); + } // ── locate ────────────────────────────────────────────────────────────── Future locate() async { _set(_NavPhase.locating, 'Mencari lokasi GPS…'); final located = await guarded( () async { - LocationPermission perm = await Geolocator.checkPermission(); - if (perm == LocationPermission.denied) { - perm = await Geolocator.requestPermission(); - } - if (perm == LocationPermission.deniedForever) { - _set(_NavPhase.error, 'Izin lokasi diblokir permanen.'); - return false; - } - final pos = await Geolocator.getCurrentPosition( - desiredAccuracy: LocationAccuracy.high, - ).timeout(const Duration(seconds: 12)); - currentPosition = LatLng(pos.latitude, pos.longitude); - _set(_NavPhase.idle, 'Lokasi ditemukan. Cari tujuan atau ketuk peta.'); - _reportToBackend(pos); - return true; + LocationPermission perm = await Geolocator.checkPermission(); + if (perm == LocationPermission.denied) { + perm = await Geolocator.requestPermission(); + } + if (perm == LocationPermission.deniedForever) { + _set(_NavPhase.error, 'Izin lokasi diblokir permanen.'); + return false; + } + final pos = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ).timeout(const Duration(seconds: 12)); + currentPosition = LatLng(pos.latitude, pos.longitude); + _set(_NavPhase.idle, 'Lokasi ditemukan. Cari tujuan atau ketuk peta.'); + _reportToBackend(pos); + return true; }, - onTimeout: () => _set(_NavPhase.error, 'GPS timeout. Pastikan GPS aktif.'), + onTimeout: () => + _set(_NavPhase.error, 'GPS timeout. Pastikan GPS aktif.'), onError: (_) => _set(_NavPhase.error, 'Lokasi belum bisa dibaca. Pastikan GPS dan izin lokasi aktif.'), ); @@ -113,33 +118,37 @@ class _NavState extends Cubit { Future> searchPlaces(String query) async { if (query.trim().isEmpty) return const []; return await guarded>( - () async { - final res = await Dio().get( - 'https://nominatim.openstreetmap.org/search', - queryParameters: { - 'q': query, - 'format': 'jsonv2', - 'limit': 6, - 'addressdetails': 0, - if (currentPosition != null) 'viewbox': _viewbox(currentPosition!), - if (currentPosition != null) 'bounded': 0, - }, - options: Options( - headers: {'User-Agent': 'WalkGuide/1.0 (walkguide@campus.ac.id)'}, - receiveTimeout: const Duration(seconds: 8), - ), - ); - final list = res.data as List; - return list.map((e) { - final lat = double.tryParse(e['lat'].toString()) ?? 0; - final lng = double.tryParse(e['lon'].toString()) ?? 0; - return _Place( - displayName: e['display_name'].toString(), - position: LatLng(lat, lng), - ); - }).toList(); - }, - ) ?? const []; + () async { + final res = await Dio().get( + 'https://nominatim.openstreetmap.org/search', + queryParameters: { + 'q': query, + 'format': 'jsonv2', + 'limit': 6, + 'addressdetails': 0, + if (currentPosition != null) + 'viewbox': _viewbox(currentPosition!), + if (currentPosition != null) 'bounded': 0, + }, + options: Options( + headers: { + 'User-Agent': 'WalkGuide/1.0 (walkguide@campus.ac.id)' + }, + receiveTimeout: const Duration(seconds: 8), + ), + ); + final list = res.data as List; + return list.map((e) { + final lat = double.tryParse(e['lat'].toString()) ?? 0; + final lng = double.tryParse(e['lon'].toString()) ?? 0; + return _Place( + displayName: e['display_name'].toString(), + position: LatLng(lat, lng), + ); + }).toList(); + }, + ) ?? + const []; } String _viewbox(LatLng c) => @@ -148,23 +157,25 @@ class _NavState extends Cubit { // ── reverse geocode ────────────────────────────────────────────────────── Future reverseGeocode(LatLng pos) async { return await guarded( - () async { - final res = await Dio().get( - 'https://nominatim.openstreetmap.org/reverse', - queryParameters: { - 'lat': pos.latitude, - 'lon': pos.longitude, - 'format': 'jsonv2', - }, - options: Options( - headers: {'User-Agent': 'WalkGuide/1.0 (walkguide@campus.ac.id)'}, - receiveTimeout: const Duration(seconds: 6), - ), - ); - return res.data['display_name']?.toString() ?? - '${pos.latitude.toStringAsFixed(5)}, ${pos.longitude.toStringAsFixed(5)}'; - }, - ) ?? + () async { + final res = await Dio().get( + 'https://nominatim.openstreetmap.org/reverse', + queryParameters: { + 'lat': pos.latitude, + 'lon': pos.longitude, + 'format': 'jsonv2', + }, + options: Options( + headers: { + 'User-Agent': 'WalkGuide/1.0 (walkguide@campus.ac.id)' + }, + receiveTimeout: const Duration(seconds: 6), + ), + ); + return res.data['display_name']?.toString() ?? + '${pos.latitude.toStringAsFixed(5)}, ${pos.longitude.toStringAsFixed(5)}'; + }, + ) ?? '${pos.latitude.toStringAsFixed(5)}, ${pos.longitude.toStringAsFixed(5)}'; } @@ -180,47 +191,47 @@ class _NavState extends Cubit { final origin = currentPosition!; await guarded( () async { - final url = 'http://router.project-osrm.org/route/v1/foot/' - '${origin.longitude},${origin.latitude};' - '${dest.position.longitude},${dest.position.latitude}' - '?steps=true&geometries=geojson&overview=full&annotations=false'; + final url = 'http://router.project-osrm.org/route/v1/foot/' + '${origin.longitude},${origin.latitude};' + '${dest.position.longitude},${dest.position.latitude}' + '?steps=true&geometries=geojson&overview=full&annotations=false'; - final res = await Dio().get( - url, - options: Options(receiveTimeout: const Duration(seconds: 12)), - ); + final res = await Dio().get( + url, + options: Options(receiveTimeout: const Duration(seconds: 12)), + ); - final route = res.data['routes'][0]; - final geom = route['geometry']['coordinates'] as List; - routePoints = geom.map((c) { - final lst = c as List; - return LatLng((lst[1] as num).toDouble(), (lst[0] as num).toDouble()); - }).toList(); + final route = res.data['routes'][0]; + final geom = route['geometry']['coordinates'] as List; + routePoints = geom.map((c) { + final lst = c as List; + return LatLng((lst[1] as num).toDouble(), (lst[0] as num).toDouble()); + }).toList(); - // parse steps - final legs = route['legs'] as List; - final rawSteps = <_Step>[]; - for (final leg in legs) { - for (final step in leg['steps'] as List) { - final maneuver = step['maneuver'] as Map; - final instruction = _buildInstruction(maneuver, step); - final dist = (step['distance'] as num?)?.toDouble() ?? 0; - final loc = maneuver['location'] as List; - rawSteps.add(_Step( - instruction: instruction, - distanceM: dist, - point: - LatLng((loc[1] as num).toDouble(), (loc[0] as num).toDouble()), - )); + // parse steps + final legs = route['legs'] as List; + final rawSteps = <_Step>[]; + for (final leg in legs) { + for (final step in leg['steps'] as List) { + final maneuver = step['maneuver'] as Map; + final instruction = _buildInstruction(maneuver, step); + final dist = (step['distance'] as num?)?.toDouble() ?? 0; + final loc = maneuver['location'] as List; + rawSteps.add(_Step( + instruction: instruction, + distanceM: dist, + point: LatLng( + (loc[1] as num).toDouble(), (loc[0] as num).toDouble()), + )); + } } - } - steps = rawSteps; - currentStepIndex = 0; + steps = rawSteps; + currentStepIndex = 0; - _set(_NavPhase.navigating, - steps.isNotEmpty ? steps[0].instruction : 'Ikuti rute biru.'); - _notify(); - _startTracking(); + _set(_NavPhase.navigating, + steps.isNotEmpty ? steps[0].instruction : 'Ikuti rute biru.'); + _notify(); + _startTracking(); }, onError: (_) => _set(_NavPhase.error, 'Gagal mendapat rute. Periksa koneksi lalu coba lagi.'), @@ -289,6 +300,7 @@ class _NavState extends Cubit { distanceFilter: 5, ), ).listen((pos) { + if (isClosed) return; currentPosition = LatLng(pos.latitude, pos.longitude); _reportToBackend(pos); _updateStep(); @@ -298,6 +310,7 @@ class _NavState extends Cubit { void _updateStep() { if (steps.isEmpty || phase != _NavPhase.navigating) return; + if (currentPosition == null) return; if (currentStepIndex >= steps.length - 1) return; final current = steps[currentStepIndex]; @@ -317,6 +330,7 @@ class _NavState extends Cubit { } void stopNavigation() { + if (isClosed) return; _posStream?.cancel(); _posStream = null; destination = null; diff --git a/walkguide-mobile/walkguide_app/lib/features/pairing/application/README.md b/walkguide-mobile/walkguide_app/lib/features/pairing/application/README.md new file mode 100644 index 0000000..8c056d2 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/pairing/application/README.md @@ -0,0 +1,3 @@ +Pairing application layer. + +This layer is reserved for pairing Cubit/BLoC orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/pairing/data/README.md b/walkguide-mobile/walkguide_app/lib/features/pairing/data/README.md new file mode 100644 index 0000000..7c1c937 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/pairing/data/README.md @@ -0,0 +1,3 @@ +Pairing data layer. + +This layer is reserved for `/shared/pairing/**` data sources and repository implementations. diff --git a/walkguide-mobile/walkguide_app/lib/features/pairing/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/pairing/domain/README.md new file mode 100644 index 0000000..b952f29 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/pairing/domain/README.md @@ -0,0 +1,3 @@ +Pairing domain layer. + +This layer is reserved for pairing entities, repository contracts, and use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/server_connect/application/README.md b/walkguide-mobile/walkguide_app/lib/features/server_connect/application/README.md new file mode 100644 index 0000000..629f4ef --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/server_connect/application/README.md @@ -0,0 +1,3 @@ +Server connect application layer. + +This layer is reserved for connection testing and save-server-url state orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/server_connect/data/README.md b/walkguide-mobile/walkguide_app/lib/features/server_connect/data/README.md new file mode 100644 index 0000000..0a2ee9d --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/server_connect/data/README.md @@ -0,0 +1,3 @@ +Server connect data layer. + +This layer is reserved for ping data sources and local server URL persistence adapters. diff --git a/walkguide-mobile/walkguide_app/lib/features/server_connect/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/server_connect/domain/README.md new file mode 100644 index 0000000..1dcf057 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/server_connect/domain/README.md @@ -0,0 +1,3 @@ +Server connect domain layer. + +This layer is reserved for server info entities and connection use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/server_connect/presentation/README.md b/walkguide-mobile/walkguide_app/lib/features/server_connect/presentation/README.md new file mode 100644 index 0000000..1d21c16 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/server_connect/presentation/README.md @@ -0,0 +1,3 @@ +Server connect presentation layer. + +This layer is reserved for first-run connection pages and widgets. diff --git a/walkguide-mobile/walkguide_app/lib/features/server_connect/server_connect_server.dart b/walkguide-mobile/walkguide_app/lib/features/server_connect/server_connect_server.dart index 7a72ed9..c4217a5 100644 --- a/walkguide-mobile/walkguide_app/lib/features/server_connect/server_connect_server.dart +++ b/walkguide-mobile/walkguide_app/lib/features/server_connect/server_connect_server.dart @@ -37,6 +37,7 @@ class _ServerConnectScreenState extends State { await runFriendlyAction( () async { final clean = AppConstants.normalizeServerUrl(_url.text); + await sl().init(clean); final res = await Dio(BaseOptions( connectTimeout: AppConstants.pingTimeout, receiveTimeout: AppConstants.pingTimeout, @@ -47,7 +48,8 @@ class _ServerConnectScreenState extends State { : 'Server merespons, tetapi format ping tidak valid.'; }, onError: (message) => _message = message, - fallback: 'Tidak bisa terhubung. Periksa URL dan jaringan.', + fallback: + 'Tidak bisa terhubung. Untuk HP via USB, jalankan adb reverse tcp:8080 tcp:8080 lalu pakai http://127.0.0.1:8080.', ); if (mounted) setState(() => _loading = false); } @@ -60,6 +62,8 @@ class _ServerConnectScreenState extends State { } void _useUsbUrl() => setState(() => _url.text = 'http://127.0.0.1:8080'); + void _useAndroidEmulatorUrl() => + setState(() => _url.text = 'http://10.0.2.2:8080'); @override Widget build(BuildContext context) { @@ -96,12 +100,23 @@ class _ServerConnectScreenState extends State { SafeArea( child: LayoutBuilder( builder: (context, constraints) { - final compact = constraints.maxWidth < 390; + final compact = + constraints.maxWidth < 480 || constraints.maxHeight < 720; + final horizontalPadding = + constraints.maxWidth < 480 ? 12.0 : 20.0; return SingleChildScrollView( - padding: EdgeInsets.fromLTRB(20, compact ? 20 : 36, 20, 24), + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.fromLTRB( + horizontalPadding, + compact ? 10 : 32, + horizontalPadding, + 20 + MediaQuery.of(context).viewInsets.bottom, + ), child: Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 460), + constraints: + BoxConstraints(maxWidth: compact ? 380 : 520), child: TweenAnimationBuilder( tween: Tween(begin: 18, end: 0), duration: const Duration(milliseconds: 520), @@ -114,7 +129,8 @@ class _ServerConnectScreenState extends State { child: Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.96), - borderRadius: BorderRadius.circular(28), + borderRadius: + BorderRadius.circular(compact ? 22 : 28), border: Border.all( color: Colors.white.withValues(alpha: 0.7)), boxShadow: [ @@ -126,13 +142,18 @@ class _ServerConnectScreenState extends State { ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(28), + borderRadius: + BorderRadius.circular(compact ? 22 : 28), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( - padding: - const EdgeInsets.fromLTRB(22, 22, 22, 20), + padding: EdgeInsets.fromLTRB( + compact ? 14 : 22, + compact ? 14 : 22, + compact ? 14 : 22, + compact ? 14 : 20, + ), decoration: const BoxDecoration( color: Color(0xFF071226), ), @@ -143,37 +164,38 @@ class _ServerConnectScreenState extends State { Row( children: [ Container( - width: 48, - height: 48, + width: compact ? 38 : 48, + height: compact ? 38 : 48, decoration: BoxDecoration( color: const Color(0xFF2563EB), borderRadius: BorderRadius.circular(16), ), - child: const Icon( - Icons.navigation_rounded, - color: Colors.white, - size: 28), + child: Icon( + Icons.navigation_rounded, + color: Colors.white, + size: compact ? 24 : 28, + ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - 'WalkGuide Link', + 'WalkGuide', style: TextStyle( color: Colors.white, - fontSize: 20, + fontSize: compact ? 16 : 20, fontWeight: FontWeight.w900, ), ), ), ], ), - const SizedBox(height: 18), - const Text( + SizedBox(height: compact ? 14 : 18), + Text( 'Connect to Server', style: TextStyle( color: Colors.white, - fontSize: 30, + fontSize: compact ? 22 : 30, fontWeight: FontWeight.w900, height: 1, ), @@ -191,7 +213,7 @@ class _ServerConnectScreenState extends State { ), ), Padding( - padding: const EdgeInsets.all(22), + padding: EdgeInsets.all(compact ? 14 : 22), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -217,13 +239,18 @@ class _ServerConnectScreenState extends State { label: 'USB: 127.0.0.1', onTap: _useUsbUrl, ), + _HintChip( + icon: Icons.phone_android_outlined, + label: 'Emulator: 10.0.2.2', + onTap: _useAndroidEmulatorUrl, + ), const _HintChip( icon: Icons.wifi_tethering_outlined, label: 'Wi-Fi: IP laptop', ), ], ), - const SizedBox(height: 16), + SizedBox(height: compact ? 12 : 16), OutlinedButton.icon( onPressed: _loading ? null : _test, icon: _loading @@ -251,15 +278,17 @@ class _ServerConnectScreenState extends State { label: const Text('Continue'), ), ], - const SizedBox(height: 18), - const Center( - child: Text( - 'v1.0.0 | Spring Boot + Flutter', - style: TextStyle( - fontSize: 11, - color: Color(0xFF94A3B8)), + if (!compact) ...[ + const SizedBox(height: 18), + const Center( + child: Text( + 'v1.0.0 | Spring Boot + Flutter', + style: TextStyle( + fontSize: 11, + color: Color(0xFF94A3B8)), + ), ), - ), + ], ], ), ), diff --git a/walkguide-mobile/walkguide_app/lib/features/settings/application/README.md b/walkguide-mobile/walkguide_app/lib/features/settings/application/README.md new file mode 100644 index 0000000..c6c6d26 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/settings/application/README.md @@ -0,0 +1,3 @@ +Settings application layer. + +This layer is reserved for settings Cubit/BLoC orchestration. diff --git a/walkguide-mobile/walkguide_app/lib/features/settings/data/README.md b/walkguide-mobile/walkguide_app/lib/features/settings/data/README.md new file mode 100644 index 0000000..413e5db --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/settings/data/README.md @@ -0,0 +1,3 @@ +Settings data layer. + +This layer is reserved for `/user/settings` and local settings data adapters. diff --git a/walkguide-mobile/walkguide_app/lib/features/settings/domain/README.md b/walkguide-mobile/walkguide_app/lib/features/settings/domain/README.md new file mode 100644 index 0000000..a04ce83 --- /dev/null +++ b/walkguide-mobile/walkguide_app/lib/features/settings/domain/README.md @@ -0,0 +1,3 @@ +Settings domain layer. + +This layer is reserved for settings entities, repository contracts, and update use cases. diff --git a/walkguide-mobile/walkguide_app/lib/features/settings/user_settings_screen.dart b/walkguide-mobile/walkguide_app/lib/features/settings/user_settings_screen.dart index 91f011b..0cfdc49 100644 --- a/walkguide-mobile/walkguide_app/lib/features/settings/user_settings_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/settings/user_settings_screen.dart @@ -125,6 +125,7 @@ class _UserSettingsScreenState extends State { setState(() => _saving = true); // Apply TTS locally dulu await sl().setLanguage(_ttsLanguage); + context.read().setLocaleCode(_ttsLanguage); if (_hapticEnabled) { await sl().success(); } diff --git a/walkguide-mobile/walkguide_app/lib/features/sos/sos_screen.dart b/walkguide-mobile/walkguide_app/lib/features/sos/sos_screen.dart index 9b5fe7f..f7648cc 100644 --- a/walkguide-mobile/walkguide_app/lib/features/sos/sos_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/sos/sos_screen.dart @@ -243,9 +243,18 @@ class _SosScreenState extends State bloc: _sosCubit, builder: (context, sosState) { final sending = sosState.phase == SosPhase.sending; + final size = MediaQuery.sizeOf(context); + final compact = size.height < 620; + final landscapeTight = size.width > size.height && size.height < 520; + final pagePadding = compact ? 12.0 : 16.0; + final sectionGap = landscapeTight + ? 8.0 + : compact + ? 12.0 + : 24.0; return SafeArea( child: Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(pagePadding), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -278,14 +287,14 @@ class _SosScreenState extends State ], ), - const SizedBox(height: 24), + SizedBox(height: sectionGap), // Active SOS banner if (_hasActiveSos) _ActiveSosBanner( event: _events.first, onRefresh: _loadHistory), - const SizedBox(height: 24), + SizedBox(height: sectionGap), // SOS Button Center( @@ -312,6 +321,8 @@ class _SosScreenState extends State ? 'SOS aktif — Guardian sudah mendapat notifikasi' : 'Tekan tombol untuk kirim SOS darurat ke Guardian', textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: TextStyle( color: _hasActiveSos ? const Color(0xFFDC2626) @@ -321,22 +332,25 @@ class _SosScreenState extends State ), ), - const SizedBox(height: 28), + SizedBox(height: sectionGap), // History section - const Text( - 'Riwayat SOS', - style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16), - ), - const SizedBox(height: 10), - - Expanded( + if (!landscapeTight) ...[ + const Text( + 'Riwayat SOS', + style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + ), + const SizedBox(height: 10), + Expanded( child: _SosHistory( - loading: _historyLoading, - error: _historyError, - events: _events, - onRefresh: _loadHistory, - )), + loading: _historyLoading, + error: _historyError, + events: _events, + onRefresh: _loadHistory, + ), + ), + ] else + const Spacer(), ], ), ), @@ -361,8 +375,16 @@ class _SosButton extends StatelessWidget { @override Widget build(BuildContext context) { + final screen = MediaQuery.sizeOf(context); + final compact = screen.height < 620; + final landscapeTight = screen.width > screen.height && screen.height < 520; + final dimension = landscapeTight + ? 132.0 + : compact + ? 154.0 + : 200.0; return SizedBox.square( - dimension: 200, + dimension: dimension, child: FilledButton( style: FilledButton.styleFrom( shape: const CircleBorder(), @@ -377,14 +399,14 @@ class _SosButton extends StatelessWidget { children: [ Icon( active ? Icons.emergency : Icons.emergency_outlined, - size: 48, + size: dimension < 150 ? 34 : 48, color: Colors.white, ), - const SizedBox(height: 6), + SizedBox(height: dimension < 150 ? 3 : 6), Text( 'SOS', - style: const TextStyle( - fontSize: 38, + style: TextStyle( + fontSize: dimension < 150 ? 28 : 38, fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 2, @@ -402,8 +424,16 @@ class _SendingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { + final screen = MediaQuery.sizeOf(context); + final compact = screen.height < 620; + final landscapeTight = screen.width > screen.height && screen.height < 520; + final dimension = landscapeTight + ? 132.0 + : compact + ? 154.0 + : 200.0; return SizedBox.square( - dimension: 200, + dimension: dimension, child: DecoratedBox( decoration: BoxDecoration( color: const Color(0xFFDC2626).withValues(alpha: 0.15), diff --git a/walkguide-mobile/walkguide_app/lib/features/walk_guide/walk_guide_screen.dart b/walkguide-mobile/walkguide_app/lib/features/walk_guide/walk_guide_screen.dart index 70aea8d..e4c040c 100644 --- a/walkguide-mobile/walkguide_app/lib/features/walk_guide/walk_guide_screen.dart +++ b/walkguide-mobile/walkguide_app/lib/features/walk_guide/walk_guide_screen.dart @@ -144,6 +144,7 @@ class _WalkGuideScreenState extends State () async { final cameras = await availableCameras(); if (cameras.isEmpty) return; + await sl().init(); final backCamera = cameras.firstWhere( (camera) => camera.lensDirection == CameraLensDirection.back, orElse: () => cameras.first, @@ -808,6 +809,7 @@ class _Page extends StatelessWidget { @override Widget build(BuildContext context) { + final compact = MediaQuery.sizeOf(context).height < 520; return SafeArea( child: DecoratedBox( decoration: const BoxDecoration( @@ -818,15 +820,20 @@ class _Page extends StatelessWidget { ), ), child: Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), + padding: EdgeInsets.fromLTRB( + compact ? 12 : 16, + compact ? 8 : 14, + compact ? 12 : 16, + compact ? 10 : 16, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( - width: 46, - height: 46, + width: compact ? 38 : 46, + height: compact ? 38 : 46, decoration: BoxDecoration( color: const Color(0xFF2563EB), borderRadius: BorderRadius.circular(14), @@ -839,21 +846,24 @@ class _Page extends StatelessWidget { ), ], ), - child: const Icon(Icons.navigation_rounded, - color: Colors.white, size: 26), + child: Icon(Icons.navigation_rounded, + color: Colors.white, size: compact ? 22 : 26), ), - const SizedBox(width: 12), + SizedBox(width: compact ? 10 : 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .headlineSmall ?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF0F172A), + fontSize: compact ? 22 : null, )), if (subtitle != null) Text(subtitle!, @@ -866,7 +876,7 @@ class _Page extends StatelessWidget { ...?actions, ], ), - const SizedBox(height: 16), + SizedBox(height: compact ? 8 : 16), Expanded(child: child), ], ), diff --git a/walkguide-mobile/walkguide_app/lib/main.dart b/walkguide-mobile/walkguide_app/lib/main.dart index 6703c4c..7e6f5ae 100644 --- a/walkguide-mobile/walkguide_app/lib/main.dart +++ b/walkguide-mobile/walkguide_app/lib/main.dart @@ -1,36 +1,207 @@ +import 'dart:async'; +import 'dart:ui'; + import 'package:flutter/material.dart'; -import 'package:camera/camera.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'app/injection_container.dart'; import 'app/app.dart'; +import 'core/constants/app_constants.dart'; import 'core/utils/init_guard.dart'; -List cameras = []; - @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); } Future main() async { - WidgetsFlutterBinding.ensureInitialized(); + await runZonedGuarded( + () async { + WidgetsFlutterBinding.ensureInitialized(); + _installGlobalErrorUi(); + await AppConstants.clearServerUrl(); - cameras = await ignoreInitFailure( - availableCameras, - label: 'Camera init', - ) ?? - []; + if (!kIsWeb) { + final firebaseApp = await ignoreInitFailure( + () => Firebase.initializeApp(), + label: 'Firebase init', + ); + if (firebaseApp != null) { + FirebaseMessaging.onBackgroundMessage( + _firebaseMessagingBackgroundHandler, + ); + } + } - if (!kIsWeb) { - await ignoreInitFailure(() => Firebase.initializeApp(), - label: 'Firebase init'); - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + try { + await initDependencies(); + } catch (error, stackTrace) { + debugPrint('WalkGuide startup failed: $error\n$stackTrace'); + runApp(WalkGuideFatalApp(error: error, stackTrace: stackTrace)); + return; + } + + runApp(const WalkGuideApp()); + }, + (error, stackTrace) { + debugPrint('WalkGuide uncaught error: $error\n$stackTrace'); + runApp(WalkGuideFatalApp(error: error, stackTrace: stackTrace)); + }, + ); +} + +void _installGlobalErrorUi() { + FlutterError.onError = (details) { + FlutterError.presentError(details); + debugPrint('WalkGuide Flutter error: ${details.exceptionAsString()}'); + }; + + PlatformDispatcher.instance.onError = (error, stackTrace) { + debugPrint('WalkGuide platform error: $error\n$stackTrace'); + return true; + }; + + ErrorWidget.builder = (details) { + return WalkGuideErrorPanel( + title: 'WalkGuide UI Error', + message: + 'A screen failed to render. Please report this message to the developer.', + details: details.exceptionAsString(), + ); + }; +} + +class WalkGuideFatalApp extends StatelessWidget { + final Object error; + final StackTrace? stackTrace; + + const WalkGuideFatalApp({ + super.key, + required this.error, + this.stackTrace, + }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: WalkGuideErrorPanel( + title: 'WalkGuide Startup Error', + message: + 'The app could not finish startup. Please report this screen to the developer.', + details: error.toString(), + stackTrace: stackTrace?.toString(), + ), + ); + } +} + +class WalkGuideErrorPanel extends StatelessWidget { + final String title; + final String message; + final String details; + final String? stackTrace; + + const WalkGuideErrorPanel({ + super.key, + required this.title, + required this.message, + required this.details, + this.stackTrace, + }); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + body: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: const [ + BoxShadow( + blurRadius: 24, + offset: Offset(0, 12), + color: Color(0x1A0F172A), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(22), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.error_outline, + color: Color(0xFFDC2626), + size: 42, + ), + const SizedBox(height: 16), + Text( + title, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + color: const Color(0xFF0F172A), + ), + ), + const SizedBox(height: 8), + Text( + message, + style: textTheme.bodyMedium?.copyWith( + color: const Color(0xFF475569), + ), + ), + const SizedBox(height: 18), + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: SelectableText( + _formatDetails(), + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + color: Color(0xFF334155), + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Tip: close the app and open it again after fixing the configuration.', + style: TextStyle( + color: Color(0xFF64748B), + fontSize: 12, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); } - // Init GetIt dependencies - await initDependencies(); - - runApp(const WalkGuideApp()); + String _formatDetails() { + final stack = stackTrace; + if (stack == null || stack.isEmpty) return details; + final shortStack = stack.split('\n').take(8).join('\n'); + return '$details\n\n$shortStack'; + } } diff --git a/walkguide-mobile/walkguide_app/lib/shared/widgets/app_shells.dart b/walkguide-mobile/walkguide_app/lib/shared/widgets/app_shells.dart index 8948efc..6b0d64f 100644 --- a/walkguide-mobile/walkguide_app/lib/shared/widgets/app_shells.dart +++ b/walkguide-mobile/walkguide_app/lib/shared/widgets/app_shells.dart @@ -26,7 +26,9 @@ class _UserShellState extends State { super.initState(); _loadVoiceCommands(); _startHardwareShortcuts(); - sl().startListening(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _startVoiceListening(); + }); sl().onCommand = (key) { if (!mounted) return; switch (key) { @@ -64,6 +66,17 @@ class _UserShellState extends State { }; } + Future _startVoiceListening() async { + await runFriendlyAction( + () async { + await sl().init(); + await sl().startListening(); + }, + onError: (_) {}, + fallback: 'Voice listener belum bisa dimuat.', + ); + } + Future _loadVoiceCommands() async { await runFriendlyAction( () async { @@ -182,42 +195,41 @@ class _AppShell extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.surface, - body: AnimatedSwitcher( - duration: const Duration(milliseconds: 180), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: KeyedSubtree( - key: ValueKey(location), - child: child, - ), - ), - bottomNavigationBar: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white, - border: const Border(top: BorderSide(color: AppColors.border)), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.06), - blurRadius: 18, - offset: const Offset(0, -8), - ), - ], - ), - child: NavigationBar( - selectedIndex: _selectedIndex, - onDestinationSelected: (index) => context.go(items[index].route), - destinations: [ - for (final item in items) - NavigationDestination( - icon: Icon(item.icon), - selectedIcon: Icon(item.selectedIcon), - label: item.label, - ), - ], - ), - ), + return LayoutBuilder( + builder: (context, constraints) { + final useRail = constraints.maxWidth >= 760; + final content = AnimatedSwitcher( + duration: const Duration(milliseconds: 180), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + child: KeyedSubtree( + key: ValueKey(location), + child: child, + ), + ); + + return Scaffold( + backgroundColor: AppColors.surface, + body: useRail + ? Row( + children: [ + _RailNavigation( + items: items, + selectedIndex: _selectedIndex, + ), + const VerticalDivider(width: 1, color: AppColors.border), + Expanded(child: content), + ], + ) + : content, + bottomNavigationBar: useRail + ? null + : _BottomScrollNavigation( + items: items, + selectedIndex: _selectedIndex, + ), + ); + }, ); } @@ -227,6 +239,194 @@ class _AppShell extends StatelessWidget { } } +class _RailNavigation extends StatelessWidget { + final List<_ShellItem> items; + final int selectedIndex; + + const _RailNavigation({ + required this.items, + required this.selectedIndex, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final compact = constraints.maxHeight < 520; + final width = compact ? 76.0 : 86.0; + final itemHeight = compact ? 58.0 : 70.0; + + return DecoratedBox( + decoration: const BoxDecoration(color: Colors.white), + child: SafeArea( + right: false, + child: SizedBox( + width: width, + child: ListView.separated( + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: compact ? 6 : 12, + ), + itemCount: items.length, + separatorBuilder: (_, __) => SizedBox(height: compact ? 2 : 6), + itemBuilder: (context, index) { + final item = items[index]; + final selected = index == selectedIndex; + return Semantics( + button: true, + selected: selected, + label: item.label, + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () => context.go(items[index].route), + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + height: itemHeight, + padding: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: selected + ? const Color(0xFFEFF6FF) + : Colors.transparent, + borderRadius: BorderRadius.circular(18), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + selected ? item.selectedIcon : item.icon, + size: compact ? 23 : 25, + color: selected + ? AppColors.primary + : const Color(0xFF334155), + ), + SizedBox(height: compact ? 2 : 5), + Text( + item.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: compact ? 10 : 12, + height: 1, + fontWeight: selected + ? FontWeight.w800 + : FontWeight.w600, + color: selected + ? const Color(0xFF1D4ED8) + : const Color(0xFF334155), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ), + ); + }, + ); + } +} + +class _BottomScrollNavigation extends StatelessWidget { + final List<_ShellItem> items; + final int selectedIndex; + + const _BottomScrollNavigation({ + required this.items, + required this.selectedIndex, + }); + + @override + Widget build(BuildContext context) { + final bottom = MediaQuery.of(context).padding.bottom; + final extraBottom = bottom > 12 ? 12.0 : bottom; + return DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + border: const Border(top: BorderSide(color: AppColors.border)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 18, + offset: const Offset(0, -8), + ), + ], + ), + child: SafeArea( + top: false, + child: SizedBox( + height: 68 + extraBottom, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + itemCount: items.length, + separatorBuilder: (_, __) => const SizedBox(width: 6), + itemBuilder: (context, index) { + final item = items[index]; + final selected = index == selectedIndex; + return Semantics( + button: true, + selected: selected, + label: item.label, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => context.go(item.route), + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), + width: 72, + padding: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + color: selected + ? const Color(0xFFEFF6FF) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selected + ? const Color(0xFFBFDBFE) + : Colors.transparent, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + selected ? item.selectedIcon : item.icon, + color: selected + ? AppColors.primary + : const Color(0xFF64748B), + size: 22, + ), + const SizedBox(height: 4), + Text( + item.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + fontWeight: + selected ? FontWeight.w800 : FontWeight.w600, + color: selected + ? AppColors.primary + : const Color(0xFF64748B), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} + class _ShellItem { final String label; final IconData icon; diff --git a/walkguide-mobile/walkguide_app/lib/shared/widgets/feature_page.dart b/walkguide-mobile/walkguide_app/lib/shared/widgets/feature_page.dart index 26731fb..477b9fc 100644 --- a/walkguide-mobile/walkguide_app/lib/shared/widgets/feature_page.dart +++ b/walkguide-mobile/walkguide_app/lib/shared/widgets/feature_page.dart @@ -19,62 +19,127 @@ class FeaturePage extends StatelessWidget { @override Widget build(BuildContext context) { return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TweenAnimationBuilder( - tween: Tween(begin: 12, end: 0), - duration: const Duration(milliseconds: 360), - curve: Curves.easeOutCubic, - builder: (_, offset, child) => Opacity( - opacity: (1 - offset / 12).clamp(0.0, 1.0), - child: Transform.translate( - offset: Offset(0, offset), - child: child, + child: LayoutBuilder( + builder: (context, constraints) { + final short = constraints.maxHeight < 520; + final compact = constraints.maxWidth < 420 || short; + final wide = constraints.maxWidth >= 900; + final horizontal = compact ? 12.0 : 20.0; + return Padding( + padding: EdgeInsets.fromLTRB( + horizontal, + short ? 8 : 12, + horizontal, + short ? 10 : 14, + ), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: wide ? 1160 : double.infinity, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 12, end: 0), + duration: const Duration(milliseconds: 360), + curve: Curves.easeOutCubic, + builder: (_, offset, child) => Opacity( + opacity: (1 - offset / 12).clamp(0.0, 1.0), + child: Transform.translate( + offset: Offset(0, offset), + child: child, + ), + ), + child: compact + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _FeatureHeading( + title: title, + subtitle: subtitle, + compact: compact, + ), + if (trailing != null) ...[ + const SizedBox(height: 10), + Align( + alignment: Alignment.centerLeft, + child: trailing!, + ), + ], + ], + ) + : Row( + children: [ + Expanded( + child: _FeatureHeading( + title: title, + subtitle: subtitle, + compact: compact, + ), + ), + if (trailing != null) trailing!, + ], + ), + ), + SizedBox(height: short ? 8 : (compact ? 12 : 16)), + Expanded( + child: child, + ), + ], ), ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - fontWeight: FontWeight.w900, - color: AppColors.text, - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: const TextStyle( - color: AppColors.muted, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - if (trailing != null) trailing!, - ], - ), ), - const SizedBox(height: 16), - Expanded(child: child), - ], - ), + ); + }, ), ); } } +class _FeatureHeading extends StatelessWidget { + final String title; + final String subtitle; + final bool compact; + + const _FeatureHeading({ + required this.title, + required this.subtitle, + required this.compact, + }); + + @override + Widget build(BuildContext context) { + final short = MediaQuery.sizeOf(context).height < 520; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: short ? 1 : 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontSize: compact ? 22 : null, + fontWeight: FontWeight.w900, + color: AppColors.text, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + maxLines: short ? 1 : (compact ? 2 : 3), + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.muted, + fontWeight: FontWeight.w500, + height: 1.25, + ), + ), + ], + ); + } +} + class FeatureEmptyPanel extends StatelessWidget { final IconData icon; final String title; diff --git a/walkguide-mobile/walkguide_app/macos/Flutter/GeneratedPluginRegistrant.swift b/walkguide-mobile/walkguide_app/macos/Flutter/GeneratedPluginRegistrant.swift index 6017910..c64d224 100644 --- a/walkguide-mobile/walkguide_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/walkguide-mobile/walkguide_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import agora_rtc_engine import audio_session +import battery_plus import connectivity_plus import device_info_plus import firebase_core @@ -27,6 +28,7 @@ import sqlite3_flutter_libs func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AgoraRtcNgPlugin.register(with: registry.registrar(forPlugin: "AgoraRtcNgPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) diff --git a/walkguide-mobile/walkguide_app/pubspec.lock b/walkguide-mobile/walkguide_app/pubspec.lock index 42a445f..aa0bafa 100644 --- a/walkguide-mobile/walkguide_app/pubspec.lock +++ b/walkguide-mobile/walkguide_app/pubspec.lock @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.25" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: "03d5a6bb36db9d2b977c548f6b0262d5a84c4d5a4cfee2edac4a91d57011b365" + url: "https://pub.dev" + source: hosted + version: "6.2.3" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 + url: "https://pub.dev" + source: hosted + version: "2.0.1" bloc: dependency: transitive description: @@ -571,6 +587,11 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_map: dependency: "direct main" description: @@ -1652,6 +1673,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.dev" + source: hosted + version: "0.7.0" uuid: dependency: transitive description: diff --git a/walkguide-mobile/walkguide_app/pubspec.yaml b/walkguide-mobile/walkguide_app/pubspec.yaml index 2ab6e23..c232f25 100644 --- a/walkguide-mobile/walkguide_app/pubspec.yaml +++ b/walkguide-mobile/walkguide_app/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # State management flutter_bloc: ^8.1.6 @@ -50,6 +52,7 @@ dependencies: # Location geolocator: ^12.0.0 + battery_plus: ^6.2.3 # Agora VoIP agora_rtc_engine: ^6.3.2 diff --git a/walkguide-mobile/walkguide_app/windows/flutter/generated_plugin_registrant.cc b/walkguide-mobile/walkguide_app/windows/flutter/generated_plugin_registrant.cc index c75836c..6265406 100644 --- a/walkguide-mobile/walkguide_app/windows/flutter/generated_plugin_registrant.cc +++ b/walkguide-mobile/walkguide_app/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AgoraRtcEnginePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AgoraRtcEnginePlugin")); + BatteryPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FirebaseCorePluginCApiRegisterWithRegistrar( diff --git a/walkguide-mobile/walkguide_app/windows/flutter/generated_plugins.cmake b/walkguide-mobile/walkguide_app/windows/flutter/generated_plugins.cmake index 4664fac..af3098f 100644 --- a/walkguide-mobile/walkguide_app/windows/flutter/generated_plugins.cmake +++ b/walkguide-mobile/walkguide_app/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST agora_rtc_engine + battery_plus connectivity_plus firebase_core flutter_secure_storage_windows