From 217c17dfef3b6c6d6889dfd153704c796897b6a1 Mon Sep 17 00:00:00 2001 From: toki Date: Mon, 25 May 2026 22:10:43 +0900 Subject: [PATCH] refactor: rename apps/mobile to apps/client and update project structure - Rename mobile app directory to client throughout the project - Restructure client app code into feature-based organization - Move code to src/features/workspaces/domain/presentation - Move services to src/integrations/ (mattermost, workspace, proto_socket) - Add app bootstrap and main entry point - Add push notification integration (Mattermost push client/host/plugin) - Add proto socket integration for real-time communication - Add integration tests for push and socket components - Archive old milestone: client-integration-standardization.md - Add new workflow core milestone: roadmap-driven-agent-ops-automation.md - Update agent-ops rules (project, core, mobile, contracts, workspace-ops) - Update roadmap files (ROADMAP.md, current.md, phase PHASE.md files) - Update bin scripts (build, dev, lint, test) - Add test environments documentation - Update contracts notes for Flutter Core API --- README.md | 10 +- agent-ops/roadmap/ROADMAP.md | 8 +- .../client-integration-standardization.md | 66 ++++--- agent-ops/roadmap/current.md | 9 +- .../project-workspace-management-ux/PHASE.md | 9 +- .../project-workspace-management-ux.md | 4 +- .../roadmap/phase/workflow-core/PHASE.md | 8 +- .../roadmap-driven-agent-ops-automation.md | 86 +++++++++ .../rules/project/domain/contracts/rules.md | 2 +- agent-ops/rules/project/domain/core/rules.md | 2 +- .../rules/project/domain/mobile/rules.md | 12 +- .../project/domain/workspace-ops/rules.md | 2 +- agent-ops/rules/project/rules.md | 6 +- apps/{mobile => client}/.gitignore | 0 .../.idea/libraries/Dart_SDK.xml | 0 .../.idea/libraries/KotlinJavaRuntime.xml | 0 apps/{mobile => client}/.idea/modules.xml | 0 .../.idea/runConfigurations/main_dart.xml | 0 apps/{mobile => client}/.idea/workspace.xml | 0 apps/{mobile => client}/.metadata | 0 apps/{mobile => client}/.vscode/launch.json | 0 apps/client/README.md | 102 +++++++++++ apps/{mobile => client}/analysis_options.yaml | 0 apps/{mobile => client}/android/.gitignore | 0 .../android/app/build.gradle.kts | 0 .../android/app/google-services.json | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../com/tokilabs/mattermost/MainActivity.kt | 0 .../res/drawable-v21/launch_background.xml | 0 .../main/res/drawable/launch_background.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../mipmap-hdpi/ic_launcher_background.png | Bin .../mipmap-hdpi/ic_launcher_foreground.png | Bin .../res/mipmap-hdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../mipmap-mdpi/ic_launcher_background.png | Bin .../mipmap-mdpi/ic_launcher_foreground.png | Bin .../res/mipmap-mdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../mipmap-xhdpi/ic_launcher_background.png | Bin .../mipmap-xhdpi/ic_launcher_foreground.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../mipmap-xxhdpi/ic_launcher_background.png | Bin .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../mipmap-xxxhdpi/ic_launcher_background.png | Bin .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../app/src/main/res/values-night/styles.xml | 0 .../app/src/main/res/values/styles.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 .../android/build.gradle.kts | 0 .../android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../android/nomadcode_app_android.iml | 0 .../android/settings.gradle.kts | 0 apps/{mobile => client}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../ios/Flutter/Debug.xcconfig | 0 .../ios/Flutter/Release.xcconfig | 0 apps/{mobile => client}/ios/Podfile | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 apps/{mobile => client}/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 .../ios/Runner/SceneDelegate.swift | 0 .../ios/RunnerTests/RunnerTests.swift | 0 apps/client/lib/main.dart | 3 + apps/client/lib/src/app/bootstrap.dart | 54 ++++++ .../lib/src/app/nomadcode_client_app.dart | 98 ++++++++++ .../workspaces/domain}/project_workspace.dart | 0 .../presentation}/workspace_home_page.dart | 2 +- .../mattermost}/mattermost_auth_service.dart | 5 +- .../mattermost/mattermost_push_client.dart | 21 +++ .../mattermost_push_host_integration.dart | 70 ++++++++ .../mattermost_push_plugin_client.dart | 51 ++++++ .../proto_socket_endpoint_config.dart | 84 +++++++++ .../proto_socket/proto_socket_lifecycle.dart | 80 +++++++++ .../workspace}/workspace_launcher.dart | 2 +- apps/{mobile => client}/linux/.gitignore | 0 apps/{mobile => client}/linux/CMakeLists.txt | 0 .../linux/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../linux/flutter/generated_plugins.cmake | 0 .../linux/runner/CMakeLists.txt | 0 apps/{mobile => client}/linux/runner/main.cc | 0 .../linux/runner/my_application.cc | 0 .../linux/runner/my_application.h | 0 apps/{mobile => client}/macos/.gitignore | 0 .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../Flutter/GeneratedPluginRegistrant.swift | 0 apps/{mobile => client}/macos/Podfile | 0 .../macos/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../macos/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../macos/Runner/Release.entitlements | 0 .../macos/RunnerTests/RunnerTests.swift | 0 apps/{mobile => client}/nomadcode_app.iml | 0 apps/{mobile => client}/pubspec.lock | 23 +++ apps/{mobile => client}/pubspec.yaml | 4 + apps/client/push-notification-todo.md | 82 +++++++++ ...mattermost_push_host_integration_test.dart | 170 ++++++++++++++++++ .../proto_socket_endpoint_config_test.dart | 80 +++++++++ .../proto_socket_lifecycle_test.dart | 89 +++++++++ apps/{mobile => client}/test/widget_test.dart | 8 +- apps/{mobile => client}/web/favicon.png | Bin .../{mobile => client}/web/icons/Icon-192.png | Bin .../{mobile => client}/web/icons/Icon-512.png | Bin .../web/icons/Icon-maskable-192.png | Bin .../web/icons/Icon-maskable-512.png | Bin apps/{mobile => client}/web/index.html | 0 apps/{mobile => client}/web/manifest.json | 0 apps/{mobile => client}/windows/.gitignore | 0 .../{mobile => client}/windows/CMakeLists.txt | 0 .../windows/flutter/CMakeLists.txt | 0 .../flutter/generated_plugin_registrant.cc | 0 .../flutter/generated_plugin_registrant.h | 0 .../windows/flutter/generated_plugins.cmake | 0 .../windows/runner/CMakeLists.txt | 0 .../windows/runner/Runner.rc | 0 .../windows/runner/flutter_window.cpp | 0 .../windows/runner/flutter_window.h | 0 .../windows/runner/main.cpp | 0 .../windows/runner/resource.h | 0 .../windows/runner/resources/app_icon.ico | Bin .../windows/runner/runner.exe.manifest | 0 .../windows/runner/utils.cpp | 0 .../{mobile => client}/windows/runner/utils.h | 0 .../windows/runner/win32_window.cpp | 0 .../windows/runner/win32_window.h | 0 apps/mobile/README.md | 39 ---- apps/mobile/lib/main.dart | 125 ------------- apps/mobile/push-notification-todo.md | 34 ---- bin/build | 6 +- bin/dev | 2 +- bin/lint | 6 +- bin/test | 6 +- docs/monorepo.md | 3 +- docs/test-environments.md | 70 ++++++++ .../notes/flutter-core-api-candidates.md | 32 +++- 196 files changed, 1295 insertions(+), 280 deletions(-) rename agent-ops/roadmap/{ => archive}/phase/project-workspace-management-ux/milestones/client-integration-standardization.md (51%) create mode 100644 agent-ops/roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md rename apps/{mobile => client}/.gitignore (100%) rename apps/{mobile => client}/.idea/libraries/Dart_SDK.xml (100%) rename apps/{mobile => client}/.idea/libraries/KotlinJavaRuntime.xml (100%) rename apps/{mobile => client}/.idea/modules.xml (100%) rename apps/{mobile => client}/.idea/runConfigurations/main_dart.xml (100%) rename apps/{mobile => client}/.idea/workspace.xml (100%) rename apps/{mobile => client}/.metadata (100%) rename apps/{mobile => client}/.vscode/launch.json (100%) create mode 100644 apps/client/README.md rename apps/{mobile => client}/analysis_options.yaml (100%) rename apps/{mobile => client}/android/.gitignore (100%) rename apps/{mobile => client}/android/app/build.gradle.kts (100%) rename apps/{mobile => client}/android/app/google-services.json (100%) rename apps/{mobile => client}/android/app/src/debug/AndroidManifest.xml (100%) rename apps/{mobile => client}/android/app/src/main/AndroidManifest.xml (100%) rename apps/{mobile => client}/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt (100%) rename apps/{mobile => client}/android/app/src/main/res/drawable-v21/launch_background.xml (100%) rename apps/{mobile => client}/android/app/src/main/res/drawable/launch_background.xml (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png (100%) rename apps/{mobile => client}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename apps/{mobile => client}/android/app/src/main/res/values-night/styles.xml (100%) rename apps/{mobile => client}/android/app/src/main/res/values/styles.xml (100%) rename apps/{mobile => client}/android/app/src/profile/AndroidManifest.xml (100%) rename apps/{mobile => client}/android/build.gradle.kts (100%) rename apps/{mobile => client}/android/gradle.properties (100%) rename apps/{mobile => client}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename apps/{mobile => client}/android/nomadcode_app_android.iml (100%) rename apps/{mobile => client}/android/settings.gradle.kts (100%) rename apps/{mobile => client}/ios/.gitignore (100%) rename apps/{mobile => client}/ios/Flutter/AppFrameworkInfo.plist (100%) rename apps/{mobile => client}/ios/Flutter/Debug.xcconfig (100%) rename apps/{mobile => client}/ios/Flutter/Release.xcconfig (100%) rename apps/{mobile => client}/ios/Podfile (100%) rename apps/{mobile => client}/ios/Runner.xcodeproj/project.pbxproj (100%) rename apps/{mobile => client}/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename apps/{mobile => client}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename apps/{mobile => client}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename apps/{mobile => client}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename apps/{mobile => client}/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) rename apps/{mobile => client}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename apps/{mobile => client}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename apps/{mobile => client}/ios/Runner/AppDelegate.swift (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename apps/{mobile => client}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename apps/{mobile => client}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename apps/{mobile => client}/ios/Runner/Base.lproj/Main.storyboard (100%) rename apps/{mobile => client}/ios/Runner/Info.plist (100%) rename apps/{mobile => client}/ios/Runner/Runner-Bridging-Header.h (100%) rename apps/{mobile => client}/ios/Runner/SceneDelegate.swift (100%) rename apps/{mobile => client}/ios/RunnerTests/RunnerTests.swift (100%) create mode 100644 apps/client/lib/main.dart create mode 100644 apps/client/lib/src/app/bootstrap.dart create mode 100644 apps/client/lib/src/app/nomadcode_client_app.dart rename apps/{mobile/lib/models => client/lib/src/features/workspaces/domain}/project_workspace.dart (100%) rename apps/{mobile/lib/screens => client/lib/src/features/workspaces/presentation}/workspace_home_page.dart (99%) rename apps/{mobile/lib/services => client/lib/src/integrations/mattermost}/mattermost_auth_service.dart (97%) create mode 100644 apps/client/lib/src/integrations/mattermost/mattermost_push_client.dart create mode 100644 apps/client/lib/src/integrations/mattermost/mattermost_push_host_integration.dart create mode 100644 apps/client/lib/src/integrations/mattermost/mattermost_push_plugin_client.dart create mode 100644 apps/client/lib/src/integrations/proto_socket/proto_socket_endpoint_config.dart create mode 100644 apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart rename apps/{mobile/lib/services => client/lib/src/integrations/workspace}/workspace_launcher.dart (82%) rename apps/{mobile => client}/linux/.gitignore (100%) rename apps/{mobile => client}/linux/CMakeLists.txt (100%) rename apps/{mobile => client}/linux/flutter/CMakeLists.txt (100%) rename apps/{mobile => client}/linux/flutter/generated_plugin_registrant.cc (100%) rename apps/{mobile => client}/linux/flutter/generated_plugin_registrant.h (100%) rename apps/{mobile => client}/linux/flutter/generated_plugins.cmake (100%) rename apps/{mobile => client}/linux/runner/CMakeLists.txt (100%) rename apps/{mobile => client}/linux/runner/main.cc (100%) rename apps/{mobile => client}/linux/runner/my_application.cc (100%) rename apps/{mobile => client}/linux/runner/my_application.h (100%) rename apps/{mobile => client}/macos/.gitignore (100%) rename apps/{mobile => client}/macos/Flutter/Flutter-Debug.xcconfig (100%) rename apps/{mobile => client}/macos/Flutter/Flutter-Release.xcconfig (100%) rename apps/{mobile => client}/macos/Flutter/GeneratedPluginRegistrant.swift (100%) rename apps/{mobile => client}/macos/Podfile (100%) rename apps/{mobile => client}/macos/Runner.xcodeproj/project.pbxproj (100%) rename apps/{mobile => client}/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename apps/{mobile => client}/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename apps/{mobile => client}/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename apps/{mobile => client}/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename apps/{mobile => client}/macos/Runner/AppDelegate.swift (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename apps/{mobile => client}/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename apps/{mobile => client}/macos/Runner/Base.lproj/MainMenu.xib (100%) rename apps/{mobile => client}/macos/Runner/Configs/AppInfo.xcconfig (100%) rename apps/{mobile => client}/macos/Runner/Configs/Debug.xcconfig (100%) rename apps/{mobile => client}/macos/Runner/Configs/Release.xcconfig (100%) rename apps/{mobile => client}/macos/Runner/Configs/Warnings.xcconfig (100%) rename apps/{mobile => client}/macos/Runner/DebugProfile.entitlements (100%) rename apps/{mobile => client}/macos/Runner/Info.plist (100%) rename apps/{mobile => client}/macos/Runner/MainFlutterWindow.swift (100%) rename apps/{mobile => client}/macos/Runner/Release.entitlements (100%) rename apps/{mobile => client}/macos/RunnerTests/RunnerTests.swift (100%) rename apps/{mobile => client}/nomadcode_app.iml (100%) rename apps/{mobile => client}/pubspec.lock (94%) rename apps/{mobile => client}/pubspec.yaml (97%) create mode 100644 apps/client/push-notification-todo.md create mode 100644 apps/client/test/integrations/mattermost_push_host_integration_test.dart create mode 100644 apps/client/test/integrations/proto_socket_endpoint_config_test.dart create mode 100644 apps/client/test/integrations/proto_socket_lifecycle_test.dart rename apps/{mobile => client}/test/widget_test.dart (77%) rename apps/{mobile => client}/web/favicon.png (100%) rename apps/{mobile => client}/web/icons/Icon-192.png (100%) rename apps/{mobile => client}/web/icons/Icon-512.png (100%) rename apps/{mobile => client}/web/icons/Icon-maskable-192.png (100%) rename apps/{mobile => client}/web/icons/Icon-maskable-512.png (100%) rename apps/{mobile => client}/web/index.html (100%) rename apps/{mobile => client}/web/manifest.json (100%) rename apps/{mobile => client}/windows/.gitignore (100%) rename apps/{mobile => client}/windows/CMakeLists.txt (100%) rename apps/{mobile => client}/windows/flutter/CMakeLists.txt (100%) rename apps/{mobile => client}/windows/flutter/generated_plugin_registrant.cc (100%) rename apps/{mobile => client}/windows/flutter/generated_plugin_registrant.h (100%) rename apps/{mobile => client}/windows/flutter/generated_plugins.cmake (100%) rename apps/{mobile => client}/windows/runner/CMakeLists.txt (100%) rename apps/{mobile => client}/windows/runner/Runner.rc (100%) rename apps/{mobile => client}/windows/runner/flutter_window.cpp (100%) rename apps/{mobile => client}/windows/runner/flutter_window.h (100%) rename apps/{mobile => client}/windows/runner/main.cpp (100%) rename apps/{mobile => client}/windows/runner/resource.h (100%) rename apps/{mobile => client}/windows/runner/resources/app_icon.ico (100%) rename apps/{mobile => client}/windows/runner/runner.exe.manifest (100%) rename apps/{mobile => client}/windows/runner/utils.cpp (100%) rename apps/{mobile => client}/windows/runner/utils.h (100%) rename apps/{mobile => client}/windows/runner/win32_window.cpp (100%) rename apps/{mobile => client}/windows/runner/win32_window.h (100%) delete mode 100644 apps/mobile/README.md delete mode 100644 apps/mobile/lib/main.dart delete mode 100644 apps/mobile/push-notification-todo.md create mode 100644 docs/test-environments.md diff --git a/README.md b/README.md index 2f44234..c449994 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ NomadCode는 IOP를 사용하는 개발 워크플로우 셸입니다. 사용자 ## 현재 상태 - `services/core`는 health/readiness endpoint, task API, PostgreSQL persistence, goose migration, sqlc query 생성 구조, River job, IOP OpenAI-compatible Responses client 경로, 향후 A2A agent delegation scaffolding, Plane/Mattermost adapter stub을 포함합니다. -- `apps/mobile`은 NomadCode 제품 UI의 기준 Flutter 앱이며 모바일/데스크톱/선택적 web target, Firebase Messaging, local notifications, HTTP client 의존성을 포함합니다. +- `apps/client`는 NomadCode 제품 UI의 기준 Flutter 앱이며 모바일/데스크톱/선택적 web target, Firebase Messaging, local notifications, HTTP client 의존성을 포함합니다. - `apps/web`은 이전 React/Vite 운영 콘솔 scaffold였으며, Flutter-first 통합에 따라 완전히 폐기 및 제거되었습니다. - `packages/contracts`는 향후 OpenAPI, protobuf, generated client, fixture, compatibility asset을 담을 공유 계약 공간입니다. - 활성 계획과 Milestone 상태는 `agent-ops/roadmap/`에서 관리합니다. README에는 현재 작업 상태를 복사하지 않고 진입 문서만 연결합니다. @@ -56,7 +56,7 @@ cd services/core Flutter 앱을 실행합니다. ```bash -cd apps/mobile +cd apps/client flutter pub get flutter run ``` @@ -73,14 +73,14 @@ flutter run | core DB migration | `cd services/core && ./bin/migrate-up` | goose로 PostgreSQL migration을 적용합니다. | | core 테스트 | `cd services/core && ./bin/test` | `go test ./...` wrapper입니다. | | core build | `cd services/core && ./bin/build` | 기본 출력 경로는 `.build/nomadcode-core`입니다. | -| mobile 테스트 | `cd apps/mobile && flutter test` | Flutter test suite입니다. | +| client 테스트 | `cd apps/client && flutter test` | Flutter test suite입니다. | ## 구조 | 경로 | 역할 | |------|------| | `services/core/` | Go backend. orchestration, persistence, scheduling, HTTP API, 외부 service adapter를 담당합니다. | -| `apps/mobile/` | Flutter 기반 제품 UI source of truth, 모바일/데스크톱 클라이언트와 notification 연동 표면입니다. | +| `apps/client/` | Flutter 기반 제품 UI source of truth, 모바일/데스크톱/선택적 web 클라이언트와 notification 연동 표면입니다. | | `apps/web/` | [Legacy/Retired] React/Vite 운영 콘솔 scaffold입니다. Flutter-first 클라이언트 통합에 따라 완전히 폐기 및 제거되었습니다. | | `packages/contracts/` | 서비스와 클라이언트가 공유할 API/schema 계약 공간입니다. | | `docs/` | 아키텍처와 운영 문서입니다. | @@ -129,6 +129,6 @@ IOP 경계가 걸린 작업은 같은 workspace에 sibling IOP repository가 있 - `docs/monorepo.md` - `services/core/README.md` -- `apps/mobile/README.md` +- `apps/client/README.md` - `packages/contracts/README.md` - `agent-ops/roadmap/current.md` diff --git a/agent-ops/roadmap/ROADMAP.md b/agent-ops/roadmap/ROADMAP.md index ce28e33..dd1d8cb 100644 --- a/agent-ops/roadmap/ROADMAP.md +++ b/agent-ops/roadmap/ROADMAP.md @@ -4,7 +4,7 @@ NomadCode는 Flutter 기반 앱, core 서비스, 공유 계약, agent-operation 규칙을 하나의 원레포로 묶어 AI-assisted development workflow를 조율하는 프로젝트다. -현재 로드맵은 `ROADMAP.md -> phase//PHASE.md -> phase//milestones/.md` scaffold를 기준으로 관리한다. React/Vite 웹 콘솔 제거, 서버/Plane/provider 기반 작업, Flutter-first 클라이언트 정리, Mattermost push plugin extraction은 완료되었다. 이후 client 표준화, core workflow 안정화, 외부 통합, Flutter-first 프로젝트 제어 UX를 이어간다. +현재 로드맵은 `ROADMAP.md -> phase//PHASE.md -> phase//milestones/.md` scaffold를 기준으로 관리한다. React/Vite 웹 콘솔 제거, 서버/Plane/provider 기반 작업, Flutter-first 클라이언트 정리, Mattermost push plugin extraction, client integration 표준화는 완료되었다. 이후 core workflow 안정화와 외부 통합을 먼저 정리하고, 실제 Flutter-first 프로젝트 제어 UX 구현은 가장 뒤로 미룬다. ## Phase 흐름 @@ -27,15 +27,15 @@ NomadCode는 Flutter 기반 앱, core 서비스, 공유 계약, agent-operation - [완료] Mattermost Push Plugin Extraction - 경로: `agent-ops/roadmap/archive/phase/mattermost-push-plugin-extraction/PHASE.md` - 요약: Flutter 앱에 섞인 Mattermost push notification migration을 `../mattermost-push-plugin` Flutter plugin repo로 분리했다. -- [진행중] Project Workspace Management UX - - 경로: `agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md` - - 요약: Flutter client 모듈명과 bootstrap, proto-socket, host integration 표준을 먼저 정리하고 이후 프로젝트 단위 제어 UX로 확장한다. - [진행중] Workflow Core - 경로: `agent-ops/roadmap/phase/workflow-core/PHASE.md` - 요약: 실제 e2e 흐름을 기준으로 task lifecycle, retry, timeout, notification event를 안정화한다. - [계획] External Integration - 경로: `agent-ops/roadmap/phase/external-integration/PHASE.md` - 요약: Plane 확장, Mattermost, Agent Integrator, IOP OpenAI API Responses-compatible 호출을 실제 통합 흐름으로 확장한다. +- [계획] Project Workspace Management UX + - 경로: `agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md` + - 요약: client integration 표준화는 완료했고, 실제 프로젝트 단위 앱 UX 구현은 core workflow와 외부 통합 기준 이후로 미룬다. ## 로딩 정책 diff --git a/agent-ops/roadmap/phase/project-workspace-management-ux/milestones/client-integration-standardization.md b/agent-ops/roadmap/archive/phase/project-workspace-management-ux/milestones/client-integration-standardization.md similarity index 51% rename from agent-ops/roadmap/phase/project-workspace-management-ux/milestones/client-integration-standardization.md rename to agent-ops/roadmap/archive/phase/project-workspace-management-ux/milestones/client-integration-standardization.md index 278a732..d79afa0 100644 --- a/agent-ops/roadmap/phase/project-workspace-management-ux/milestones/client-integration-standardization.md +++ b/agent-ops/roadmap/archive/phase/project-workspace-management-ux/milestones/client-integration-standardization.md @@ -7,11 +7,11 @@ ## 목표 -NomadCode Flutter client를 `../iop`와 `../alt`에 복제 가능한 기준 구현으로 정리한다. `apps/client` 모듈명, Web + Android 우선 Dart bootstrap, proto-socket, Mattermost push host integration, cloning handoff 문서를 먼저 표준화하고 iOS는 같은 Dart API 위에 확장 가능한 후속 대상으로 남긴다. +NomadCode Flutter client를 `../iop`와 `../alt`에 복제 가능한 기준 구현으로 정리한다. `apps/client` 모듈명, Web + Android 우선 Dart bootstrap, proto-socket, Mattermost push host integration, cloning handoff 문서를 먼저 표준화한다. iOS native 확장은 이번 범위에서 제외하고 후속 대상으로 남긴다. ## 상태 -[진행중] +[완료] ## 구현 잠금 @@ -20,12 +20,12 @@ NomadCode Flutter client를 `../iop`와 `../alt`에 복제 가능한 기준 구 ## 범위 -- `apps/mobile`을 `apps/client` 기준 구조로 정리하거나 동등한 client 모듈 표준을 명확히 문서화 +- Flutter host를 `apps/client` 기준 구조로 정리하고 동등한 client 모듈 표준을 명확히 문서화 - web/mobile 공통 Dart bootstrap 계층 정리 - proto-socket Dart client 설정, connection bootstrap, 환경 주입 기준 정리 - Mattermost push host integration 표준화 - Firebase host 설정과 plugin 초기화 책임 경계 정리 -- Web과 Android를 1차 target으로 검증하고 iOS 확장 지점을 명확히 문서화 +- Web과 Android를 1차 target으로 검증하고 iOS native 확장은 이번 범위 제외로 명시 - `../iop`, `../alt`로 복제할 client skeleton, integration boundary, handoff 체크리스트 작성 ## 필수 기능 @@ -34,61 +34,71 @@ NomadCode Flutter client를 `../iop`와 `../alt`에 복제 가능한 기준 구 NomadCode client를 다른 프로젝트가 따라올 수 있는 구조 기준으로 정리한다. -- [ ] [module-name] Flutter host 모듈명을 `client` 기준으로 정리하고 package/app 표시 이름의 프로젝트별 유지 기준을 문서화한다. -- [ ] [bootstrap] web/mobile 공통 Dart bootstrap과 platform-specific 초기화 guard를 정의한다. -- [ ] [folder-layout] `src/app`, `src/integrations`, `src/features` 기준의 client skeleton을 정리한다. +- [x] [module-name] Flutter host 모듈명을 `client` 기준으로 정리하고 package/app 표시 이름의 프로젝트별 유지 기준을 문서화한다. +- [x] [bootstrap] web/mobile 공통 Dart bootstrap과 platform-specific 초기화 guard를 정의한다. +- [x] [folder-layout] `src/app`, `src/integrations`, `src/features` 기준의 client skeleton을 정리한다. ### Epic: [integration-standard] Integration boundaries proto-socket과 Mattermost push를 같은 client integration 방식으로 소비하도록 표준화한다. -- [ ] [proto-socket] proto-socket Dart dependency, endpoint configuration, connection lifecycle 기준을 client integration으로 정리한다. -- [ ] [mattermost-push] Mattermost push는 plugin이 runtime을 소유하고 host는 Firebase config, auth/token/signing key, routing callback만 소유하도록 정리한다. -- [ ] [platform-targets] Web과 Android를 1차 target으로 검증하고 iOS는 동일 Dart API 위의 후속 native 확장 지점으로 남긴다. +- [x] [proto-socket] proto-socket Dart dependency, endpoint configuration, connection lifecycle 기준을 client integration으로 정리한다. +- [x] [mattermost-push] Mattermost push는 plugin이 runtime을 소유하고 host는 Firebase config, auth/token/signing key, routing callback만 소유하도록 정리한다. +- [x] [platform-targets] Web과 Android를 1차 target으로 검증하고 iOS native 확장은 이번 범위 제외로 남긴다. ### Epic: [clone-handoff] Clone handoff `../iop`와 `../alt`가 같은 수준으로 따라올 수 있도록 복제 단위와 차이를 문서화한다. -- [ ] [clone-notes] `../iop`와 `../alt`에 전달할 cloning checklist와 프로젝트별 치환 항목을 작성한다. -- [ ] [iop-naming] `../iop`에서는 Portal 명칭을 강조하지 않고 `client` 모듈과 IOP product surface 기준으로 정리한다. -- [ ] [alt-alignment] `../alt`의 기존 `apps/client` 구조와 Riverpod/go_router 경계를 유지하면서 공통 integration skeleton만 맞추는 기준을 작성한다. +- [x] [clone-notes] `../iop`와 `../alt`에 전달할 cloning checklist와 프로젝트별 치환 항목을 작성한다. +- [x] [iop-naming] `../iop`에서는 Portal 명칭을 강조하지 않고 `client` 모듈과 IOP product surface 기준으로 정리한다. +- [x] [alt-alignment] `../alt`의 기존 `apps/client` 구조와 Riverpod/go_router 경계를 유지하면서 공통 integration skeleton만 맞추는 기준을 작성한다. ## 완료 기준 -- [ ] NomadCode Flutter host가 client 표준 구조와 명명 기준을 따른다. -- [ ] Web build와 Android integration이 같은 Dart bootstrap 기준에서 설명되고 검증된다. -- [ ] proto-socket과 Mattermost push integration의 책임 경계가 host/plugin/server 사이에서 충돌 없이 설명된다. -- [ ] iOS가 Android-only 예외가 아니라 동일 Dart API 위의 후속 확장 대상으로 문서화된다. -- [ ] `../iop`와 `../alt`에 클로닝할 파일/구조/설정/문서 체크리스트가 남아 있다. -- [ ] `apps/client` 또는 현행 client host에서 `flutter test`와 필요한 analyze/build 검증이 통과하거나 환경 제약이 명확히 기록된다. +- [x] NomadCode Flutter host가 client 표준 구조와 명명 기준을 따른다. +- [x] Web build와 Android integration이 같은 Dart bootstrap 기준에서 설명되고 검증된다. +- [x] proto-socket과 Mattermost push integration의 책임 경계가 host/plugin/server 사이에서 충돌 없이 설명된다. +- [x] iOS native 확장이 이번 범위 제외이며 후속 대상으로 남는다는 점이 문서화된다. +- [x] `../iop`와 `../alt`에 클로닝할 파일/구조/설정/문서 체크리스트가 남아 있다. +- [x] `apps/client` 또는 현행 client host에서 `flutter test`와 필요한 analyze/build 검증이 통과하거나 환경 제약이 명확히 기록된다. ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 없음 +- 상태: 승인됨 +- 요청일: 2026-05-25 +- 완료 근거: + - `apps/client` 구조, bootstrap, proto-socket, Mattermost push host boundary, clone handoff 문서가 정리되었다. + - 로컬에서 `flutter test`, `flutter analyze --no-fatal-infos`, `flutter build web`가 통과했다. + - `ssh toki@toki-labs.com`의 공용 Android SDK 환경에서 `flutter build apk --debug`가 통과했다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 2026-05-25 사용자 확인에 따라 완료 전환 및 archive 이동을 승인했다. ## 범위 제외 - `../iop`, `../alt` 코드 변경 직접 반영 - Mattermost push plugin 자체의 native runtime 대규모 재작성 -- iOS native push full implementation +- iOS native push full implementation과 iOS target 검증 - 각 프로젝트의 제품별 feature 화면을 동일하게 복제 - IOP 내부 모델 라우팅, ALT quant feature, NomadCode project UX 상세 화면 확정 ## 작업 컨텍스트 -- 관련 경로: `apps/mobile/`, `packages/contracts/`, `docs/`, `README.md`, `../mattermost-push-plugin/` +- 관련 경로: `apps/client/`, `packages/contracts/`, `docs/`, `README.md`, `../mattermost-push-plugin/`, `../../../proto-socket/dart` - 클로닝 대상: `../iop/apps/client`, `../alt/apps/client` - 표준선(선택): repo별 Flutter host 경로는 `apps/client`로 맞추고, Dart package/app 표시 이름은 프로젝트 정체성을 유지한다. - 표준선(선택): `mattermost_push_plugin`은 별도 plugin 이름을 유지하고, host app은 plugin consumer 책임만 갖는다. -- 표준선(선택): Web과 Android를 1차 target으로 두며, iOS는 같은 Dart API와 integration boundary 위에서 후속 native implementation으로 확장한다. +- 표준선(선택): Web과 Android를 1차 target으로 두며, iOS native implementation과 iOS target 검증은 후속 범위로 분리한다. - 표준선(선택): `../iop`는 Portal 명칭을 강조하지 않고 IOP client/product surface로 정리한다. - 선행 작업: Mattermost Push Plugin Extraction - 후속 작업: Project Workspace Management UX, External Integration +- 공용 테스트 환경: `docs/test-environments.md#android-sdk-remote-runner` +- 2026-05-25 갱신 근거: + - `apps/client` 경로와 root helper/docs/rules가 client 기준으로 정리되었다. + - `apps/client/lib/src/app`, `apps/client/lib/src/features`, `apps/client/lib/src/integrations` skeleton과 proto-socket/Mattermost push integration facade가 추가되었다. + - `apps/client/README.md`, `apps/client/push-notification-todo.md`, `packages/contracts/notes/flutter-core-api-candidates.md`에 host/plugin/transport 책임 경계와 clone handoff가 정리되었다. + - `flutter test`, `flutter analyze --no-fatal-infos`, `flutter build web`가 통과했다. analyze의 `avoid_print`는 기존 mobile rule의 baseline info로 남긴다. + - `ssh toki@toki-labs.com`의 공용 Android SDK 환경에서 `flutter build apk --debug`가 통과했다. - 확인 필요: 없음 diff --git a/agent-ops/roadmap/current.md b/agent-ops/roadmap/current.md index 65c11f1..7617147 100644 --- a/agent-ops/roadmap/current.md +++ b/agent-ops/roadmap/current.md @@ -2,19 +2,17 @@ ## 활성 Phase -- [진행중] Project Workspace Management UX - - 경로: `agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md` - [진행중] Workflow Core - 경로: `agent-ops/roadmap/phase/workflow-core/PHASE.md` ## 활성 Milestone -- [진행중] Client Integration Standardization - - Phase: `agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md` - - 경로: `agent-ops/roadmap/phase/project-workspace-management-ux/milestones/client-integration-standardization.md` - [진행중] Workflow Core - Phase: `agent-ops/roadmap/phase/workflow-core/PHASE.md` - 경로: `agent-ops/roadmap/phase/workflow-core/milestones/workflow-core.md` +- [스케치] Roadmap Driven Agent-Ops Automation + - Phase: `agent-ops/roadmap/phase/workflow-core/PHASE.md` + - 경로: `agent-ops/roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md` ## 선택 규칙 @@ -22,6 +20,7 @@ - 활성 Phase는 `agent-ops/roadmap/phase//PHASE.md`를 가리킨다. - 활성 Milestone은 `agent-ops/roadmap/phase//milestones/.md`를 가리킨다. - 활성 항목은 아카이브 경로를 포함하지 않는다. +- `[스케치]` 항목은 컨셉 보강과 `[계획]` 승격 후보이며, 구현 계획 생성 대상은 아니다. - `[검토중]` 항목은 사용자 완료 확인 전까지 활성 항목으로 남길 수 있다. - `[완료]` 또는 `[폐기]` 항목은 archive 링크를 남긴 뒤 활성 항목에서 제거한다. - 요청 내용, 현재 브랜치, 변경 파일, 관련 코드 경로를 보고 가장 관련 있는 Phase와 Milestone을 선택하고 같은 세션에서 1회 읽는다. diff --git a/agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md b/agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md index 6f11839..3e55397 100644 --- a/agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md +++ b/agent-ops/roadmap/phase/project-workspace-management-ux/PHASE.md @@ -2,7 +2,7 @@ ## 상태 -[진행중] +[계획] ## 목표 @@ -13,9 +13,9 @@ Flutter-first 클라이언트 구조 위에서 `apps/client` 모듈, web/mobile 완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다. 완료, 검토중, 진행중, 계획 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. -- [진행중] Client Integration Standardization - - 경로: `agent-ops/roadmap/phase/project-workspace-management-ux/milestones/client-integration-standardization.md` - - 요약: NomadCode client를 기준으로 `apps/client` 모듈명, Web + Android 우선 bootstrap, proto-socket, Mattermost push host integration, cloning handoff를 표준화한다. +- [완료] Client Integration Standardization + - 경로: `agent-ops/roadmap/archive/phase/project-workspace-management-ux/milestones/client-integration-standardization.md` + - 요약: NomadCode client를 기준으로 `apps/client` 모듈명, Web + Android 우선 bootstrap, proto-socket, Mattermost push host integration, cloning handoff를 표준화했다. - [계획] Project Workspace Management UX - 경로: `agent-ops/roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md` @@ -24,6 +24,7 @@ Flutter-first 클라이언트 구조 위에서 `apps/client` 모듈, web/mobile ## Phase 경계 - 기존 Project/Session/Workspace 방향을 대체하지 않고 보강한다. +- 실제 앱 UX 구현과 상세 화면 작업은 공통 모듈, workflow/core 안정화, 외부 통합 기준이 먼저 정리된 뒤 후순위로 진행한다. - IOP 내부 모델 라우팅, 외부 agent 추가 흐름, 상세 화면/API/DB schema 확정은 이 Phase의 결정 전 범위에서 제외한다. - UX 책임 경계와 우선순위는 사용자 결정이 필요한 잠금 항목으로 유지한다. - `../iop`와 `../alt`로 복제할 내용은 제품 feature 전체가 아니라 client skeleton, integration boundary, bootstrap convention, 문서화된 handoff 기준으로 제한한다. diff --git a/agent-ops/roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md b/agent-ops/roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md index 3a62e16..a5c391e 100644 --- a/agent-ops/roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md +++ b/agent-ops/roadmap/phase/project-workspace-management-ux/milestones/project-workspace-management-ux.md @@ -70,8 +70,8 @@ Flutter-first 앱에서 project/session/workspace 제어 표면과 desktop/mobil ## 작업 컨텍스트 -- 관련 경로: `apps/mobile/`, `packages/contracts/`, `services/core/` +- 관련 경로: `apps/client/`, `packages/contracts/`, `services/core/` - 전환 참고 경로: [Legacy/Retired] `apps/web/` (제거됨) -- 선행 작업: Flutter-first Client Consolidation, External Integration +- 선행 작업: Flutter-first Client Consolidation, Client Integration Standardization - 후속 작업: 확인 필요 - 확인 필요: `구현 잠금`의 결정 필요 항목 참고. diff --git a/agent-ops/roadmap/phase/workflow-core/PHASE.md b/agent-ops/roadmap/phase/workflow-core/PHASE.md index 4aa114b..8f32bd3 100644 --- a/agent-ops/roadmap/phase/workflow-core/PHASE.md +++ b/agent-ops/roadmap/phase/workflow-core/PHASE.md @@ -10,13 +10,17 @@ Plane 통신 토대와 provider-neutral pipeline 설계 이후 실제 상태 변 ## Milestone 흐름 -완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다. -완료, 검토중, 진행중, 계획 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. +완료된 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획, 스케치 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다. +완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다. - [진행중] Workflow Core - 경로: `agent-ops/roadmap/phase/workflow-core/milestones/workflow-core.md` - 요약: task lifecycle, 상태 전이 책임, 실패 정책, retry/timeout, notification event 모델을 안정화한다. +- [스케치] Roadmap Driven Agent-Ops Automation + - 경로: `agent-ops/roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md` + - 요약: 사용자가 중요 판단과 상위 계획을 맡고, 에이전트가 로드맵-마일스톤-계획-리뷰 루프를 자동으로 이어가는 운영 컨셉을 구현 가능한 계획으로 구체화한다. + ## Phase 경계 - Workflow Core는 canonical task lifecycle, retry/timeout envelope, heartbeat, terminal 상태 기록을 소유한다. diff --git a/agent-ops/roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md b/agent-ops/roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md new file mode 100644 index 0000000..06470e0 --- /dev/null +++ b/agent-ops/roadmap/phase/workflow-core/milestones/roadmap-driven-agent-ops-automation.md @@ -0,0 +1,86 @@ +# Milestone: Roadmap Driven Agent-Ops Automation + +## 위치 + +- Roadmap: `agent-ops/roadmap/ROADMAP.md` +- Phase: `agent-ops/roadmap/phase/workflow-core/PHASE.md` + +## 목표 + +NomadCode가 로드맵을 중심으로 판단, 실행, 리뷰 루프를 끊기지 않게 운영하는 컨셉을 정리한다. 사용자는 중요 판단과 상위 계획을 결정하고, 에이전트는 그 판단을 바탕으로 마일스톤 구체화, plan 생성, 작업, code-review, 재계획, 완료 반영을 표준 루프로 이어가도록 만드는 방향을 다듬는다. + +## 상태 + +[스케치] + +## 승격 조건 + +- [ ] 로드맵, Phase, Milestone, plan, code-review, runtime 완료 이벤트 사이의 책임 경계를 정의한다. +- [ ] 사용자가 반드시 결정해야 하는 항목과 에이전트가 표준선으로 채워도 되는 항목을 구분한다. +- [ ] `[스케치]`, `[계획]`, `[진행중]`, `[검토중]`, `[완료]` 상태 전환 조건을 NomadCode 자동화 흐름에 맞게 검증한다. +- [ ] `[계획]` 이상으로 승격된 후속 Milestone에서 사용할 `agent-task/m-` 생성, 반복 리뷰, `complete.log`, roadmap 갱신, archive 승인 흐름의 최소 계약을 정한다. +- [ ] 자동 실행 범위와 중단 조건, `USER_REVIEW.md` 사용자 확인 지점, 실패 또는 보완 필요 시 되돌아가는 정책을 정한다. +- [ ] 첫 구현 Milestone으로 쪼갤 후보와 완료 기준을 나눈다. + +## 구현 잠금 + +- 상태: 잠금 +- 결정 필요: + - [ ] 사용자가 직접 승인해야 하는 경계가 milestone 생성, plan 생성, 작업 시작, 검토중 전환, 완료 archive 중 어디까지인지 정한다. + - [ ] NomadCode가 백그라운드에서 자동 진행해도 되는 작업 단위와 명시 확인이 필요한 작업 단위를 정한다. + - [ ] "현재 작업" 질의가 스케치/계획/진행중 후보를 어떤 우선순위로 보여줘야 하는지 정한다. + +## 범위 + +- roadmap 중심 운영 루프의 제품 컨셉 정리 +- Milestone 상태 전환과 구현 잠금의 운용 기준 +- `[계획]` 이상 Milestone에서 사용할 milestone 기반 plan/code-review 반복 루프의 자동화 후보 정의 +- 완료 이벤트가 roadmap에 반영되는 방식과 사용자 최종 확인 지점 정리 +- NomadCode 내부 agent orchestration 후보 정리 + +## 필수 기능 + +### Epic: [roadmap-loop] Roadmap-led operation loop + +로드맵과 마일스톤을 기준으로 에이전트가 다음 작업을 판단하고, 계획/작업/리뷰/재계획을 반복하는 운영 루프를 구체화한다. + +- [ ] [loop-map] 초기 컨셉에서 roadmap, milestone, plan, code-review, complete.log, archive까지 이어지는 상태 흐름을 정의한다. +- [ ] [decision-boundary] 사용자 판단 영역과 에이전트 자동 보강 영역을 구분한다. +- [ ] [automation-contract] 자동으로 진행 가능한 단계와 중단 또는 사용자 확인이 필요한 단계를 정한다. +- [ ] [user-review-stop] 반복 실패와 테스트 환경 차단 상황에서 `USER_REVIEW.md`로 루프를 멈추는 조건과 템플릿 계약을 정한다. +- [ ] [runtime-routing] `[계획]` 이상 Milestone의 task group과 완료 이벤트가 roadmap 갱신으로 이어지는 routing 계약을 정한다. +- [ ] [first-slice] 구현 가능한 첫 Milestone 또는 Epic 후보를 분리한다. + +## 완료 기준 + +- [ ] 컨셉이 `[계획]` Milestone으로 승격 가능한 목표, 범위, 완료 기준을 갖춘다. +- [ ] 사용자 결정 항목과 에이전트 표준 처리 항목이 분리된다. +- [ ] plan/code-review 반복 루프와 roadmap 갱신 루프의 최소 계약이 문서화된다. +- [ ] 자동 루프 중단 조건과 사용자 리뷰 파일 계약이 문서화된다. +- [ ] 첫 구현 단위 후보가 하나 이상 도출된다. + +## 완료 리뷰 + +- 상태: 없음 +- 요청일: 없음 +- 완료 근거: 없음 +- 리뷰 필요: + - [ ] 사용자가 완료 결과를 확인했다 + - [ ] archive 이동을 승인했다 +- 리뷰 코멘트: 없음 + +## 범위 제외 + +- 실제 runtime scheduler 구현 +- 백그라운드 agent 실행 큐 구현 +- UI 화면/API/DB schema 확정 +- 외부 provider 또는 IOP 세부 통합 +- 사용자 승인 없는 자동 archive 이동 + +## 작업 컨텍스트 + +- 관련 경로: `agent-ops/roadmap/`, `agent-ops/skills/common/plan/SKILL.md`, `agent-ops/skills/common/code-review/SKILL.md`, `agent-ops/skills/common/update-roadmap/SKILL.md` +- 표준선(선택): 로드맵은 판단과 방향의 source of truth로 둔다. 이 `[스케치]` 자체는 구현 계획 대상이 아니며, 승격 이후 구현 작업만 milestone 기반 `agent-task/m-/` 루프로 분리한다. +- 선행 작업: Workflow Core +- 후속 작업: `[계획]` 승격 후 runtime, roadmap updater, UI/agent interaction 중 첫 구현 Milestone 후보를 결정한다. +- 확인 필요: 이 스케치를 `[계획]`으로 올릴 때 실제 구현 Milestone을 runtime, roadmap updater, UI/agent interaction 중 어디부터 나눌지 결정해야 한다. diff --git a/agent-ops/rules/project/domain/contracts/rules.md b/agent-ops/rules/project/domain/contracts/rules.md index 80593fc..0daba9b 100644 --- a/agent-ops/rules/project/domain/contracts/rules.md +++ b/agent-ops/rules/project/domain/contracts/rules.md @@ -17,7 +17,7 @@ NomadCode 런타임 사이의 공유 계약 도메인이다. API schema, event s ## 제외 경로 - `services/core/internal/http/` — API implementation은 core 도메인이다. -- `apps/mobile/lib/` — mobile-specific client/service code는 mobile 도메인이다. +- `apps/client/lib/` — Flutter client/service code는 mobile 도메인이다. - `apps/web/` — [Legacy/Retired] 이전 React/Vite web scaffold 경로는 완전히 제거되어 제외 대상입니다. ## 주요 구성 요소 diff --git a/agent-ops/rules/project/domain/core/rules.md b/agent-ops/rules/project/domain/core/rules.md index 0384c50..4d3050d 100644 --- a/agent-ops/rules/project/domain/core/rules.md +++ b/agent-ops/rules/project/domain/core/rules.md @@ -30,7 +30,7 @@ NomadCode의 백엔드 오케스트레이션 도메인이다. workflow, scheduli ## 제외 경로 -- `apps/mobile/` — Flutter-first 제품 클라이언트이며 core 내부 패키지를 직접 참조하지 않는다. +- `apps/client/` — Flutter-first 제품 클라이언트이며 core 내부 패키지를 직접 참조하지 않는다. - `apps/web/` — [Legacy/Retired] React/Vite scaffold 경로는 완전히 제거되어 제외 대상입니다. - `packages/contracts/` — 공유 API/schema 계약의 소유 영역이다. diff --git a/agent-ops/rules/project/domain/mobile/rules.md b/agent-ops/rules/project/domain/mobile/rules.md index fc25912..0c71898 100644 --- a/agent-ops/rules/project/domain/mobile/rules.md +++ b/agent-ops/rules/project/domain/mobile/rules.md @@ -12,12 +12,12 @@ NomadCode의 Flutter-first 제품 UI 도메인이다. 모바일/데스크톱/선 ## 포함 경로 -- `apps/mobile/lib/` — Flutter application code. -- `apps/mobile/lib/services/` — auth, notification, background handlers. -- `apps/mobile/test/` — Flutter widget/service tests. -- `apps/mobile/android/`, `apps/mobile/ios/`, `apps/mobile/macos/`, `apps/mobile/linux/`, `apps/mobile/windows/`, `apps/mobile/web/` — platform-specific runner/config. -- `apps/mobile/pubspec.yaml`, `apps/mobile/pubspec.lock`, `apps/mobile/analysis_options.yaml` — Dart/Flutter dependencies and analysis. -- `apps/mobile/README.md`, `apps/mobile/push-notification-todo.md` — mobile 운영 메모와 push notification 작업 메모. +- `apps/client/lib/` — Flutter application code. +- `apps/client/lib/services/` — auth, notification, background handlers. +- `apps/client/test/` — Flutter widget/service tests. +- `apps/client/android/`, `apps/client/ios/`, `apps/client/macos/`, `apps/client/linux/`, `apps/client/windows/`, `apps/client/web/` — platform-specific runner/config. +- `apps/client/pubspec.yaml`, `apps/client/pubspec.lock`, `apps/client/analysis_options.yaml` — Dart/Flutter dependencies and analysis. +- `apps/client/README.md`, `apps/client/push-notification-todo.md` — mobile 운영 메모와 push notification 작업 메모. ## 제외 경로 diff --git a/agent-ops/rules/project/domain/workspace-ops/rules.md b/agent-ops/rules/project/domain/workspace-ops/rules.md index 934135a..01d0c95 100644 --- a/agent-ops/rules/project/domain/workspace-ops/rules.md +++ b/agent-ops/rules/project/domain/workspace-ops/rules.md @@ -27,7 +27,7 @@ NomadCode 원레포 운영 도메인이다. 루트 helper, 문서, ignore 정책 - `agent-ops/rules/common/` — agentic-framework에서 동기화되는 공통 rules. - `agent-ops/skills/common/` — agentic-framework에서 동기화되는 공통 skills. -- `services/core/`, `apps/web/` (Legacy/Retired), `apps/mobile/`, `packages/contracts/` — 각 제품 도메인 소유 코드. +- `services/core/`, `apps/web/` (Legacy/Retired), `apps/client/`, `packages/contracts/` — 각 제품 도메인 소유 코드. ## 주요 구성 요소 diff --git a/agent-ops/rules/project/rules.md b/agent-ops/rules/project/rules.md index a614461..c9485d1 100644 --- a/agent-ops/rules/project/rules.md +++ b/agent-ops/rules/project/rules.md @@ -12,7 +12,7 @@ NomadCode는 AI 병렬 작업 운용을 위한 원레포다. 백엔드 오케스 ## 주요 구조 - `services/core/` — Go 기반 백엔드, workflow, scheduler, persistence, 외부 adapter. -- `apps/mobile/` — Flutter 기반 제품 UI source of truth, 모바일/데스크톱 클라이언트와 알림 연동. +- `apps/client/` — Flutter 기반 제품 UI source of truth, 모바일/데스크톱/선택적 web 클라이언트와 알림 연동. - `packages/contracts/` — 서비스와 클라이언트가 공유하는 API/schema 계약. - `bin/` — 원레포 공통 test/build/lint/dev entrypoint. - `docs/` — 아키텍처와 운영 문서. @@ -29,7 +29,7 @@ NomadCode는 AI 병렬 작업 운용을 위한 원레포다. 백엔드 오케스 - 원레포 루트에서 가능한 작업은 `bin/test`, `bin/lint`, `bin/build`, `bin/dev`를 우선 사용한다. - cross-boundary 변경은 계약, 서비스, 클라이언트 반영을 같은 브랜치에서 닫는다. - `services/core` 변경은 `go test ./...` 또는 `bin/test`로 확인한다. -- `apps/mobile` 변경은 `flutter test`와 필요 시 `flutter analyze --no-fatal-infos`로 확인한다. +- `apps/client` 변경은 `flutter test`와 필요 시 `flutter analyze --no-fatal-infos`로 확인한다. - `packages/contracts`가 실제 계약을 갖기 전에는 API 형태를 서비스/클라이언트에 중복 고정하지 말고 계약 후보를 문서화한다. - 하위 앱/서비스에 별도 `agent-ops`나 AI entry 파일을 만들지 않는다. 루트 `agent-ops`와 루트 entry 파일만 사용한다. - Plane 관련 core 작업은 `agent-ops/rules/project/domain/core/rules.md`의 `Plane dev 작업 메모`를 먼저 확인한다. Plane API 토큰은 ignored local secret file `.env.plane.local`에서 로드하며, 토큰 값은 규칙/로드맵/문서에 직접 기록하지 않는다. @@ -39,7 +39,7 @@ NomadCode는 AI 병렬 작업 운용을 위한 원레포다. 백엔드 오케스 | 경로 패턴 | 도메인 | rules.md | |----------|--------|----------| | `services/core/**` | core | `agent-ops/rules/project/domain/core/rules.md` | -| `apps/mobile/**` | mobile | `agent-ops/rules/project/domain/mobile/rules.md` | +| `apps/client/**` | mobile | `agent-ops/rules/project/domain/mobile/rules.md` | | `packages/contracts/**` | contracts | `agent-ops/rules/project/domain/contracts/rules.md` | | `bin/**`, `docs/**`, `README.md`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `opencode.json`, `agent-ops/.version`, `agent-ops/bin/**`, `agent-ops/roadmap/**`, `agent-ops/rules/project/**` | workspace-ops | `agent-ops/rules/project/domain/workspace-ops/rules.md` | diff --git a/apps/mobile/.gitignore b/apps/client/.gitignore similarity index 100% rename from apps/mobile/.gitignore rename to apps/client/.gitignore diff --git a/apps/mobile/.idea/libraries/Dart_SDK.xml b/apps/client/.idea/libraries/Dart_SDK.xml similarity index 100% rename from apps/mobile/.idea/libraries/Dart_SDK.xml rename to apps/client/.idea/libraries/Dart_SDK.xml diff --git a/apps/mobile/.idea/libraries/KotlinJavaRuntime.xml b/apps/client/.idea/libraries/KotlinJavaRuntime.xml similarity index 100% rename from apps/mobile/.idea/libraries/KotlinJavaRuntime.xml rename to apps/client/.idea/libraries/KotlinJavaRuntime.xml diff --git a/apps/mobile/.idea/modules.xml b/apps/client/.idea/modules.xml similarity index 100% rename from apps/mobile/.idea/modules.xml rename to apps/client/.idea/modules.xml diff --git a/apps/mobile/.idea/runConfigurations/main_dart.xml b/apps/client/.idea/runConfigurations/main_dart.xml similarity index 100% rename from apps/mobile/.idea/runConfigurations/main_dart.xml rename to apps/client/.idea/runConfigurations/main_dart.xml diff --git a/apps/mobile/.idea/workspace.xml b/apps/client/.idea/workspace.xml similarity index 100% rename from apps/mobile/.idea/workspace.xml rename to apps/client/.idea/workspace.xml diff --git a/apps/mobile/.metadata b/apps/client/.metadata similarity index 100% rename from apps/mobile/.metadata rename to apps/client/.metadata diff --git a/apps/mobile/.vscode/launch.json b/apps/client/.vscode/launch.json similarity index 100% rename from apps/mobile/.vscode/launch.json rename to apps/client/.vscode/launch.json diff --git a/apps/client/README.md b/apps/client/README.md new file mode 100644 index 0000000..b9a5c10 --- /dev/null +++ b/apps/client/README.md @@ -0,0 +1,102 @@ +# nomadcode-app + +The Flutter-first NomadCode client application. + +## Client Module Standard + +The Flutter host application in this monorepo is standardized under `apps/client/`. This directory serves as the reference architecture for other projects (such as `../iop` and `../alt`) to replicate. + +### Directory Layout +- **`lib/src/app/`**: Root application routing, bootstrap settings (`bootstrap.dart`), global configurations, and state setup. +- **`lib/src/features/`**: Feature-specific UI surfaces, business logic, and local UI state. Keep layout decoupled from shared integration protocols. +- **`lib/src/integrations/`**: Adapters and lifecycle hooks for external system boundaries. Any connection initialization or platform-specific service logic should reside here under strict interfaces. + +### Naming Conventions +- **Flutter Host Path**: Always kept as `apps/client/` to act as the primary client skeleton. +- **Dart Package Name**: Standardized as `nomadcode_app`. When cloning, this package name can be customized to match target project identities (e.g. `iop_client_app`, `alt_client_app`) in `pubspec.yaml`, while maintaining the same directory layout. + +--- + +## Bootstrap + +NomadCode features a standardized, platform-safe bootstrap layer in `lib/src/app/bootstrap.dart`: +- **Common Initialization Path**: `bootstrapNomadCodeClient()` performs synchronous preparation, setups UI configurations (e.g., fullscreen system overlays), and initializes Firebase + Mattermost push notification boundaries. +- **Platform-Specific Guard**: Native modules and background tasks (such as Firebase Messaging background handlers) are cleanly guarded inside integration-specific or native layers, preventing background crashes or initialization collision. +- **No-Op Defaults**: External transport components (such as the proto-socket lifecycle engine) initialize with safe no-op adapters by default to guarantee bootstrap safety. + +--- + +## Platform Targets + +We prioritize and validate two primary platforms in this milestone, while ensuring others can be added seamlessly: +- **Web & Android (Primary Targets)**: Fully validated in the main bootstrap configuration. Local notification integration and web socket configurations are verified to build and bootstrap without compilation errors. +- **iOS (Future Expansion)**: iOS is treated as a first-class citizen using the same Dart APIs and integration facade boundaries (`MattermostPushClient`, `ProtoSocketLifecycle`). Native iOS APNS and notification delegate handlers can be wired later into the same Dart interfaces without modifying the core bootstrap logic. + +--- + +## Integration Boundaries + +`apps/client/lib/src/integrations/` groups all external-system adapters behind small Dart interfaces: + +- `integrations/mattermost/` — `MattermostPushClient` interface, `MattermostPushPluginClient` adapter (the only production reference to `MattermostPushPlugin.instance`), `MattermostPushHostIntegration` (initialize + auth handoff + navigation callbacks), and `MattermostAuthService`. +- `integrations/proto_socket/` — `ProtoSocketEndpointConfig` and `ProtoSocketLifecycle` facade over the local `proto_socket` Dart path dependency. Bootstrap does not auto-connect; enabling and wiring a real connector are deferred to the next milestone. + +For detailed integration boundaries, platform targets, and clone handoff guidelines, see [push-notification-todo.md](push-notification-todo.md). + +--- + +## Mattermost Push Notification Integration + +Mattermost push notification delivery is implemented via the local `mattermost_push_plugin` path dependency. + +### Architecture & Responsibility Split + +To maintain a clean and maintainable codebase, the responsibilities are split between the host application and the plugin as follows: + +#### Host Application (`apps/client`) Responsibilities: +- **Firebase Configuration**: Contains the standard Firebase configuration files (`android/app/google-services.json`) and performs the standard `Firebase.initializeApp()` call in Dart main. +- **Mattermost Authentication**: Handles login and session lifecycles, and passes the user authentication token and server signing key through the `MattermostPushClient` boundary. The production `MattermostPushPluginClient` adapter owns the plugin singleton calls. +- **Navigation Callbacks**: Registers standard Flutter navigation callbacks `onNavigateToChannel` and `onNavigateToThread` to handle workspace channel and CRT thread routing upon user interaction. +- **FCM Token Sync**: Listens to device token registrations via `onDeviceTokenReady` and registers the token with the Mattermost server. + +#### Mattermost Push Plugin (`mattermost_push_plugin`) Responsibilities: +- **Native Android FCM Service**: Intercepts background/foreground FCM notifications securely via a custom Kotlin FCM service. +- **Signature Verification**: Validates incoming push payload signatures against stored public signing keys (using `JJWT`). +- **System Notification Builder**: Displays structured notifications with local avatar caches, thread summaries, and action buttons. +- **Inline Reply / Dismiss Actions**: Receives native background actions, records responses in a Room database, and forwards replies/ACKs directly to the Mattermost API via custom OkHttp adapters. + +--- + +## Cloning & Handoff Checklist + +For sibling repositories (`../iop` and `../alt`) aiming to align with this standard structure: + +### Sibling Specific Alignment Guidelines: +- **`../iop`**: Avoid emphasizing legacy Portal naming conventions. Reorganize/rename legacy mobile paths to the `apps/client` standard. Align folder structures under `src/app`, `src/features`, and `src/integrations`. Do not clone NomadCode product-specific screens—only the integration and bootstrap skeletons. +- **`../alt`**: Retain ALT-specific Riverpod-based dependency graph and `go_router` for app routing. However, extract external adapters into `lib/src/integrations/` matching the common Dart facade interfaces (`MattermostPushClient`, `ProtoSocketLifecycle`) to align the bootstrap pipeline. + +### Common Replacement Checklist: +When cloning this host template into a new environment, replace the following values: +1. **Dart Package Name**: Rename `nomadcode_app` to your target app package identifier in `pubspec.yaml`. +2. **Display & App Naming**: Adjust launcher configurations, bundle identifiers, and target product names in native assets (`android/app/src/main/AndroidManifest.xml`, `ios/Runner/Info.plist`). +3. **Firebase Configuration**: Replace `google-services.json` (Android) and `GoogleService-Info.plist` (iOS) with target project project credentials. +4. **Endpoint Defaults**: Update `ProtoSocketEndpointConfig` initialization defaults in environment configurations to route to your target backends. +5. **Mattermost Server Configurations**: Update server URLs, authentications, and asymmetrical public keys matching the target deployment environment. + +--- + +## Verification + +To ensure code stability and structure correctness, run the following verification checks within this directory: +- **Unit Tests**: + ```bash + flutter test + ``` +- **Static Analysis**: + ```bash + flutter analyze --no-fatal-infos + ``` +- **Web Build Validation**: + ```bash + flutter build web + ``` diff --git a/apps/mobile/analysis_options.yaml b/apps/client/analysis_options.yaml similarity index 100% rename from apps/mobile/analysis_options.yaml rename to apps/client/analysis_options.yaml diff --git a/apps/mobile/android/.gitignore b/apps/client/android/.gitignore similarity index 100% rename from apps/mobile/android/.gitignore rename to apps/client/android/.gitignore diff --git a/apps/mobile/android/app/build.gradle.kts b/apps/client/android/app/build.gradle.kts similarity index 100% rename from apps/mobile/android/app/build.gradle.kts rename to apps/client/android/app/build.gradle.kts diff --git a/apps/mobile/android/app/google-services.json b/apps/client/android/app/google-services.json similarity index 100% rename from apps/mobile/android/app/google-services.json rename to apps/client/android/app/google-services.json diff --git a/apps/mobile/android/app/src/debug/AndroidManifest.xml b/apps/client/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from apps/mobile/android/app/src/debug/AndroidManifest.xml rename to apps/client/android/app/src/debug/AndroidManifest.xml diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/client/android/app/src/main/AndroidManifest.xml similarity index 100% rename from apps/mobile/android/app/src/main/AndroidManifest.xml rename to apps/client/android/app/src/main/AndroidManifest.xml diff --git a/apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt b/apps/client/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt similarity index 100% rename from apps/mobile/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt rename to apps/client/android/app/src/main/java/com/tokilabs/mattermost/MainActivity.kt diff --git a/apps/mobile/android/app/src/main/res/drawable-v21/launch_background.xml b/apps/client/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from apps/mobile/android/app/src/main/res/drawable-v21/launch_background.xml rename to apps/client/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/apps/mobile/android/app/src/main/res/drawable/launch_background.xml b/apps/client/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from apps/mobile/android/app/src/main/res/drawable/launch_background.xml rename to apps/client/android/app/src/main/res/drawable/launch_background.xml diff --git a/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to apps/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to apps/client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png rename to apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png rename to apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to apps/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png rename to apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to apps/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png rename to apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to apps/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png rename to apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to apps/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png rename to apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to apps/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/apps/mobile/android/app/src/main/res/values-night/styles.xml b/apps/client/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from apps/mobile/android/app/src/main/res/values-night/styles.xml rename to apps/client/android/app/src/main/res/values-night/styles.xml diff --git a/apps/mobile/android/app/src/main/res/values/styles.xml b/apps/client/android/app/src/main/res/values/styles.xml similarity index 100% rename from apps/mobile/android/app/src/main/res/values/styles.xml rename to apps/client/android/app/src/main/res/values/styles.xml diff --git a/apps/mobile/android/app/src/profile/AndroidManifest.xml b/apps/client/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from apps/mobile/android/app/src/profile/AndroidManifest.xml rename to apps/client/android/app/src/profile/AndroidManifest.xml diff --git a/apps/mobile/android/build.gradle.kts b/apps/client/android/build.gradle.kts similarity index 100% rename from apps/mobile/android/build.gradle.kts rename to apps/client/android/build.gradle.kts diff --git a/apps/mobile/android/gradle.properties b/apps/client/android/gradle.properties similarity index 100% rename from apps/mobile/android/gradle.properties rename to apps/client/android/gradle.properties diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties b/apps/client/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from apps/mobile/android/gradle/wrapper/gradle-wrapper.properties rename to apps/client/android/gradle/wrapper/gradle-wrapper.properties diff --git a/apps/mobile/android/nomadcode_app_android.iml b/apps/client/android/nomadcode_app_android.iml similarity index 100% rename from apps/mobile/android/nomadcode_app_android.iml rename to apps/client/android/nomadcode_app_android.iml diff --git a/apps/mobile/android/settings.gradle.kts b/apps/client/android/settings.gradle.kts similarity index 100% rename from apps/mobile/android/settings.gradle.kts rename to apps/client/android/settings.gradle.kts diff --git a/apps/mobile/ios/.gitignore b/apps/client/ios/.gitignore similarity index 100% rename from apps/mobile/ios/.gitignore rename to apps/client/ios/.gitignore diff --git a/apps/mobile/ios/Flutter/AppFrameworkInfo.plist b/apps/client/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from apps/mobile/ios/Flutter/AppFrameworkInfo.plist rename to apps/client/ios/Flutter/AppFrameworkInfo.plist diff --git a/apps/mobile/ios/Flutter/Debug.xcconfig b/apps/client/ios/Flutter/Debug.xcconfig similarity index 100% rename from apps/mobile/ios/Flutter/Debug.xcconfig rename to apps/client/ios/Flutter/Debug.xcconfig diff --git a/apps/mobile/ios/Flutter/Release.xcconfig b/apps/client/ios/Flutter/Release.xcconfig similarity index 100% rename from apps/mobile/ios/Flutter/Release.xcconfig rename to apps/client/ios/Flutter/Release.xcconfig diff --git a/apps/mobile/ios/Podfile b/apps/client/ios/Podfile similarity index 100% rename from apps/mobile/ios/Podfile rename to apps/client/ios/Podfile diff --git a/apps/mobile/ios/Runner.xcodeproj/project.pbxproj b/apps/client/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from apps/mobile/ios/Runner.xcodeproj/project.pbxproj rename to apps/client/ios/Runner.xcodeproj/project.pbxproj diff --git a/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to apps/client/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from apps/mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to apps/client/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/apps/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from apps/mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to apps/client/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/apps/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/client/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from apps/mobile/ios/Runner.xcworkspace/contents.xcworkspacedata rename to apps/client/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/apps/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from apps/mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/client/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apps/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from apps/mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to apps/client/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/apps/mobile/ios/Runner/AppDelegate.swift b/apps/client/ios/Runner/AppDelegate.swift similarity index 100% rename from apps/mobile/ios/Runner/AppDelegate.swift rename to apps/client/ios/Runner/AppDelegate.swift diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to apps/client/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from apps/mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to apps/client/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/apps/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/client/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from apps/mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to apps/client/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/apps/mobile/ios/Runner/Base.lproj/Main.storyboard b/apps/client/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from apps/mobile/ios/Runner/Base.lproj/Main.storyboard rename to apps/client/ios/Runner/Base.lproj/Main.storyboard diff --git a/apps/mobile/ios/Runner/Info.plist b/apps/client/ios/Runner/Info.plist similarity index 100% rename from apps/mobile/ios/Runner/Info.plist rename to apps/client/ios/Runner/Info.plist diff --git a/apps/mobile/ios/Runner/Runner-Bridging-Header.h b/apps/client/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from apps/mobile/ios/Runner/Runner-Bridging-Header.h rename to apps/client/ios/Runner/Runner-Bridging-Header.h diff --git a/apps/mobile/ios/Runner/SceneDelegate.swift b/apps/client/ios/Runner/SceneDelegate.swift similarity index 100% rename from apps/mobile/ios/Runner/SceneDelegate.swift rename to apps/client/ios/Runner/SceneDelegate.swift diff --git a/apps/mobile/ios/RunnerTests/RunnerTests.swift b/apps/client/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from apps/mobile/ios/RunnerTests/RunnerTests.swift rename to apps/client/ios/RunnerTests/RunnerTests.swift diff --git a/apps/client/lib/main.dart b/apps/client/lib/main.dart new file mode 100644 index 0000000..5215b8f --- /dev/null +++ b/apps/client/lib/main.dart @@ -0,0 +1,3 @@ +import 'src/app/bootstrap.dart'; + +Future main() => runNomadCodeClient(); diff --git a/apps/client/lib/src/app/bootstrap.dart b/apps/client/lib/src/app/bootstrap.dart new file mode 100644 index 0000000..bbc3d3b --- /dev/null +++ b/apps/client/lib/src/app/bootstrap.dart @@ -0,0 +1,54 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../integrations/mattermost/mattermost_push_host_integration.dart'; +import '../integrations/mattermost/mattermost_push_plugin_client.dart'; +import '../integrations/proto_socket/proto_socket_endpoint_config.dart'; +import '../integrations/proto_socket/proto_socket_lifecycle.dart'; +import 'nomadcode_client_app.dart'; + +final _mattermostHost = MattermostPushHostIntegration( + pushClient: MattermostPushPluginClient(), +); +final _navigatorKey = GlobalKey(); + +/// proto-socket lifecycle wired with a no-op connector by default. Bootstrap +/// does NOT auto-connect; an explicit configuration switch enables it once a +/// real endpoint contract is wired. +final _protoSocketLifecycle = ProtoSocketLifecycle( + connector: NoopProtoSocketConnector(), +); +ProtoSocketEndpointConfig? _protoSocketConfig; + +Future applyFullscreenMode() async { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + ), + ); +} + +Future bootstrapNomadCodeClient() async { + await applyFullscreenMode(); + await Firebase.initializeApp(); + await _mattermostHost.initialize(); + + _protoSocketConfig = ProtoSocketEndpointConfig.fromEnv(const {}); +} + +Future runNomadCodeClient() async { + WidgetsFlutterBinding.ensureInitialized(); + await bootstrapNomadCodeClient(); + runApp( + NomadCodeClientApp( + mattermostHost: _mattermostHost, + navigatorKey: _navigatorKey, + protoSocketLifecycle: _protoSocketLifecycle, + protoSocketConfig: _protoSocketConfig, + ), + ); +} diff --git a/apps/client/lib/src/app/nomadcode_client_app.dart b/apps/client/lib/src/app/nomadcode_client_app.dart new file mode 100644 index 0000000..6562f81 --- /dev/null +++ b/apps/client/lib/src/app/nomadcode_client_app.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../features/workspaces/presentation/workspace_home_page.dart'; +import '../integrations/mattermost/mattermost_push_host_integration.dart'; +import '../integrations/proto_socket/proto_socket_endpoint_config.dart'; +import '../integrations/proto_socket/proto_socket_lifecycle.dart'; +import '../integrations/workspace/workspace_launcher.dart'; +import 'bootstrap.dart' show applyFullscreenMode; + +class NomadCodeClientApp extends StatelessWidget { + final MattermostPushHostIntegration? mattermostHost; + final GlobalKey? navigatorKey; + final ProtoSocketLifecycle? protoSocketLifecycle; + final ProtoSocketEndpointConfig? protoSocketConfig; + + const NomadCodeClientApp({ + super.key, + this.mattermostHost, + this.navigatorKey, + this.protoSocketLifecycle, + this.protoSocketConfig, + }); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'nomadcode-app', + navigatorKey: navigatorKey, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.dark, + ), + useMaterial3: true, + ), + home: _NomadCodeClientHome(mattermostHost: mattermostHost), + ); + } +} + +class _NomadCodeClientHome extends StatefulWidget { + final MattermostPushHostIntegration? mattermostHost; + + const _NomadCodeClientHome({this.mattermostHost}); + + @override + State<_NomadCodeClientHome> createState() => _NomadCodeClientHomeState(); +} + +class _NomadCodeClientHomeState extends State<_NomadCodeClientHome> { + final WorkspaceLauncher _launcher = WorkspaceLauncher(); + StreamSubscription? _notifSub; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + applyFullscreenMode(); + }); + + final host = widget.mattermostHost; + if (host != null) { + _notifSub = host.onNotification.listen((data) { + if (data['type'] != 'message') return; + final message = data['message'] as String? ?? ''; + final channel = data['channel_name'] as String? ?? ''; + final sender = data['sender_name'] as String? ?? ''; + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + sender.isNotEmpty ? '[$channel] $sender: $message' : message, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + duration: const Duration(seconds: 4), + behavior: SnackBarBehavior.floating, + ), + ); + }); + } + } + + @override + void dispose() { + _notifSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return WorkspaceHomePage( + onLaunchCodeServer: (project) => _launcher.openCodeServer(project), + ); + } +} diff --git a/apps/mobile/lib/models/project_workspace.dart b/apps/client/lib/src/features/workspaces/domain/project_workspace.dart similarity index 100% rename from apps/mobile/lib/models/project_workspace.dart rename to apps/client/lib/src/features/workspaces/domain/project_workspace.dart diff --git a/apps/mobile/lib/screens/workspace_home_page.dart b/apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart similarity index 99% rename from apps/mobile/lib/screens/workspace_home_page.dart rename to apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart index afabce8..f0cf2f1 100644 --- a/apps/mobile/lib/screens/workspace_home_page.dart +++ b/apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../models/project_workspace.dart'; +import '../domain/project_workspace.dart'; class WorkspaceHomePage extends StatefulWidget { final Future Function(ProjectWorkspace)? onLaunchCodeServer; diff --git a/apps/mobile/lib/services/mattermost_auth_service.dart b/apps/client/lib/src/integrations/mattermost/mattermost_auth_service.dart similarity index 97% rename from apps/mobile/lib/services/mattermost_auth_service.dart rename to apps/client/lib/src/integrations/mattermost/mattermost_auth_service.dart index bc2e081..59c0090 100644 --- a/apps/mobile/lib/services/mattermost_auth_service.dart +++ b/apps/client/lib/src/integrations/mattermost/mattermost_auth_service.dart @@ -2,11 +2,12 @@ import 'dart:convert'; import 'package:flutter/foundation.dart' show FlutterError; import 'package:flutter/services.dart' show rootBundle; import 'package:http/http.dart' as http; -import 'package:mattermost_push_plugin/mattermost_push_plugin.dart'; + +import 'mattermost_push_client.dart'; /// Mattermost 자동 로그인 + FCM 디바이스 토큰 서버 등록 서비스. class MattermostAuthService { - final MattermostPushPlugin _pushService; + final MattermostPushClient _pushService; String? _serverUrl; String? _authToken; diff --git a/apps/client/lib/src/integrations/mattermost/mattermost_push_client.dart b/apps/client/lib/src/integrations/mattermost/mattermost_push_client.dart new file mode 100644 index 0000000..f346cce --- /dev/null +++ b/apps/client/lib/src/integrations/mattermost/mattermost_push_client.dart @@ -0,0 +1,21 @@ +/// Host-facing interface for the Mattermost push plugin. +/// +/// All production access to the platform plugin singleton is routed through +/// implementations of this interface. The host integration depends on this +/// abstraction so tests can inject fakes without booting Firebase/FCM. +abstract interface class MattermostPushClient { + Stream> get onNotification; + + Future initialize(); + Future getDeviceToken(); + Future setAuthToken(String serverUrl, String token); + Future setSigningKey(String serverUrl, String signingKey); + + set onDeviceTokenReady(Future Function(String token)? callback); + set onNavigateToChannel( + void Function(String serverUrl, String channelId)? callback, + ); + set onNavigateToThread( + void Function(String serverUrl, String rootId)? callback, + ); +} diff --git a/apps/client/lib/src/integrations/mattermost/mattermost_push_host_integration.dart b/apps/client/lib/src/integrations/mattermost/mattermost_push_host_integration.dart new file mode 100644 index 0000000..a795b5b --- /dev/null +++ b/apps/client/lib/src/integrations/mattermost/mattermost_push_host_integration.dart @@ -0,0 +1,70 @@ +import 'package:flutter/foundation.dart' show debugPrint; + +import 'mattermost_auth_service.dart'; +import 'mattermost_push_client.dart'; + +/// Owns the Mattermost host/plugin responsibility boundary. +/// +/// The integration: +/// - initializes the push client exactly once, +/// - performs auto-login best-effort (skips silently when credentials are +/// unavailable, so app boot is not blocked), +/// - registers navigation callbacks in one place (not from a widget build), +/// - exposes the notification stream for app-level consumers. +class MattermostPushHostIntegration { + final MattermostPushClient pushClient; + final MattermostAuthService Function(MattermostPushClient client) + _authServiceFactory; + + bool _initialized = false; + + MattermostPushHostIntegration({ + required this.pushClient, + MattermostAuthService Function(MattermostPushClient client)? + authServiceFactory, + }) : _authServiceFactory = + authServiceFactory ?? + ((MattermostPushClient client) => MattermostAuthService(client)); + + Stream> get onNotification => pushClient.onNotification; + + bool get isInitialized => _initialized; + + /// Initialize plugin, perform auth handoff, and register navigation + /// callbacks. Safe to call once at bootstrap; subsequent calls are no-ops. + /// + /// [onNavigateToChannel] / [onNavigateToThread] are optional. When omitted, + /// the integration installs debug-logging fallbacks so the navigation path + /// remains observable without forcing the caller to wire routing. + Future initialize({ + void Function(String serverUrl, String channelId)? onNavigateToChannel, + void Function(String serverUrl, String rootId)? onNavigateToThread, + }) async { + if (_initialized) return; + _initialized = true; + + await pushClient.initialize(); + + final authService = _authServiceFactory(pushClient); + try { + await authService.autoLoginAndRegister(); + } catch (e) { + debugPrint('[MattermostHost] Mattermost auto-login failed: $e'); + } + + pushClient.onNavigateToChannel = + onNavigateToChannel ?? + (serverUrl, channelId) { + debugPrint( + '[MattermostHost] Navigate to channel: $channelId on $serverUrl', + ); + }; + pushClient.onNavigateToThread = + onNavigateToThread ?? + (serverUrl, rootId) { + debugPrint( + '[MattermostHost] Navigate to thread: $rootId on $serverUrl', + ); + }; + } +} diff --git a/apps/client/lib/src/integrations/mattermost/mattermost_push_plugin_client.dart b/apps/client/lib/src/integrations/mattermost/mattermost_push_plugin_client.dart new file mode 100644 index 0000000..ce29edb --- /dev/null +++ b/apps/client/lib/src/integrations/mattermost/mattermost_push_plugin_client.dart @@ -0,0 +1,51 @@ +import 'package:mattermost_push_plugin/mattermost_push_plugin.dart'; + +import 'mattermost_push_client.dart'; + +/// Production adapter wrapping the platform-channel singleton. +/// +/// This is the ONLY file in production that should reference +/// `MattermostPushPlugin.instance`. Everything else depends on +/// [MattermostPushClient]. +class MattermostPushPluginClient implements MattermostPushClient { + final MattermostPushPlugin _plugin; + + MattermostPushPluginClient({MattermostPushPlugin? plugin}) + : _plugin = plugin ?? MattermostPushPlugin.instance; + + @override + Stream> get onNotification => _plugin.onNotification; + + @override + Future initialize() => _plugin.initialize(); + + @override + Future getDeviceToken() => _plugin.getDeviceToken(); + + @override + Future setAuthToken(String serverUrl, String token) => + _plugin.setAuthToken(serverUrl, token); + + @override + Future setSigningKey(String serverUrl, String signingKey) => + _plugin.setSigningKey(serverUrl, signingKey); + + @override + set onDeviceTokenReady(Future Function(String token)? callback) { + _plugin.onDeviceTokenReady = callback; + } + + @override + set onNavigateToChannel( + void Function(String serverUrl, String channelId)? callback, + ) { + _plugin.onNavigateToChannel = callback; + } + + @override + set onNavigateToThread( + void Function(String serverUrl, String rootId)? callback, + ) { + _plugin.onNavigateToThread = callback; + } +} diff --git a/apps/client/lib/src/integrations/proto_socket/proto_socket_endpoint_config.dart b/apps/client/lib/src/integrations/proto_socket/proto_socket_endpoint_config.dart new file mode 100644 index 0000000..c7e937e --- /dev/null +++ b/apps/client/lib/src/integrations/proto_socket/proto_socket_endpoint_config.dart @@ -0,0 +1,84 @@ +/// Endpoint/transport configuration for proto-socket connections. +/// +/// Pure value object. No network I/O, no Flutter binding dependencies — safe +/// to construct from app bootstrap or unit tests. +class ProtoSocketEndpointConfig { + static const String defaultPath = '/'; + static const int defaultHeartbeatIntervalSeconds = 30; + static const int defaultHeartbeatWaitSeconds = 60; + + final String host; + final int port; + final bool secure; + final String path; + final int heartbeatIntervalSeconds; + final int heartbeatWaitSeconds; + final bool enabled; + + const ProtoSocketEndpointConfig({ + required this.host, + required this.port, + required this.secure, + this.path = defaultPath, + this.heartbeatIntervalSeconds = defaultHeartbeatIntervalSeconds, + this.heartbeatWaitSeconds = defaultHeartbeatWaitSeconds, + this.enabled = false, + }); + + /// Build a config from a flat string map (e.g. dart-define / env). + /// + /// `host` is required; everything else falls back to defaults. + /// Returns null when `host` is absent or empty so callers can treat the + /// integration as disabled. + static ProtoSocketEndpointConfig? fromEnv(Map env) { + final host = env['PROTO_SOCKET_HOST']?.trim(); + if (host == null || host.isEmpty) { + return null; + } + + final secure = _parseBool(env['PROTO_SOCKET_SECURE']) ?? false; + final port = _parseInt(env['PROTO_SOCKET_PORT']) ?? (secure ? 443 : 80); + final path = _normalizePath(env['PROTO_SOCKET_PATH']); + final heartbeatInterval = + _parseInt(env['PROTO_SOCKET_HEARTBEAT_INTERVAL_SECONDS']) ?? + defaultHeartbeatIntervalSeconds; + final heartbeatWait = + _parseInt(env['PROTO_SOCKET_HEARTBEAT_WAIT_SECONDS']) ?? + defaultHeartbeatWaitSeconds; + final enabled = _parseBool(env['PROTO_SOCKET_ENABLED']) ?? false; + + return ProtoSocketEndpointConfig( + host: host, + port: port, + secure: secure, + path: path, + heartbeatIntervalSeconds: heartbeatInterval, + heartbeatWaitSeconds: heartbeatWait, + enabled: enabled, + ); + } + + String get scheme => secure ? 'wss' : 'ws'; + + Uri toUri() => Uri(scheme: scheme, host: host, port: port, path: path); + + static String _normalizePath(String? raw) { + final value = raw?.trim(); + if (value == null || value.isEmpty) return defaultPath; + return value.startsWith('/') ? value : '/$value'; + } + + static int? _parseInt(String? raw) { + final value = raw?.trim(); + if (value == null || value.isEmpty) return null; + return int.tryParse(value); + } + + static bool? _parseBool(String? raw) { + final value = raw?.trim().toLowerCase(); + if (value == null || value.isEmpty) return null; + if (value == 'true' || value == '1' || value == 'yes') return true; + if (value == 'false' || value == '0' || value == 'no') return false; + return null; + } +} diff --git a/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart b/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart new file mode 100644 index 0000000..8c9a764 --- /dev/null +++ b/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'proto_socket_endpoint_config.dart'; + +enum ProtoSocketConnectionState { disconnected, connecting, connected, failed } + +/// Pluggable connector. Production wires a real proto-socket client; tests +/// inject a fake to drive lifecycle transitions deterministically without +/// opening network sockets. +abstract interface class ProtoSocketConnector { + Future connect(ProtoSocketEndpointConfig config); + Future disconnect(); +} + +/// App-facing lifecycle facade for proto-socket integration. +/// +/// Owns the connection state machine and exposes a stream for observers. +/// Does NOT auto-connect on construction — bootstrap creates the facade with +/// a config and an explicit `connect()` call (or skips it entirely when the +/// integration is disabled). +class ProtoSocketLifecycle { + final ProtoSocketConnector _connector; + final StreamController _stateController = + StreamController.broadcast(); + + ProtoSocketConnectionState _state = ProtoSocketConnectionState.disconnected; + Object? _lastError; + + ProtoSocketLifecycle({required ProtoSocketConnector connector}) + : _connector = connector; + + ProtoSocketConnectionState get state => _state; + Stream get stateStream => _stateController.stream; + Object? get lastError => _lastError; + + Future connect(ProtoSocketEndpointConfig config) async { + if (_state == ProtoSocketConnectionState.connecting || + _state == ProtoSocketConnectionState.connected) { + return; + } + _emit(ProtoSocketConnectionState.connecting); + try { + await _connector.connect(config); + _emit(ProtoSocketConnectionState.connected); + } catch (e) { + _lastError = e; + _emit(ProtoSocketConnectionState.failed); + rethrow; + } + } + + Future disconnect() async { + if (_state == ProtoSocketConnectionState.disconnected) return; + await _connector.disconnect(); + _emit(ProtoSocketConnectionState.disconnected); + } + + Future dispose() async { + await _stateController.close(); + } + + void _emit(ProtoSocketConnectionState next) { + _state = next; + if (!_stateController.isClosed) { + _stateController.add(next); + } + } +} + +/// Default production connector. Constructs only when proto-socket is enabled. +/// Currently a no-op placeholder: bootstrap does not auto-open a real socket +/// by default, and a concrete transport will be wired in a later milestone +/// once the core endpoint contract stabilizes. +class NoopProtoSocketConnector implements ProtoSocketConnector { + @override + Future connect(ProtoSocketEndpointConfig config) async {} + + @override + Future disconnect() async {} +} diff --git a/apps/mobile/lib/services/workspace_launcher.dart b/apps/client/lib/src/integrations/workspace/workspace_launcher.dart similarity index 82% rename from apps/mobile/lib/services/workspace_launcher.dart rename to apps/client/lib/src/integrations/workspace/workspace_launcher.dart index acefc34..eac78f8 100644 --- a/apps/mobile/lib/services/workspace_launcher.dart +++ b/apps/client/lib/src/integrations/workspace/workspace_launcher.dart @@ -1,5 +1,5 @@ import 'package:url_launcher/url_launcher.dart'; -import '../models/project_workspace.dart'; +import '../../features/workspaces/domain/project_workspace.dart'; class WorkspaceLauncher { Future openCodeServer(ProjectWorkspace project) async { diff --git a/apps/mobile/linux/.gitignore b/apps/client/linux/.gitignore similarity index 100% rename from apps/mobile/linux/.gitignore rename to apps/client/linux/.gitignore diff --git a/apps/mobile/linux/CMakeLists.txt b/apps/client/linux/CMakeLists.txt similarity index 100% rename from apps/mobile/linux/CMakeLists.txt rename to apps/client/linux/CMakeLists.txt diff --git a/apps/mobile/linux/flutter/CMakeLists.txt b/apps/client/linux/flutter/CMakeLists.txt similarity index 100% rename from apps/mobile/linux/flutter/CMakeLists.txt rename to apps/client/linux/flutter/CMakeLists.txt diff --git a/apps/mobile/linux/flutter/generated_plugin_registrant.cc b/apps/client/linux/flutter/generated_plugin_registrant.cc similarity index 100% rename from apps/mobile/linux/flutter/generated_plugin_registrant.cc rename to apps/client/linux/flutter/generated_plugin_registrant.cc diff --git a/apps/mobile/linux/flutter/generated_plugin_registrant.h b/apps/client/linux/flutter/generated_plugin_registrant.h similarity index 100% rename from apps/mobile/linux/flutter/generated_plugin_registrant.h rename to apps/client/linux/flutter/generated_plugin_registrant.h diff --git a/apps/mobile/linux/flutter/generated_plugins.cmake b/apps/client/linux/flutter/generated_plugins.cmake similarity index 100% rename from apps/mobile/linux/flutter/generated_plugins.cmake rename to apps/client/linux/flutter/generated_plugins.cmake diff --git a/apps/mobile/linux/runner/CMakeLists.txt b/apps/client/linux/runner/CMakeLists.txt similarity index 100% rename from apps/mobile/linux/runner/CMakeLists.txt rename to apps/client/linux/runner/CMakeLists.txt diff --git a/apps/mobile/linux/runner/main.cc b/apps/client/linux/runner/main.cc similarity index 100% rename from apps/mobile/linux/runner/main.cc rename to apps/client/linux/runner/main.cc diff --git a/apps/mobile/linux/runner/my_application.cc b/apps/client/linux/runner/my_application.cc similarity index 100% rename from apps/mobile/linux/runner/my_application.cc rename to apps/client/linux/runner/my_application.cc diff --git a/apps/mobile/linux/runner/my_application.h b/apps/client/linux/runner/my_application.h similarity index 100% rename from apps/mobile/linux/runner/my_application.h rename to apps/client/linux/runner/my_application.h diff --git a/apps/mobile/macos/.gitignore b/apps/client/macos/.gitignore similarity index 100% rename from apps/mobile/macos/.gitignore rename to apps/client/macos/.gitignore diff --git a/apps/mobile/macos/Flutter/Flutter-Debug.xcconfig b/apps/client/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from apps/mobile/macos/Flutter/Flutter-Debug.xcconfig rename to apps/client/macos/Flutter/Flutter-Debug.xcconfig diff --git a/apps/mobile/macos/Flutter/Flutter-Release.xcconfig b/apps/client/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from apps/mobile/macos/Flutter/Flutter-Release.xcconfig rename to apps/client/macos/Flutter/Flutter-Release.xcconfig diff --git a/apps/mobile/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/client/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 100% rename from apps/mobile/macos/Flutter/GeneratedPluginRegistrant.swift rename to apps/client/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/apps/mobile/macos/Podfile b/apps/client/macos/Podfile similarity index 100% rename from apps/mobile/macos/Podfile rename to apps/client/macos/Podfile diff --git a/apps/mobile/macos/Runner.xcodeproj/project.pbxproj b/apps/client/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from apps/mobile/macos/Runner.xcodeproj/project.pbxproj rename to apps/client/macos/Runner.xcodeproj/project.pbxproj diff --git a/apps/mobile/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/client/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from apps/mobile/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/client/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apps/mobile/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/client/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from apps/mobile/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to apps/client/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/apps/mobile/macos/Runner.xcworkspace/contents.xcworkspacedata b/apps/client/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from apps/mobile/macos/Runner.xcworkspace/contents.xcworkspacedata rename to apps/client/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/apps/mobile/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/client/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from apps/mobile/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to apps/client/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apps/mobile/macos/Runner/AppDelegate.swift b/apps/client/macos/Runner/AppDelegate.swift similarity index 100% rename from apps/mobile/macos/Runner/AppDelegate.swift rename to apps/client/macos/Runner/AppDelegate.swift diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from apps/mobile/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to apps/client/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/apps/mobile/macos/Runner/Base.lproj/MainMenu.xib b/apps/client/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from apps/mobile/macos/Runner/Base.lproj/MainMenu.xib rename to apps/client/macos/Runner/Base.lproj/MainMenu.xib diff --git a/apps/mobile/macos/Runner/Configs/AppInfo.xcconfig b/apps/client/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from apps/mobile/macos/Runner/Configs/AppInfo.xcconfig rename to apps/client/macos/Runner/Configs/AppInfo.xcconfig diff --git a/apps/mobile/macos/Runner/Configs/Debug.xcconfig b/apps/client/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from apps/mobile/macos/Runner/Configs/Debug.xcconfig rename to apps/client/macos/Runner/Configs/Debug.xcconfig diff --git a/apps/mobile/macos/Runner/Configs/Release.xcconfig b/apps/client/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from apps/mobile/macos/Runner/Configs/Release.xcconfig rename to apps/client/macos/Runner/Configs/Release.xcconfig diff --git a/apps/mobile/macos/Runner/Configs/Warnings.xcconfig b/apps/client/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from apps/mobile/macos/Runner/Configs/Warnings.xcconfig rename to apps/client/macos/Runner/Configs/Warnings.xcconfig diff --git a/apps/mobile/macos/Runner/DebugProfile.entitlements b/apps/client/macos/Runner/DebugProfile.entitlements similarity index 100% rename from apps/mobile/macos/Runner/DebugProfile.entitlements rename to apps/client/macos/Runner/DebugProfile.entitlements diff --git a/apps/mobile/macos/Runner/Info.plist b/apps/client/macos/Runner/Info.plist similarity index 100% rename from apps/mobile/macos/Runner/Info.plist rename to apps/client/macos/Runner/Info.plist diff --git a/apps/mobile/macos/Runner/MainFlutterWindow.swift b/apps/client/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from apps/mobile/macos/Runner/MainFlutterWindow.swift rename to apps/client/macos/Runner/MainFlutterWindow.swift diff --git a/apps/mobile/macos/Runner/Release.entitlements b/apps/client/macos/Runner/Release.entitlements similarity index 100% rename from apps/mobile/macos/Runner/Release.entitlements rename to apps/client/macos/Runner/Release.entitlements diff --git a/apps/mobile/macos/RunnerTests/RunnerTests.swift b/apps/client/macos/RunnerTests/RunnerTests.swift similarity index 100% rename from apps/mobile/macos/RunnerTests/RunnerTests.swift rename to apps/client/macos/RunnerTests/RunnerTests.swift diff --git a/apps/mobile/nomadcode_app.iml b/apps/client/nomadcode_app.iml similarity index 100% rename from apps/mobile/nomadcode_app.iml rename to apps/client/nomadcode_app.iml diff --git a/apps/mobile/pubspec.lock b/apps/client/pubspec.lock similarity index 94% rename from apps/mobile/pubspec.lock rename to apps/client/pubspec.lock index a46feb0..a8fcf3b 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/client/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.10.10" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -231,6 +239,21 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + proto_socket: + dependency: "direct main" + description: + path: "../../../proto-socket/dart" + relative: true + source: path + version: "1.0.5" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" sky_engine: dependency: transitive description: flutter diff --git a/apps/mobile/pubspec.yaml b/apps/client/pubspec.yaml similarity index 97% rename from apps/mobile/pubspec.yaml rename to apps/client/pubspec.yaml index aa5cd8a..3bffd2f 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/client/pubspec.yaml @@ -43,6 +43,10 @@ dependencies: mattermost_push_plugin: path: ../../../mattermost-push-plugin + # proto-socket: shared protocol/transport library (local path) + proto_socket: + path: ../../../proto-socket/dart + # HTTP client http: ^1.3.0 diff --git a/apps/client/push-notification-todo.md b/apps/client/push-notification-todo.md new file mode 100644 index 0000000..2a23ba7 --- /dev/null +++ b/apps/client/push-notification-todo.md @@ -0,0 +1,82 @@ +# Client Integration Handoff & Migration Notes + +> [!IMPORTANT] +> **Host App Integration Standard** +> As of the `mattermost-push-plugin-extraction` milestone, all native FCM services, Room DB structures, JJWT signature verification, inline reply/dismiss receivers, notification builders, and ACK request handling have been successfully extracted into the local Flutter plugin repository: `../mattermost-push-plugin`. +> +> **Do NOT edit native push files directly inside this host app repository (`apps/client/android`).** All native push logic and platform services now reside in and must be maintained within the plugin repository. + +--- + +## 1. Responsibility Split & Boundaries + +To keep host architectures generic and lightweight, we maintain a strict boundary between host logic and external dependency plugins. + +### 1.1 Mattermost Push Notification Boundary +- **Host Application (`apps/client`) Responsibilities**: + 1. **Firebase Manifest & Metadata**: + - `google-services.json` inside `apps/client/android/app/` + - Initializing Firebase inside `lib/src/app/bootstrap.dart` via `Firebase.initializeApp()`. + 2. **Credential Plumbing**: + - Host standardizes all credential synchronization through the `MattermostPushClient` interface rather than accessing the native plugin directly: + - Login: Sync authorization credentials via `pushClient.setAuthToken(serverUrl, token)`. + - Logout: Revoke and clear credentials via `pushClient.setAuthToken(serverUrl, '')`. + - Asymmetric Signing Keys: Register server public keys via `pushClient.setSigningKey(serverUrl, publicKey)`. + 3. **App Navigation Callbacks**: + - Wire navigation delegates on the `MattermostPushClient` interface: + - `onNavigateToChannel = (serverUrl, channelId) => ...`: Handles custom routing logic to channel view on notification click. + - `onNavigateToThread = (serverUrl, rootId) => ...`: Handles CRT thread view navigation. + 4. **Device Token Sync**: + - Listen to token registration callbacks via the `onDeviceTokenReady` callback on `MattermostPushClient` interface, and forward the target device token back to the Mattermost backend server. + - *Note*: Production calls to `MattermostPushPlugin.instance` are strictly isolated and encapsulated within the `MattermostPushPluginClient` adapter implementation, maintaining a generic, testable host bootstrap. +- **Mattermost Push Plugin (`mattermost_push_plugin`) Responsibilities**: + - Intercepting incoming FCM pushes via Kotlin services (`MattermostFirebaseMessagingService`). + - Parsing payloads and verifying cryptographic signatures with JJWT. + - Interacting with local SQLite database (Room DB). + - Building native system notifications, dismissing ACKs, and inline replies. + +### 1.2 Proto-socket Integration Boundary +- **Host Application (`apps/client`) Responsibilities**: + 1. **Local Dependency Setup**: + - Reference the local path package `proto_socket` in `pubspec.yaml`. + 2. **Facade Configuration**: + - Read configuration defaults through `ProtoSocketEndpointConfig.fromEnv()`. + - Wire connections via `ProtoSocketLifecycle` facade inside the bootstrap stream. + 3. **No-Op Auto-connect**: + - By default, bootstrap configures the lifecycle with `enabled: false` and uses `NoopProtoSocketConnector()`. + - Real socket initialization and active transport loops are decoupled from bootstrap and deferred to client UI activation. + +--- + +## 2. Platform Target Status & Expansion + +### 2.1 Web Target +- Supported in core configuration bootstrap. +- Socket facade safely falls back to standard WebSocket interfaces or no-op handlers in non-supported native environments. + +### 2.2 Android Target +- Fully operational for Mattermost push notifications. +- Integrates Firebase Cloud Messaging, JJWT verification, Room Database, and native background broadcasts. + +### 2.3 iOS Target Status & Native Extension +- iOS is fully integrated into the unified Dart API boundary. +- **Dart Level Integration**: Sibling systems can target iOS using identical calls (`MattermostPushClient` methods). +- **Native Implementation Extension**: Sibling repos can implement native APNS delegate receivers inside the iOS project folder without needing to modify common Dart facades or common bootstrap parameters. + +--- + +## 3. Reference Sibling Repo Alignment & Replacement + +When replicating this integration boundary in sibling repositories (`../iop` and `../alt`): + +### Checklist for Sibling Alignment: +1. **Firebase Credential Sync**: Provide valid `google-services.json` (Android) and `GoogleService-Info.plist` (iOS) in respective project roots. +2. **Local Path Imports**: Ensure `pubspec.yaml` points correctly to the actual sibling workspace dependency paths: + ```yaml + mattermost_push_plugin: + path: ../../../mattermost-push-plugin + proto_socket: + path: ../../../proto-socket/dart + ``` + *Note*: If sibling repositories utilize different directory hierarchies (e.g. nested packages or monorepo namespaces), the relative path must be adjusted accordingly to point to the correct workspace location of the shared libraries. +3. **Endpoint Realization**: Initialize client configuration with workspace endpoints matching your target backend (e.g. IOP portal routing or ALT quant metrics server). diff --git a/apps/client/test/integrations/mattermost_push_host_integration_test.dart b/apps/client/test/integrations/mattermost_push_host_integration_test.dart new file mode 100644 index 0000000..4352f16 --- /dev/null +++ b/apps/client/test/integrations/mattermost_push_host_integration_test.dart @@ -0,0 +1,170 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:nomadcode_app/src/integrations/mattermost/mattermost_auth_service.dart'; +import 'package:nomadcode_app/src/integrations/mattermost/mattermost_push_client.dart'; +import 'package:nomadcode_app/src/integrations/mattermost/mattermost_push_host_integration.dart'; + +class _FakePushClient implements MattermostPushClient { + int initializeCalls = 0; + int navigateChannelAssignments = 0; + int navigateThreadAssignments = 0; + + final StreamController> _controller = + StreamController>.broadcast(); + + void Function(String serverUrl, String channelId)? _navigateChannel; + void Function(String serverUrl, String rootId)? _navigateThread; + + @override + Stream> get onNotification => _controller.stream; + + void emit(Map data) => _controller.add(data); + + Future dispose() => _controller.close(); + + @override + Future initialize() async { + initializeCalls += 1; + } + + @override + Future getDeviceToken() async => null; + + @override + Future setAuthToken(String serverUrl, String token) async {} + + @override + Future setSigningKey(String serverUrl, String signingKey) async {} + + @override + set onDeviceTokenReady(Future Function(String token)? callback) {} + + @override + set onNavigateToChannel( + void Function(String serverUrl, String channelId)? callback, + ) { + navigateChannelAssignments += 1; + _navigateChannel = callback; + } + + @override + set onNavigateToThread( + void Function(String serverUrl, String rootId)? callback, + ) { + navigateThreadAssignments += 1; + _navigateThread = callback; + } + + void triggerNavigateChannel(String serverUrl, String channelId) { + _navigateChannel?.call(serverUrl, channelId); + } + + void triggerNavigateThread(String serverUrl, String rootId) { + _navigateThread?.call(serverUrl, rootId); + } +} + +class _NoopAuthService implements MattermostAuthService { + int autoLoginCalls = 0; + final bool throwOnLogin; + + _NoopAuthService({this.throwOnLogin = false}); + + @override + Future autoLoginAndRegister() async { + autoLoginCalls += 1; + if (throwOnLogin) { + throw StateError('credentials missing'); + } + } + + @override + noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + test('initialize calls push client init exactly once', () async { + final push = _FakePushClient(); + final auth = _NoopAuthService(); + final host = MattermostPushHostIntegration( + pushClient: push, + authServiceFactory: (_) => auth, + ); + + await host.initialize(); + await host.initialize(); + + expect(push.initializeCalls, equals(1)); + expect(auth.autoLoginCalls, equals(1)); + expect(host.isInitialized, isTrue); + + await push.dispose(); + }); + + test('auto-login failure does not block initialize', () async { + final push = _FakePushClient(); + final auth = _NoopAuthService(throwOnLogin: true); + final host = MattermostPushHostIntegration( + pushClient: push, + authServiceFactory: (_) => auth, + ); + + await host.initialize(); + + expect(host.isInitialized, isTrue); + expect(push.initializeCalls, equals(1)); + + await push.dispose(); + }); + + test( + 'navigation callbacks are assigned exactly once during initialize', + () async { + final push = _FakePushClient(); + final host = MattermostPushHostIntegration( + pushClient: push, + authServiceFactory: (_) => _NoopAuthService(), + ); + + String? capturedChannel; + String? capturedThread; + await host.initialize( + onNavigateToChannel: (_, channelId) => capturedChannel = channelId, + onNavigateToThread: (_, rootId) => capturedThread = rootId, + ); + + expect(push.navigateChannelAssignments, equals(1)); + expect(push.navigateThreadAssignments, equals(1)); + + push.triggerNavigateChannel('https://srv', 'channel-1'); + push.triggerNavigateThread('https://srv', 'root-1'); + + expect(capturedChannel, equals('channel-1')); + expect(capturedThread, equals('root-1')); + + await push.dispose(); + }, + ); + + test('notification stream remains consumable through integration', () async { + final push = _FakePushClient(); + final host = MattermostPushHostIntegration( + pushClient: push, + authServiceFactory: (_) => _NoopAuthService(), + ); + await host.initialize(); + + final received = >[]; + final sub = host.onNotification.listen(received.add); + + push.emit(const {'type': 'message', 'message': 'hi'}); + await Future.delayed(Duration.zero); + + expect(received, hasLength(1)); + expect(received.first['message'], equals('hi')); + + await sub.cancel(); + await push.dispose(); + }); +} diff --git a/apps/client/test/integrations/proto_socket_endpoint_config_test.dart b/apps/client/test/integrations/proto_socket_endpoint_config_test.dart new file mode 100644 index 0000000..0702eb5 --- /dev/null +++ b/apps/client/test/integrations/proto_socket_endpoint_config_test.dart @@ -0,0 +1,80 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_endpoint_config.dart'; + +void main() { + group('ProtoSocketEndpointConfig defaults', () { + test('defaults path to "/" when not provided', () { + const config = ProtoSocketEndpointConfig( + host: 'core.example.com', + port: 443, + secure: true, + ); + expect(config.path, equals('/')); + expect(config.enabled, isFalse); + expect(config.heartbeatIntervalSeconds, equals(30)); + expect(config.heartbeatWaitSeconds, equals(60)); + }); + + test('scheme selects wss when secure, ws otherwise', () { + const secureConfig = ProtoSocketEndpointConfig( + host: 'core.example.com', + port: 443, + secure: true, + ); + const plainConfig = ProtoSocketEndpointConfig( + host: 'core.example.com', + port: 8080, + secure: false, + ); + expect(secureConfig.scheme, equals('wss')); + expect(plainConfig.scheme, equals('ws')); + final uri = secureConfig.toUri(); + expect(uri.scheme, equals('wss')); + expect(uri.host, equals('core.example.com')); + expect(uri.port, equals(443)); + }); + }); + + group('ProtoSocketEndpointConfig.fromEnv', () { + test('returns null when PROTO_SOCKET_HOST is missing or empty', () { + expect(ProtoSocketEndpointConfig.fromEnv(const {}), isNull); + expect( + ProtoSocketEndpointConfig.fromEnv(const {'PROTO_SOCKET_HOST': ' '}), + isNull, + ); + }); + + test('parses full env map with explicit values', () { + final config = ProtoSocketEndpointConfig.fromEnv(const { + 'PROTO_SOCKET_HOST': 'core.example.com', + 'PROTO_SOCKET_PORT': '9443', + 'PROTO_SOCKET_SECURE': 'true', + 'PROTO_SOCKET_PATH': 'socket', + 'PROTO_SOCKET_HEARTBEAT_INTERVAL_SECONDS': '15', + 'PROTO_SOCKET_HEARTBEAT_WAIT_SECONDS': '45', + 'PROTO_SOCKET_ENABLED': 'true', + }); + expect(config, isNotNull); + expect(config!.host, equals('core.example.com')); + expect(config.port, equals(9443)); + expect(config.secure, isTrue); + expect(config.path, equals('/socket')); + expect(config.heartbeatIntervalSeconds, equals(15)); + expect(config.heartbeatWaitSeconds, equals(45)); + expect(config.enabled, isTrue); + }); + + test('falls back to default port based on secure flag', () { + final secureConfig = ProtoSocketEndpointConfig.fromEnv(const { + 'PROTO_SOCKET_HOST': 'core.example.com', + 'PROTO_SOCKET_SECURE': 'true', + }); + final plainConfig = ProtoSocketEndpointConfig.fromEnv(const { + 'PROTO_SOCKET_HOST': 'core.example.com', + 'PROTO_SOCKET_SECURE': 'false', + }); + expect(secureConfig!.port, equals(443)); + expect(plainConfig!.port, equals(80)); + }); + }); +} diff --git a/apps/client/test/integrations/proto_socket_lifecycle_test.dart b/apps/client/test/integrations/proto_socket_lifecycle_test.dart new file mode 100644 index 0000000..1f93d1f --- /dev/null +++ b/apps/client/test/integrations/proto_socket_lifecycle_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_endpoint_config.dart'; +import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_lifecycle.dart'; + +class _FakeConnector implements ProtoSocketConnector { + int connectCalls = 0; + int disconnectCalls = 0; + Object? throwOnConnect; + + @override + Future connect(ProtoSocketEndpointConfig config) async { + connectCalls += 1; + final err = throwOnConnect; + if (err != null) throw err; + } + + @override + Future disconnect() async { + disconnectCalls += 1; + } +} + +void main() { + const config = ProtoSocketEndpointConfig( + host: 'core.example.com', + port: 443, + secure: true, + ); + + test('happy path emits connecting -> connected -> disconnected', () async { + final fake = _FakeConnector(); + final lifecycle = ProtoSocketLifecycle(connector: fake); + final states = []; + final sub = lifecycle.stateStream.listen(states.add); + + expect(lifecycle.state, ProtoSocketConnectionState.disconnected); + + await lifecycle.connect(config); + expect(lifecycle.state, ProtoSocketConnectionState.connected); + expect(fake.connectCalls, equals(1)); + + await lifecycle.disconnect(); + expect(lifecycle.state, ProtoSocketConnectionState.disconnected); + expect(fake.disconnectCalls, equals(1)); + + await Future.delayed(Duration.zero); + await sub.cancel(); + await lifecycle.dispose(); + + expect(states, [ + ProtoSocketConnectionState.connecting, + ProtoSocketConnectionState.connected, + ProtoSocketConnectionState.disconnected, + ]); + }); + + test('connect failure transitions to failed and records error', () async { + final fake = _FakeConnector()..throwOnConnect = StateError('boom'); + final lifecycle = ProtoSocketLifecycle(connector: fake); + + await expectLater(lifecycle.connect(config), throwsA(isA())); + expect(lifecycle.state, ProtoSocketConnectionState.failed); + expect(lifecycle.lastError, isA()); + + await lifecycle.dispose(); + }); + + test('connect is a no-op when already connecting or connected', () async { + final fake = _FakeConnector(); + final lifecycle = ProtoSocketLifecycle(connector: fake); + + await lifecycle.connect(config); + await lifecycle.connect(config); + + expect(fake.connectCalls, equals(1)); + await lifecycle.dispose(); + }); + + test('disconnect from disconnected state is a no-op', () async { + final fake = _FakeConnector(); + final lifecycle = ProtoSocketLifecycle(connector: fake); + + await lifecycle.disconnect(); + expect(fake.disconnectCalls, equals(0)); + expect(lifecycle.state, ProtoSocketConnectionState.disconnected); + + await lifecycle.dispose(); + }); +} diff --git a/apps/mobile/test/widget_test.dart b/apps/client/test/widget_test.dart similarity index 77% rename from apps/mobile/test/widget_test.dart rename to apps/client/test/widget_test.dart index c72ef7d..5024b14 100644 --- a/apps/mobile/test/widget_test.dart +++ b/apps/client/test/widget_test.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:nomadcode_app/main.dart'; -import 'package:nomadcode_app/screens/workspace_home_page.dart'; -import 'package:nomadcode_app/models/project_workspace.dart'; +import 'package:nomadcode_app/src/app/nomadcode_client_app.dart'; +import 'package:nomadcode_app/src/features/workspaces/domain/project_workspace.dart'; +import 'package:nomadcode_app/src/features/workspaces/presentation/workspace_home_page.dart'; void main() { testWidgets('App renders console dashboard title', ( WidgetTester tester, ) async { - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const NomadCodeClientApp()); expect(find.text('NomadCode Console'), findsOneWidget); expect(find.text('NomadCode Core'), findsNWidgets(2)); diff --git a/apps/mobile/web/favicon.png b/apps/client/web/favicon.png similarity index 100% rename from apps/mobile/web/favicon.png rename to apps/client/web/favicon.png diff --git a/apps/mobile/web/icons/Icon-192.png b/apps/client/web/icons/Icon-192.png similarity index 100% rename from apps/mobile/web/icons/Icon-192.png rename to apps/client/web/icons/Icon-192.png diff --git a/apps/mobile/web/icons/Icon-512.png b/apps/client/web/icons/Icon-512.png similarity index 100% rename from apps/mobile/web/icons/Icon-512.png rename to apps/client/web/icons/Icon-512.png diff --git a/apps/mobile/web/icons/Icon-maskable-192.png b/apps/client/web/icons/Icon-maskable-192.png similarity index 100% rename from apps/mobile/web/icons/Icon-maskable-192.png rename to apps/client/web/icons/Icon-maskable-192.png diff --git a/apps/mobile/web/icons/Icon-maskable-512.png b/apps/client/web/icons/Icon-maskable-512.png similarity index 100% rename from apps/mobile/web/icons/Icon-maskable-512.png rename to apps/client/web/icons/Icon-maskable-512.png diff --git a/apps/mobile/web/index.html b/apps/client/web/index.html similarity index 100% rename from apps/mobile/web/index.html rename to apps/client/web/index.html diff --git a/apps/mobile/web/manifest.json b/apps/client/web/manifest.json similarity index 100% rename from apps/mobile/web/manifest.json rename to apps/client/web/manifest.json diff --git a/apps/mobile/windows/.gitignore b/apps/client/windows/.gitignore similarity index 100% rename from apps/mobile/windows/.gitignore rename to apps/client/windows/.gitignore diff --git a/apps/mobile/windows/CMakeLists.txt b/apps/client/windows/CMakeLists.txt similarity index 100% rename from apps/mobile/windows/CMakeLists.txt rename to apps/client/windows/CMakeLists.txt diff --git a/apps/mobile/windows/flutter/CMakeLists.txt b/apps/client/windows/flutter/CMakeLists.txt similarity index 100% rename from apps/mobile/windows/flutter/CMakeLists.txt rename to apps/client/windows/flutter/CMakeLists.txt diff --git a/apps/mobile/windows/flutter/generated_plugin_registrant.cc b/apps/client/windows/flutter/generated_plugin_registrant.cc similarity index 100% rename from apps/mobile/windows/flutter/generated_plugin_registrant.cc rename to apps/client/windows/flutter/generated_plugin_registrant.cc diff --git a/apps/mobile/windows/flutter/generated_plugin_registrant.h b/apps/client/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from apps/mobile/windows/flutter/generated_plugin_registrant.h rename to apps/client/windows/flutter/generated_plugin_registrant.h diff --git a/apps/mobile/windows/flutter/generated_plugins.cmake b/apps/client/windows/flutter/generated_plugins.cmake similarity index 100% rename from apps/mobile/windows/flutter/generated_plugins.cmake rename to apps/client/windows/flutter/generated_plugins.cmake diff --git a/apps/mobile/windows/runner/CMakeLists.txt b/apps/client/windows/runner/CMakeLists.txt similarity index 100% rename from apps/mobile/windows/runner/CMakeLists.txt rename to apps/client/windows/runner/CMakeLists.txt diff --git a/apps/mobile/windows/runner/Runner.rc b/apps/client/windows/runner/Runner.rc similarity index 100% rename from apps/mobile/windows/runner/Runner.rc rename to apps/client/windows/runner/Runner.rc diff --git a/apps/mobile/windows/runner/flutter_window.cpp b/apps/client/windows/runner/flutter_window.cpp similarity index 100% rename from apps/mobile/windows/runner/flutter_window.cpp rename to apps/client/windows/runner/flutter_window.cpp diff --git a/apps/mobile/windows/runner/flutter_window.h b/apps/client/windows/runner/flutter_window.h similarity index 100% rename from apps/mobile/windows/runner/flutter_window.h rename to apps/client/windows/runner/flutter_window.h diff --git a/apps/mobile/windows/runner/main.cpp b/apps/client/windows/runner/main.cpp similarity index 100% rename from apps/mobile/windows/runner/main.cpp rename to apps/client/windows/runner/main.cpp diff --git a/apps/mobile/windows/runner/resource.h b/apps/client/windows/runner/resource.h similarity index 100% rename from apps/mobile/windows/runner/resource.h rename to apps/client/windows/runner/resource.h diff --git a/apps/mobile/windows/runner/resources/app_icon.ico b/apps/client/windows/runner/resources/app_icon.ico similarity index 100% rename from apps/mobile/windows/runner/resources/app_icon.ico rename to apps/client/windows/runner/resources/app_icon.ico diff --git a/apps/mobile/windows/runner/runner.exe.manifest b/apps/client/windows/runner/runner.exe.manifest similarity index 100% rename from apps/mobile/windows/runner/runner.exe.manifest rename to apps/client/windows/runner/runner.exe.manifest diff --git a/apps/mobile/windows/runner/utils.cpp b/apps/client/windows/runner/utils.cpp similarity index 100% rename from apps/mobile/windows/runner/utils.cpp rename to apps/client/windows/runner/utils.cpp diff --git a/apps/mobile/windows/runner/utils.h b/apps/client/windows/runner/utils.h similarity index 100% rename from apps/mobile/windows/runner/utils.h rename to apps/client/windows/runner/utils.h diff --git a/apps/mobile/windows/runner/win32_window.cpp b/apps/client/windows/runner/win32_window.cpp similarity index 100% rename from apps/mobile/windows/runner/win32_window.cpp rename to apps/client/windows/runner/win32_window.cpp diff --git a/apps/mobile/windows/runner/win32_window.h b/apps/client/windows/runner/win32_window.h similarity index 100% rename from apps/mobile/windows/runner/win32_window.h rename to apps/client/windows/runner/win32_window.h diff --git a/apps/mobile/README.md b/apps/mobile/README.md deleted file mode 100644 index 342f706..0000000 --- a/apps/mobile/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# nomadcode-app - -The Flutter-first NomadCode client application. - -## Mattermost Push Notification Integration - -Mattermost push notification delivery is implemented via the local `mattermost_push_plugin` path dependency. - -### Architecture & Responsibility Split - -To maintain a clean and maintainable codebase, the responsibilities are split between the host application and the plugin as follows: - -#### Host Application (`apps/mobile`) Responsibilities: -- **Firebase Configuration**: Contains the standard Firebase configuration files (`android/app/google-services.json`) and performs the standard `Firebase.initializeApp()` call in Dart main. -- **Mattermost Authentication**: Handles login and session lifecycles, and securely registers the user authentication token and server signing key to the plugin by calling `MattermostPushPlugin.instance.setAuthToken()` and `MattermostPushPlugin.instance.setSigningKey()`. -- **Navigation Callbacks**: Registers standard Flutter navigation callbacks `onNavigateToChannel` and `onNavigateToThread` to handle workspace channel and CRT thread routing upon user interaction. -- **FCM Token Sync**: Listens to device token registrations via `onDeviceTokenReady` and registers the token with the Mattermost server. - -#### Mattermost Push Plugin (`mattermost_push_plugin`) Responsibilities: -- **Native Android FCM Service**: Intercepts background/foreground FCM notifications securely via a custom Kotlin FCM service. -- **Signature Verification**: Validates incoming push payload signatures against stored public signing keys (using `JJWT`). -- **System Notification Builder**: Displays structured notifications with local avatar caches, thread summaries, and action buttons. -- **Inline Reply / Dismiss Actions**: Receives native background actions, records responses in a Room database, and forwards replies/ACKs directly to the Mattermost API via custom OkHttp adapters. - ---- - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) -- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/apps/mobile/lib/main.dart b/apps/mobile/lib/main.dart deleted file mode 100644 index 1e80db7..0000000 --- a/apps/mobile/lib/main.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:async'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; -import 'package:mattermost_push_plugin/mattermost_push_plugin.dart'; -import 'services/mattermost_auth_service.dart'; -import 'services/workspace_launcher.dart'; -import 'screens/workspace_home_page.dart'; - -final _navigatorKey = GlobalKey(); -final _pushService = MattermostPushPlugin.instance; -final _launcher = WorkspaceLauncher(); - -Future _applyFullscreenMode() async { - await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarDividerColor: Colors.transparent, - ), - ); -} - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - await _applyFullscreenMode(); - - await Firebase.initializeApp(); - - await _pushService.initialize(); - - // Mattermost 자동 로그인 + FCM 토큰 서버 등록 - final authService = MattermostAuthService(_pushService); - try { - await authService.autoLoginAndRegister(); - } catch (e) { - print('[Main] Mattermost auto-login failed: $e'); - } - - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'nomadcode-app', - navigatorKey: _navigatorKey, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, - brightness: Brightness.dark, - ), - useMaterial3: true, - ), - home: const MyHomePage(title: 'nomadcode-app'), - builder: (context, child) { - _pushService.onNavigateToChannel = (serverUrl, channelId) { - print('[Nav] Navigate to channel: $channelId on $serverUrl'); - }; - _pushService.onNavigateToThread = (serverUrl, rootId) { - print('[Nav] Navigate to thread: $rootId on $serverUrl'); - }; - return child!; - }, - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - StreamSubscription? _notifSub; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _applyFullscreenMode(); - }); - _notifSub = _pushService.onNotification.listen((data) { - if (data['type'] != 'message') return; - final message = data['message'] as String? ?? ''; - final channel = data['channel_name'] as String? ?? ''; - final sender = data['sender_name'] as String? ?? ''; - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - sender.isNotEmpty ? '[$channel] $sender: $message' : message, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - duration: const Duration(seconds: 4), - behavior: SnackBarBehavior.floating, - ), - ); - }); - } - - @override - void dispose() { - _notifSub?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return WorkspaceHomePage( - onLaunchCodeServer: (project) async { - return _launcher.openCodeServer(project); - }, - ); - } -} diff --git a/apps/mobile/push-notification-todo.md b/apps/mobile/push-notification-todo.md deleted file mode 100644 index 5c49bea..0000000 --- a/apps/mobile/push-notification-todo.md +++ /dev/null @@ -1,34 +0,0 @@ -# Push Notification Integration Handoff & Migration Notes - -> [!IMPORTANT] -> **This document is preserved for historical reference only.** -> As of the `mattermost-push-plugin-extraction` milestone, all native FCM services, Room DB structures, JJWT signature verification, inline reply/dismiss receivers, notification builders, and ACK request handling have been successfully extracted into the local Flutter plugin repository: `../mattermost-push-plugin`. -> -> **Do NOT edit native push files directly inside this host app repository (`apps/mobile/android`).** All native push logic and platform services now reside in and must be maintained within the plugin repository. - -## Current Responsibilities in Host App (`apps/mobile`) - -The host application acts strictly as a consumer of the `mattermost_push_plugin` package. What remains in `apps/mobile` regarding push notifications are: - -1. **Firebase Manifest & Metadata**: - - `google-services.json` inside `apps/mobile/android/app/` - - Initializing Firebase inside `lib/main.dart` -2. **Credential / Secret Plumbing**: - - Storing session authorization tokens via `MattermostPushPlugin.instance.setAuthToken()` on login and clearing them on logout. - - Registering Mattermost server public signing keys via `MattermostPushPlugin.instance.setSigningKey()`. -3. **App Navigation Routings**: - - Handling user taps on notifications by implementing `onNavigateToChannel` and `onNavigateToThread` routing callbacks on `MattermostPushPlugin.instance`. - -## Legacy Work Documentation (Reference) - -All native Kotlin/Java files, Room DB configurations, and broadcast receivers documented below are now managed within the `mattermost_push_plugin` repository: - -- `MattermostFirebaseMessagingService.kt` -- `NotificationReplyBroadcastReceiver.java` -- `NotificationDismissService.java` -- `ReceiptDelivery.java` -- `PushNotificationDataHelper.kt` -- `helpers/db/MattermostDatabase.kt` & `GlobalDatabase.kt` - -For local modifications, testing guidelines, and detailed API specs, please refer to the plugin's README: -See `../mattermost-push-plugin/README.md` from the NomadCode repository root. diff --git a/bin/build b/bin/build index 1ba17bd..dbe5dea 100755 --- a/bin/build +++ b/bin/build @@ -11,10 +11,10 @@ if [ -f "$root/services/core/go.mod" ]; then fi fi -if [ -f "$root/apps/mobile/pubspec.yaml" ]; then +if [ -f "$root/apps/client/pubspec.yaml" ]; then if command -v flutter >/dev/null 2>&1; then - (cd "$root/apps/mobile" && flutter build web) + (cd "$root/apps/client" && flutter build web) else - echo "skip apps/mobile: flutter not found" + echo "skip apps/client: flutter not found" fi fi diff --git a/bin/dev b/bin/dev index 456318b..0a1a79e 100755 --- a/bin/dev +++ b/bin/dev @@ -7,7 +7,7 @@ cat </dev/null 2>&1; then - (cd "$root/apps/mobile" && flutter analyze --no-fatal-infos) + (cd "$root/apps/client" && flutter analyze --no-fatal-infos) else - echo "skip apps/mobile: flutter not found" + echo "skip apps/client: flutter not found" fi fi diff --git a/bin/test b/bin/test index 7eb6f9c..b621be3 100755 --- a/bin/test +++ b/bin/test @@ -11,10 +11,10 @@ if [ -f "$root/services/core/go.mod" ]; then fi fi -if [ -f "$root/apps/mobile/pubspec.yaml" ]; then +if [ -f "$root/apps/client/pubspec.yaml" ]; then if command -v flutter >/dev/null 2>&1; then - (cd "$root/apps/mobile" && flutter test) + (cd "$root/apps/client" && flutter test) else - echo "skip apps/mobile: flutter not found" + echo "skip apps/client: flutter not found" fi fi diff --git a/docs/monorepo.md b/docs/monorepo.md index ca9afac..8d8dad3 100644 --- a/docs/monorepo.md +++ b/docs/monorepo.md @@ -3,9 +3,10 @@ The workspace is organized by runtime boundary rather than by task. - `services/core` owns orchestration, persistence, scheduling, and external model/service adapters. -- `apps/mobile` owns the Flutter-first product UI across mobile, desktop, and optional web targets. +- `apps/client` owns the Flutter-first product UI across mobile, desktop, and optional web targets. - `apps/web` was the previous React/Vite console scaffold, which has been removed as part of the Flutter-first client consolidation. - `packages/contracts` owns schemas shared across clients and services. - `agent-ops` owns root agent routing, domain rules, and repeatable task workflows. +- `docs/test-environments.md` records shared verification environments such as the remote Android SDK runner. Keep feature work scoped by domain, but land cross-boundary changes in one branch when API contracts, generated clients, and UI behavior must move together. diff --git a/docs/test-environments.md b/docs/test-environments.md new file mode 100644 index 0000000..fa3e19e --- /dev/null +++ b/docs/test-environments.md @@ -0,0 +1,70 @@ +# Shared Test Environments + +NomadCode milestones can reference this document when a verification step needs a shared machine or platform SDK that is not expected to exist in every agent workspace. + +## Android SDK Remote Runner + +Use the Toki Labs host for Android SDK dependent checks. + +### Access + +```bash +ssh toki@toki-labs.com +``` + +After connecting, use the NomadCode checkout prepared on that host. If the checkout is under the same workspace convention as local agent workspaces, start here: + +```bash +cd /config/workspace/nomadcode +``` + +If that path is absent, use the active NomadCode checkout on the host and make sure it is on the branch or commit that contains the change being verified. + +### Environment Check + +Run these before Android-specific verification: + +```bash +flutter doctor -v +echo "$ANDROID_HOME" +echo "$ANDROID_SDK_ROOT" +flutter devices +``` + +`flutter doctor -v` should show an Android toolchain with an installed SDK. `flutter devices` is allowed to show no physical device when the check only needs a build. A device or emulator is required only for an interactive smoke run. + +### Standard Checks + +For Android SDK dependent client milestones, run from the Flutter host: + +```bash +cd apps/client +flutter pub get +flutter build apk --debug +``` + +`flutter build apk --debug` is the default Android SDK validation for roadmap completion when no device smoke is explicitly required. It verifies Gradle, Android SDK, plugin registration, Firebase Android config wiring, and Dart bootstrap compilation for Android. + +Generic Dart/widget checks such as `flutter test` and `flutter analyze --no-fatal-infos` may be run locally or on the remote host when the milestone explicitly asks for them. Do not treat the remote host as the source of truth for non-Android SDK checks unless its Flutter SDK version is part of the milestone evidence. + +### Optional Device Smoke + +Run this only when the milestone asks for an Android runtime smoke or notification/device behavior: + +```bash +cd apps/client +flutter devices +flutter run -d +``` + +For Mattermost push work, confirm the app boots, Firebase initialization does not crash, and notification navigation callbacks remain observable. Do not record tokens, credentials, signing keys, or private server values in roadmap or docs. + +### Roadmap Evidence + +When a milestone depends on this environment, leave only a short evidence note in the milestone, for example: + +```text +Android SDK check: see docs/test-environments.md#android-sdk-remote-runner; flutter build apk --debug passed on toki@toki-labs.com. +``` + +Keep detailed reusable environment instructions in this document, not in individual milestone files. diff --git a/packages/contracts/notes/flutter-core-api-candidates.md b/packages/contracts/notes/flutter-core-api-candidates.md index 79c7cea..9dc5d0b 100644 --- a/packages/contracts/notes/flutter-core-api-candidates.md +++ b/packages/contracts/notes/flutter-core-api-candidates.md @@ -1,7 +1,7 @@ # Flutter/Core API Contract Candidates 이 문서는 source schema가 아니라 Flutter-first 전환 중 공유 계약 후보를 모으는 compatibility note다. -실제 Go core 구현(`services/core`)과 향후 작성될 Flutter 클라이언트(`apps/mobile`) 간의 인터페이스 정합성을 보장하기 위한 임시 계약 후보이며, 단일 source-of-truth 스키마가 정의되기 전까지의 과도기적 스펙을 정의한다. +실제 Go core 구현(`services/core`)과 향후 작성될 Flutter 클라이언트(`apps/client`) 간의 인터페이스 정합성을 보장하기 위한 임시 계약 후보이며, 단일 source-of-truth 스키마가 정의되기 전까지의 과도기적 스펙을 정의한다. ## API 안정성 레벨 정의 - **안정 (Stable)**: 인터페이스와 데이터 포맷이 고정되어 있어 즉시 연동에 사용할 수 있음. @@ -194,3 +194,33 @@ - `provider`: 연동 플랫폼 식별자 (예: `plane`, `github`) - `external_project_id`: 연동 플랫폼 측 프로젝트 ID - `status`: 프로젝트 상태 (예: `active`, `paused`) + +--- + +## 5. 클라이언트 Integration 설정 계약 후보 + +> [!IMPORTANT] +> 이 절은 `apps/client`의 integration boundary에서 host/plugin/transport 사이에 교환되는 설정값을 후보로 모은 것이다. 실제 source schema는 소비하는 core/client 경계가 안정화된 뒤로 미룬다. +> 관련 코드 경계: `apps/client/lib/src/integrations/proto_socket/` 와 `apps/client/lib/src/integrations/mattermost/`. + +### 5.1 proto-socket Endpoint 설정 후보 +- **설명**: `apps/client`가 proto-socket transport에 의존할 때 사용하는 연결/하트비트 설정. 현재 구현은 `proto_socket` Dart 패키지(local path)를 끌어 쓰며 bootstrap에서 자동 연결하지 않는다. +- **후보 필드**: + - `host`: 대상 서버 hostname + - `port`: TCP/WebSocket 포트 (기본은 `secure` 값에 따라 443 또는 80) + - `secure`: TLS 사용 여부 (boolean) + - `path`: WebSocket path (기본 `/`) + - `heartbeat_interval_seconds`: heartbeat 송신 주기 (기본 30) + - `heartbeat_wait_seconds`: heartbeat 응답 대기 한계 (기본 60) + - `enabled`: bootstrap 시 자동 연결 활성화 여부 (기본 false) + +### 5.2 Mattermost Push Host 책임 경계 후보 +- **설명**: Mattermost push 통합에서 `apps/client` host가 plugin adapter에게 위임/공급해야 하는 데이터와 콜백. plugin adapter는 platform 측 plugin singleton에 격리되어 있고, 그 외 코드는 모두 `MattermostPushClient` 인터페이스에만 의존한다. +- **호스트 소유 필드/콜백**: + - `server_url`: Mattermost 서버 base URL (auth 핸드오프, signing key 저장에 함께 사용) + - `auth_token`: 로그인 응답에서 받은 인증 토큰 (plugin의 `setAuthToken`으로 전달) + - `signing_key`: 서버 config의 `AsymmetricSigningPublicKey` (plugin의 `setSigningKey`로 전달) + - `device_token`: FCM 디바이스 토큰 (plugin의 `getDeviceToken` 및 `onDeviceTokenReady` 콜백으로 동기화) + - `on_navigate_to_channel(server_url, channel_id)`: notification open → 채널 라우팅 콜백 + - `on_navigate_to_thread(server_url, root_id)`: notification open → CRT 스레드 라우팅 콜백 + - `notification_stream`: app-level UI(snackbar 등) 소비를 위한 broadcast stream