fix: crash SQLiteConstraintException lors de la sync Bonsai

- BonsaiApiClient : optLong/optString avec fallback pour éviter JSONException
  si l'API omet projectId, type, status ou priority dans la réponse
- BonsaiSyncManager : enveloppe sync() dans runCatching pour ne jamais
  propager d'exception non gérée ; chaque insert de tâche est aussi isolé
  pour que les erreurs individuelles ne bloquent pas toute la sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 09:54:10 +02:00
parent 221cf4f80d
commit 4d59f371ac
3 changed files with 21 additions and 15 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.0.11" versionName = "0.0.12"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -40,7 +40,7 @@ class BonsaiApiClient @Inject constructor(
val o = arr.getJSONObject(i) val o = arr.getJSONObject(i)
BonsaiIssueDto( BonsaiIssueDto(
id = o.getLong("id"), id = o.getLong("id"),
projectId = o.getLong("projectId"), projectId = o.optLong("projectId", projectId), // fallback = the project we queried
type = o.getString("type"), type = o.getString("type"),
name = o.getString("name"), name = o.getString("name"),
status = o.getString("status"), status = o.getString("status"),
@@ -71,12 +71,12 @@ class BonsaiApiClient @Inject constructor(
suspend fun createIssue(projectId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> = suspend fun createIssue(projectId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
post("projects/$projectId/issues", req.toJson()) { o -> post("projects/$projectId/issues", req.toJson()) { o ->
o.toIssueDto() o.toIssueDto(fallbackProjectId = projectId)
} }
suspend fun updateIssue(projectId: Long, issueId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> = suspend fun updateIssue(projectId: Long, issueId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
put("projects/$projectId/issues/$issueId", req.toJson()) { o -> put("projects/$projectId/issues/$issueId", req.toJson()) { o ->
o.toIssueDto() o.toIssueDto(fallbackProjectId = projectId)
} }
suspend fun deleteIssue(projectId: Long, issueId: Long): ApiResult<Unit> = withContext(Dispatchers.IO) { suspend fun deleteIssue(projectId: Long, issueId: Long): ApiResult<Unit> = withContext(Dispatchers.IO) {
@@ -146,13 +146,13 @@ class BonsaiApiClient @Inject constructor(
description?.let { put("description", it) } description?.let { put("description", it) }
}.toString() }.toString()
private fun JSONObject.toIssueDto() = BonsaiIssueDto( private fun JSONObject.toIssueDto(fallbackProjectId: Long = 0L) = BonsaiIssueDto(
id = getLong("id"), id = getLong("id"),
projectId = getLong("projectId"), projectId = optLong("projectId", fallbackProjectId),
type = getString("type"), type = optString("type", "Task"),
name = getString("name"), name = getString("name"),
status = getString("status"), status = optString("status", "todo"),
priority = getString("priority"), priority = optString("priority", "MOYENNE"),
assignee = optString("assignee").takeIf { it.isNotEmpty() }, assignee = optString("assignee").takeIf { it.isNotEmpty() },
dueDate = optString("dueDate").takeIf { it.isNotEmpty() }, dueDate = optString("dueDate").takeIf { it.isNotEmpty() },
description = optString("description").takeIf { it.isNotEmpty() }, description = optString("description").takeIf { it.isNotEmpty() },
@@ -30,7 +30,10 @@ class BonsaiSyncManager @Inject constructor(
private val taskRepository: TaskRepository, private val taskRepository: TaskRepository,
private val labelRepository: LabelRepository, private val labelRepository: LabelRepository,
) { ) {
suspend fun sync(): SyncResult { suspend fun sync(): SyncResult = runCatching { doSync() }
.getOrElse { SyncResult.Failure(it.message ?: "Erreur interne") }
private suspend fun doSync(): SyncResult {
if (!authManager.isLoggedIn) return SyncResult.NotLoggedIn if (!authManager.isLoggedIn) return SyncResult.NotLoggedIn
val projectsResult = apiClient.getProjects() val projectsResult = apiClient.getProjects()
@@ -89,12 +92,15 @@ class BonsaiSyncManager @Inject constructor(
// Sync tasks (issues of type Task) // Sync tasks (issues of type Task)
allTaskIssues.forEach { issue -> allTaskIssues.forEach { issue ->
runCatching {
val labels = milestonesByIssueId[issue.id] ?: emptyList() val labels = milestonesByIssueId[issue.id] ?: emptyList()
val task = issue.toTask(labels, now) val task = issue.toTask(labels, now)
val existing = taskRepository.getTaskById(task.id) val existing = taskRepository.getTaskById(task.id)
if (existing == null) taskRepository.insertTask(task) if (existing == null) taskRepository.insertTask(task)
else taskRepository.updateTask(task) else taskRepository.updateTask(task)
} }
// Individual task failures are skipped; the next sync will retry.
}
return SyncResult.Success return SyncResult.Success
} }