TokenResponse rework

This commit is contained in:
2025-10-05 14:46:02 +03:00
parent 556b7f7c7d
commit 9286e041da
12 changed files with 32 additions and 167 deletions

View File

@@ -18,7 +18,7 @@
</META> </META>
<ROLE_DEFINITION> <ROLE_DEFINITION>
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION> <SPECIALIZATION>При исполнении этой роли, я действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION>
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL> <CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL>
</ROLE_DEFINITION> </ROLE_DEFINITION>

View File

@@ -30,21 +30,6 @@
</PHILOSOPHY_PRINCIPLE> </PHILOSOPHY_PRINCIPLE>
</CORE_PHILOSOPHY> </CORE_PHILOSOPHY>
<ISSUE_BODY_FORMAT name="Linting_Task_Specification">
<DESCRIPTION>Задачи для этой роли должны содержать XML-блок, определяющий режим работы.</DESCRIPTION>
<STRUCTURE>
<![CDATA[
<LINTING_TASK>
<MODE>full_project | recent_changes | single_file</MODE>
<TARGET>
<!-- Для recent_changes: commit range, e.g., HEAD~1..HEAD -->
<!-- Для single_file: path/to/file.kt -->
</TARGET>
</LINTING_TASK>
]]>
</STRUCTURE>
</ISSUE_BODY_FORMAT>
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle"> <MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task"> <WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/> <LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/>

View File

@@ -28,7 +28,9 @@ data class TokenResponseDto(
*/ */
fun TokenResponseDto.toDomain(): TokenResponse { fun TokenResponseDto.toDomain(): TokenResponse {
return TokenResponse( return TokenResponse(
token = this.token token = this.token,
attachmentToken = this.attachmentToken,
expiresAt = this.expiresAt
) )
} }
// [END_ENTITY: Function('toDomain')] // [END_ENTITY: Function('toDomain')]

View File

@@ -4,9 +4,6 @@
package com.homebox.lens.data.api.model package com.homebox.lens.data.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [IMPORTS] // [IMPORTS]
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View File

@@ -20,6 +20,7 @@ import com.homebox.lens.domain.model.LocationOut as DomainLocationOut
import com.homebox.lens.domain.model.LocationOutCount as DomainLocationOutCount import com.homebox.lens.domain.model.LocationOutCount as DomainLocationOutCount
import com.homebox.lens.domain.model.MaintenanceEntry as DomainMaintenanceEntry import com.homebox.lens.domain.model.MaintenanceEntry as DomainMaintenanceEntry
import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult
import com.homebox.lens.domain.model.TokenResponse as DomainTokenResponse
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('ItemOutDto.toDomain')] // [ENTITY: Function('ItemOutDto.toDomain')]
@@ -265,4 +266,5 @@ fun LabelSummaryDto.toDomain(): DomainLabelSummary {
} }
// [END_ENTITY: Function('LabelSummaryDto.toDomain')] // [END_ENTITY: Function('LabelSummaryDto.toDomain')]
// [END_FILE_DtoToDomain.kt] // [END_FILE_DtoToDomain.kt]

View File

@@ -8,8 +8,9 @@ package com.homebox.lens.data.repository
import android.content.SharedPreferences import android.content.SharedPreferences
import com.homebox.lens.data.api.HomeboxApiService import com.homebox.lens.data.api.HomeboxApiService
import com.homebox.lens.data.api.dto.LoginFormDto import com.homebox.lens.data.api.dto.LoginFormDto
import com.homebox.lens.data.api.mapper.toDomain import com.homebox.lens.data.api.dto.toDomain
import com.homebox.lens.domain.model.Credentials import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.model.Result
import com.homebox.lens.domain.model.TokenResponse import com.homebox.lens.domain.model.TokenResponse
import com.homebox.lens.domain.repository.AuthRepository import com.homebox.lens.domain.repository.AuthRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -56,7 +57,7 @@ class AuthRepositoryImpl @Inject constructor(
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." } require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
runCatching { try {
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}") Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
val tempApiService = Retrofit.Builder() val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl) .baseUrl(credentials.serverUrl)
@@ -70,7 +71,10 @@ class AuthRepositoryImpl @Inject constructor(
val tokenResponseDto = tempApiService.login(loginForm) val tokenResponseDto = tempApiService.login(loginForm)
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.") Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
tokenResponseDto.toDomain() Result.Success(tokenResponseDto.toDomain())
} catch (e: Exception) {
Timber.e(e, "[ERROR][FAILURE][login_failed] Login failed.")
Result.Error(e)
} }
} }
} }

View File

