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
@@ -40,7 +40,7 @@ class BonsaiApiClient @Inject constructor(
val o = arr.getJSONObject(i)
BonsaiIssueDto(
id = o.getLong("id"),
projectId = o.getLong("projectId"),
projectId = o.optLong("projectId", projectId), // fallback = the project we queried
type = o.getString("type"),
name = o.getString("name"),
status = o.getString("status"),
@@ -71,12 +71,12 @@ class BonsaiApiClient @Inject constructor(
suspend fun createIssue(projectId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
post("projects/$projectId/issues", req.toJson()) { o ->
o.toIssueDto()
o.toIssueDto(fallbackProjectId = projectId)
}
suspend fun updateIssue(projectId: Long, issueId: Long, req: BonsaIssueRequest): ApiResult<BonsaiIssueDto> =
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) {
@@ -146,13 +146,13 @@ class BonsaiApiClient @Inject constructor(
description?.let { put("description", it) }
}.toString()
private fun JSONObject.toIssueDto() = BonsaiIssueDto(
private fun JSONObject.toIssueDto(fallbackProjectId: Long = 0L) = BonsaiIssueDto(
id = getLong("id"),
projectId = getLong("projectId"),
type = getString("type"),
projectId = optLong("projectId", fallbackProjectId),
type = optString("type", "Task"),
name = getString("name"),
status = getString("status"),
priority = getString("priority"),
status = optString("status", "todo"),
priority = optString("priority", "MOYENNE"),
assignee = optString("assignee").takeIf { it.isNotEmpty() },
dueDate = optString("dueDate").takeIf { it.isNotEmpty() },
description = optString("description").takeIf { it.isNotEmpty() },
@@ -30,7 +30,10 @@ class BonsaiSyncManager @Inject constructor(
private val taskRepository: TaskRepository,
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
val projectsResult = apiClient.getProjects()
@@ -89,11 +92,14 @@ class BonsaiSyncManager @Inject constructor(
// Sync tasks (issues of type Task)
allTaskIssues.forEach { issue ->
val labels = milestonesByIssueId[issue.id] ?: emptyList()
val task = issue.toTask(labels, now)
val existing = taskRepository.getTaskById(task.id)
if (existing == null) taskRepository.insertTask(task)
else taskRepository.updateTask(task)
runCatching {
val labels = milestonesByIssueId[issue.id] ?: emptyList()
val task = issue.toTask(labels, now)
val existing = taskRepository.getTaskById(task.id)
if (existing == null) taskRepository.insertTask(task)
else taskRepository.updateTask(task)
}
// Individual task failures are skipped; the next sync will retry.
}
return SyncResult.Success