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

View File

@@ -30,21 +30,6 @@
</PHILOSOPHY_PRINCIPLE>
</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">
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
<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 {
return TokenResponse(
token = this.token
token = this.token,
attachmentToken = this.attachmentToken,
expiresAt = this.expiresAt
)
}
// [END_ENTITY: Function('toDomain')]

View File

@@ -4,9 +4,6 @@
package com.homebox.lens.data.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// [IMPORTS]
import com.squareup.moshi.Json
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.MaintenanceEntry as DomainMaintenanceEntry
import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult
import com.homebox.lens.domain.model.TokenResponse as DomainTokenResponse
// [END_IMPORTS]
// [ENTITY: Function('ItemOutDto.toDomain')]
@@ -265,4 +266,5 @@ fun LabelSummaryDto.toDomain(): DomainLabelSummary {
}
// [END_ENTITY: Function('LabelSummaryDto.toDomain')]
// [END_FILE_DtoToDomain.kt]

View File

@@ -8,8 +8,9 @@ package com.homebox.lens.data.repository
import android.content.SharedPreferences
import com.homebox.lens.data.api.HomeboxApiService
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.Result
import com.homebox.lens.domain.model.TokenResponse
import com.homebox.lens.domain.repository.AuthRepository
import kotlinx.coroutines.Dispatchers
@@ -56,7 +57,7 @@ class AuthRepositoryImpl @Inject constructor(
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
return withContext(Dispatchers.IO) {
runCatching {
try {
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
val tempApiService = Retrofit.Builder()
.baseUrl(credentials.serverUrl)
@@ -70,7 +71,10 @@ class AuthRepositoryImpl @Inject constructor(
val tokenResponseDto = tempApiService.login(loginForm)
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.
* @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.
*/
data class TokenResponse(val token: String) {
data class TokenResponse(
val token: String,
val attachmentToken: String,
val expiresAt: String
) {
init {
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."
}
val loginResult: com.homebox.lens.domain.model.Result<TokenResponse> = authRepository.login(credentials)
return loginResult.fold(
onSuccess = {
authRepository.saveToken(it.token)
com.homebox.lens.domain.model.Result.Success(Unit)
},
onFailure = {
com.homebox.lens.domain.model.Result.Error(it as Exception)
return try {
when (val loginResult = authRepository.login(credentials)) {
is com.homebox.lens.domain.model.Result.Success -> {
authRepository.saveToken(loginResult.data.token)
Result.success(Unit)
}
is com.homebox.lens.domain.model.Result.Error -> {
Result.failure(loginResult.exception)
}
}
} catch (e: Exception) {
Result.failure(e)
}
)
}
// [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>