@@ -8,9 +8,15 @@ package com.homebox.lens.domain.model
/** /**
* @summary Data model representing a response from the server with an authentication token. * @summary Data model representing a response from the server with an authentication token.
* @param token A string containing a JWT or other access token. * @param token A string containing a JWT or other access token.
* @param attachmentToken A token for accessing attachments.
* @param expiresAt The expiration date of the token.
* @invariant `token` must not be blank. * @invariant `token` must not be blank.
*/ */
data class TokenResponse(val token: String) { data class TokenResponse(
val token: String,
val attachmentToken: String,
val expiresAt: String
) {
init { init {
require(token.isNotBlank()) { "Token cannot be blank." } require(token.isNotBlank()) { "Token cannot be blank." }
} }

View File

@@ -31,17 +31,19 @@ class LoginUseCase @Inject constructor(
"Server URL and username must not be blank." "Server URL and username must not be blank."
} }
val loginResult: com.homebox.lens.domain.model.Result<TokenResponse> = authRepository.login(credentials) return try {
when (val loginResult = authRepository.login(credentials)) {
return loginResult.fold( is com.homebox.lens.domain.model.Result.Success -> {
onSuccess = { authRepository.saveToken(loginResult.data.token)
authRepository.saveToken(it.token) Result.success(Unit)
com.homebox.lens.domain.model.Result.Success(Unit) }
}, is com.homebox.lens.domain.model.Result.Error -> {
onFailure = { Result.failure(loginResult.exception)
com.homebox.lens.domain.model.Result.Error(it as Exception) }
} }
) } catch (e: Exception) {
Result.failure(e)
}
} }
// [END_ENTITY: Function('invoke')] // [END_ENTITY: Function('invoke')]
} }

View File

@@ -1,17 +0,0 @@
<![CDATA[
<WORK_ORDER>
<METADATA>
<ID>enrichment-task-001</ID>
<TITLE>Perform initial semantic enrichment</TITLE>
<ROLE_TARGET>agent-enrichment</ROLE_TARGET>
<TYPE>type::enrichment</TYPE>
<STATUS>status::pending</STATUS>
</METADATA>
<TASK_BODY>
<ENRICHMENT_TASK>
<SCOPE>full_project</SCOPE>
<TARGET></TARGET>
</ENRICHMENT_TASK>
</TASK_BODY>
</WORK_ORDER>
]]>

View File

@@ -1,6 +0,0 @@
<![CDATA[
<ENRICHMENT_TASK>
<SCOPE>directory</SCOPE>
<TARGET>app/src/main/java/com/homebox/lens/ui</TARGET>
</ENRICHMENT_TASK>
]]>

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<WORK_ORDER>
<META>
<ID>WO-ITEMEDIT-FIX</ID>
<TITLE>[ARCHITECT -> DEV] Исправление выбора локации и меток на экране ItemEdit</TITLE>
<DESCRIPTION>В текущей реализации на экране редактирования/создания элемента (ItemEditScreen) поля "Location" и "Labels" неактивны. Необходимо реализовать функционал выбора значения для этих полей из списка доступных.</DESCRIPTION>
<CREATED_BY>architect-agent</CREATED_BY>
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
<STATUS>pending</STATUS>
</META>
<TASK_BREAKDOWN>
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt">
<ACTION>Загрузка списков локаций и меток.</ACTION>
<DETAILS>
1. Внедрите `GetAllLocationsUseCase` и `GetAllLabelsUseCase` в `ItemEditViewModel`.
2. Обновите `ItemEditUiState`, добавив два новых поля: `val allLocations: List<Location> = emptyList()` и `val allLabels: List<Label> = emptyList()`.
3. В функции `loadItem`, после загрузки основной информации о товаре, вызовите `getAllLocationsUseCase` и `getAllLabelsUseCase` и обновите `uiState` полученными списками.
4. Добавьте публичные методы `updateLocation(location: Location)` и `updateLabels(labels: List<Label>)` для обновления `item` в `uiState`.
</DETAILS>
</STEP>
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
<ACTION>Реализация UI для выбора локации.</ACTION>
<DETAILS>
1. Замените `OutlinedTextField` для локации на `ExposedDropdownMenuBox`.
2. В качестве `dropdownMenu` используйте `DropdownMenuItem` для каждого элемента из `uiState.allLocations`.
3. При выборе элемента из списка вызывайте `viewModel.updateLocation(selectedLocation)`.
4. В `ExposedDropdownMenuBox` должно отображаться `item.location?.name`.
</DETAILS>
</STEP>
<STEP n="3" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
<ACTION>Реализация UI для выбора меток (множественный выбор).</ACTION>
<DETAILS>
1. Поле для меток `Labels` должно оставаться `OutlinedTextField` (read-only), но `onClick` по нему должен открывать диалоговое окно (`AlertDialog`).
2. В `AlertDialog` отобразите список всех меток (`uiState.allLabels`) с `Checkbox`'ами.
3. Состояние `Checkbox`'ов должно соответствовать списку `item.labels`.
4. При нажатии на "OK" в диалоге, вызывайте `viewModel.updateLabels(selectedLabels)`.
</DETAILS>
</STEP>
</TASK_BREAKDOWN>
<ACCEPTANCE_CRITERIA>
<CRITERION>При нажатии на поле "Location" открывается выпадающий список со всеми локациями.</CRITERION>
<CRITERION>Выбранная локация отображается в поле и сохраняется вместе с элементом.</CRITERION>
<CRITERION>При нажатии на поле "Labels" открывается диалоговое окно со списком всех меток и чекбоксами.</CRITERION>
<CRITERION>Выбранные метки отображаются в поле и сохраняются вместе с элементом.</CRITERION>
</ACCEPTANCE_CRITERIA>
</WORK_ORDER>

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<WORK_ORDER>
<META>
<ID>WO-LOGIN-REFACTOR</ID>
<TITLE>[ARCHITECT -> DEV] Рефакторинг экрана входа и логики первого запуска</TITLE>
<DESCRIPTION>Цель этой задачи - изменить логику запуска приложения. Экран входа (SetupScreen) должен появляться только при первом запуске, когда учетные данные еще не сохранены. В последующие запуски пользователь должен сразу попадать на главный экран (Dashboard). Также необходимо улучшить визуальное оформление экрана входа.</DESCRIPTION>
<CREATED_BY>architect-agent</CREATED_BY>
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
<STATUS>pending</STATUS>
</META>
<TASK_BREAKDOWN>
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt">
<ACTION>Добавить public-метод для синхронной проверки наличия учетных данных.</ACTION>
<DETAILS>
Добавьте в класс `SetupViewModel` новый метод `fun areCredentialsSaved(): Boolean`.
Этот метод должен синхронно проверять, сохранены ли учетные данные в `CredentialsRepository`.
Текущая реализация `getCredentials()` асинхронна, что не подходит для быстрой проверки в `NavGraph`.
Вам может потребоваться изменить `CredentialsRepository` для поддержки синхронной проверки (например, используя `SharedPreferences` напрямую).
</DETAILS>
</STEP>
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/splash/SplashScreen.kt">
<ACTION>Создать новый `SplashScreen`.</ACTION>
<DETAILS>
Создайте новый Composable-экран `SplashScreen.kt`.
Этот экран будет новой точкой входа в `NavGraph`.
Он будет использовать `SetupViewModel` для вызова `areCredentialsSaved()` и, в зависимости от результата, немедленно навигироваться либо на `Screen.Setup`, либо на `Screen.Dashboard`.
Пока идет проверка, на экране должен отображаться `CircularProgressIndicator`.
</DETAILS>
</STEP>
<STEP n="3" file="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt">
<ACTION>Обновить `NavGraph` для использования `SplashScreen`.</ACTION>
<DETAILS>
Измените `startDestination` в `NavHost` на `Screen.Splash.route`.
Добавьте `composable` для `SplashScreen`.
В `SplashScreen` вызовите `navController.navigate` с очисткой бэкстека (`popUpTo(Screen.Splash.route) { inclusive = true }`), чтобы пользователь не мог вернуться на сплэш-экран.
</DETAILS>
</STEP>
<STEP n="4" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt">
<ACTION>Улучшить UI экрана `SetupScreen`.</ACTION>
<DETAILS>
Текущий UI слишком прост. Добавьте заголовок, иконку приложения, и более приятное расположение элементов.
Используйте `Card` для группировки полей ввода. Добавьте `Spacer` для лучшего отступа.
Кнопку "Connect" сделайте более заметной.
</DETAILS>
</STEP>
</TASK_BREAKDOWN>
<ACCEPTANCE_CRITERIA>
<CRITERION>При первом запуске приложения открывается `SetupScreen`.</CRITERION>
<CRITERION>После успешного ввода данных и входа, при последующих перезапусках приложения открывается `DashboardScreen`, минуя `SetupScreen`.</CRITERION>
<CRITERION>`SetupScreen` имеет улучшенный и более привлекательный дизайн.</CRITERION>
</ACCEPTANCE_CRITERIA>
</WORK_ORDER>