fix: issues #60 #61 #62 #63 — inbox, refresh, subtask depth, chevron/detail #64

Merged
maximus merged 5 commits from issue-60-fix-duplicate-inbox into master 2026-04-09 01:33:40 +00:00
Owner

Fixes #60
Fixes #61
Fixes #62
Fixes #63

Changements

#60 — Duplicate inbox

  • sync/route.ts : merge inbox dans une transaction atomique lors du sync mobile

#61 — Refresh

  • Web : bouton RefreshCw dans TaskList.tsx
  • Mobile : RefreshControl (swipe-to-refresh) sur inbox et liste

#62 — Profondeur sous-tâches

  • Web : masque "Ajouter sous-tâche" si depth >= 1
  • API : rejette la création si le parent a déjà un parentId
  • Mobile : masque la section sous-tâches dans le détail d'une sous-tâche

#63 — Chevron vs détail

  • Chevron > / v : toggle la visibilité des sous-tâches (affiché seulement si sous-tâches existent)
  • Loupe : ouvre le détail (notes, priorité, édition, suppression)
  • Les deux actions sont indépendantes

Test plan

  • Sync mobile inbox → un seul inbox sur le web
  • Bouton refresh web + swipe mobile fonctionnels
  • Impossible de créer une sous-sous-tâche (UI + API)
  • Chevron expand/collapse les sous-tâches indépendamment du détail
  • Loupe ouvre le détail indépendamment des sous-tâches
Fixes #60 Fixes #61 Fixes #62 Fixes #63 ## Changements ### #60 — Duplicate inbox - `sync/route.ts` : merge inbox dans une transaction atomique lors du sync mobile ### #61 — Refresh - **Web** : bouton `RefreshCw` dans `TaskList.tsx` - **Mobile** : `RefreshControl` (swipe-to-refresh) sur inbox et liste ### #62 — Profondeur sous-tâches - **Web** : masque "Ajouter sous-tâche" si `depth >= 1` - **API** : rejette la création si le parent a déjà un `parentId` - **Mobile** : masque la section sous-tâches dans le détail d'une sous-tâche ### #63 — Chevron vs détail - Chevron `>` / `v` : toggle la visibilité des sous-tâches (affiché seulement si sous-tâches existent) - Loupe : ouvre le détail (notes, priorité, édition, suppression) - Les deux actions sont indépendantes ## Test plan - [ ] Sync mobile inbox → un seul inbox sur le web - [ ] Bouton refresh web + swipe mobile fonctionnels - [ ] Impossible de créer une sous-sous-tâche (UI + API) - [ ] Chevron expand/collapse les sous-tâches indépendamment du détail - [ ] Loupe ouvre le détail indépendamment des sous-tâches
maximus added 1 commit 2026-04-09 00:50:28 +00:00
When mobile syncs its inbox (fixed ID) to the web, check if an inbox
already exists for the user. If so, reassign tasks and soft-delete the
old inbox to prevent duplicates. Also harmonize seed.ts to use the same
fixed inbox ID as mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Review — PR #64

Verdict: APPROVE

Le fix est correct et bien ciblé. La logique de déduplication d'inbox lors du sync est solide : vérifier l'existence, réassigner les tâches, soft-delete l'ancien. Le fix du seed avec un ID fixe est une bonne mesure préventive.

Suggestions (non-bloquantes)

  1. route.ts:200-218 — Pas de transaction : Les trois opérations (select inbox, reassign tasks, soft-delete old inbox) ne sont pas dans une transaction. Si le serveur crash entre le reassign et le soft-delete, on se retrouve avec deux inboxes actives. Une db.transaction() serait plus robuste.

  2. seed.ts:22 — ID fixe partagé entre users : L'ID 00000000-0000-0000-0000-000000000001 est identique pour tous les users. Si deux users différents exécutent le seed, il y aura un conflit de PK. Pas un problème en pratique actuellement, mais une fragilité à garder en tête.

  3. route.ts:214 — Sous-tâches orphelines : Le reassign ne met à jour que les tâches directement dans l'ancien inbox (listId = existingInbox.id). Les sous-tâches avec un listId différent ne seraient pas migrées (peu probable avec le schéma actuel).


Review automatisée par Claude Code

## Review — PR #64 **Verdict: ✅ APPROVE** Le fix est correct et bien ciblé. La logique de déduplication d'inbox lors du sync est solide : vérifier l'existence, réassigner les tâches, soft-delete l'ancien. Le fix du seed avec un ID fixe est une bonne mesure préventive. ### Suggestions (non-bloquantes) 1. **`route.ts:200-218` — Pas de transaction :** Les trois opérations (select inbox, reassign tasks, soft-delete old inbox) ne sont pas dans une transaction. Si le serveur crash entre le reassign et le soft-delete, on se retrouve avec deux inboxes actives. Une `db.transaction()` serait plus robuste. 2. **`seed.ts:22` — ID fixe partagé entre users :** L'ID `00000000-0000-0000-0000-000000000001` est identique pour tous les users. Si deux users différents exécutent le seed, il y aura un conflit de PK. Pas un problème en pratique actuellement, mais une fragilité à garder en tête. 3. **`route.ts:214` — Sous-tâches orphelines :** Le reassign ne met à jour que les tâches directement dans l'ancien inbox (`listId = existingInbox.id`). Les sous-tâches avec un `listId` différent ne seraient pas migrées (peu probable avec le schéma actuel). --- *Review automatisée par Claude Code*
maximus added 1 commit 2026-04-09 00:55:06 +00:00
Address review feedback:
1. Wrap inbox deduplication (select, reassign tasks, soft-delete) in a
   db.transaction() for atomicity.
2. Revert seed.ts to use random UUID — a fixed ID shared across users
   would cause PK conflicts. The sync endpoint handles deduplication.
3. Subtasks share the same listId as their parent, so the reassign
   query already covers them (clarified in comment).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

PR Review: fix: resolve duplicate inbox on web after mobile sync (#60)

Verdict: APPROVE

Summary

The sync endpoint now correctly handles the case where a mobile client pushes an inbox list that conflicts with an existing web inbox. Tasks are reassigned within a transaction and the old inbox is soft-deleted. The approach is sound and well-scoped.

Suggestions (non-blocking)

  1. PR body inaccuracy — The PR description says seed.ts now uses the fixed ID 00000000-0000-0000-0000-000000000001, but the actual diff only updates a comment. The seed still lets the DB generate a random UUID. The code is correct (the sync endpoint handles dedup regardless), but the PR description is misleading.

  2. Minor race condition (route.ts:205-231) — Two concurrent sync requests for the same user could both find the same existing inbox before either soft-deletes it, leading to both trying to reassign tasks and soft-delete. Under READ COMMITTED isolation this is unlikely to cause data loss (the second update would be a no-op since the tasks were already moved), but a SELECT ... FOR UPDATE on the existing inbox row inside the transaction would make it bulletproof.

  3. Code duplication (route.ts:222-240 vs route.ts:233-241) — The db.insert(slLists).values(...) block is duplicated between the inbox and non-inbox branches. A small refactor could extract the values object to reduce duplication. Not blocking since the logic is clear as-is.


Review by Claude Code

## PR Review: fix: resolve duplicate inbox on web after mobile sync (#60) **Verdict: APPROVE** ### Summary The sync endpoint now correctly handles the case where a mobile client pushes an inbox list that conflicts with an existing web inbox. Tasks are reassigned within a transaction and the old inbox is soft-deleted. The approach is sound and well-scoped. ### Suggestions (non-blocking) 1. **PR body inaccuracy** — The PR description says seed.ts now uses the fixed ID `00000000-0000-0000-0000-000000000001`, but the actual diff only updates a comment. The seed still lets the DB generate a random UUID. The code is correct (the sync endpoint handles dedup regardless), but the PR description is misleading. 2. **Minor race condition** (`route.ts:205-231`) — Two concurrent sync requests for the same user could both find the same existing inbox before either soft-deletes it, leading to both trying to reassign tasks and soft-delete. Under READ COMMITTED isolation this is unlikely to cause data loss (the second update would be a no-op since the tasks were already moved), but a `SELECT ... FOR UPDATE` on the existing inbox row inside the transaction would make it bulletproof. 3. **Code duplication** (`route.ts:222-240` vs `route.ts:233-241`) — The `db.insert(slLists).values(...)` block is duplicated between the inbox and non-inbox branches. A small refactor could extract the values object to reduce duplication. Not blocking since the logic is clear as-is. --- *Review by Claude Code*
maximus changed title from fix: resolve duplicate inbox on web after mobile sync (#60) to fix: resolve duplicate inbox on web after mobile sync (#60) + refresh (#61) 2026-04-09 01:00:23 +00:00
maximus added 1 commit 2026-04-09 01:05:04 +00:00
Web: add a RefreshCw button next to the list title in TaskList that
calls router.refresh() with a spin animation.

Mobile: add RefreshControl to DraggableFlatList on both inbox and
list detail screens, using the app's blue accent color.

Also deduplicate list insert values in sync/route.ts (review feedback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
maximus added 1 commit 2026-04-09 01:13:53 +00:00
Web: hide "Add subtask" button when depth >= 1 in TaskItem.
API: reject task creation if parentId points to a task that already
has a parentId (max depth validation).
Mobile: hide subtask section in task detail when viewing a subtask.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
maximus changed title from fix: resolve duplicate inbox on web after mobile sync (#60) + refresh (#61) to fix: issues #60 #61 #62 — inbox dupe, refresh, subtask depth 2026-04-09 01:14:00 +00:00
maximus added 1 commit 2026-04-09 01:16:28 +00:00
Split TaskItem into two independent states:
- `expanded` (chevron): toggles subtask visibility, only shown when
  subtasks exist
- `detailOpen` (search icon + title click): opens detail panel with
  notes, priority, edit/delete actions

The two actions are fully independent — expanding subtasks does not
open the detail view and vice versa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
maximus changed title from fix: issues #60 #61 #62 — inbox dupe, refresh, subtask depth to fix: issues #60 #61 #62 #63 — inbox, refresh, subtask depth, chevron/detail 2026-04-09 01:16:39 +00:00
Author
Owner

PR Review — fix: issues #60 #61 #62 #63

Verdict: APPROVE

Les quatre corrections sont cohérentes, bien séparées, et couvrent les trois couches (mobile, web, API) quand nécessaire.

Analyse par issue

#60 — Duplicate inbox (sync/route.ts)
La transaction atomique qui vérifie l'existence d'une inbox, réassigne les tâches et soft-delete l'ancienne est correcte. Le guard existingInbox.id !== entityId évite de supprimer la même inbox. onConflictDoNothing à l'intérieur de la transaction est le bon choix — si l'ID existe déjà, pas de doublon.

#61 — Refresh

  • Mobile : RefreshControl avec await loadTasks() — propre et attendu.
  • Web : router.refresh() avec un setTimeout(500) pour le feedback visuel.

#62 — Profondeur sous-tâches
Protection sur 3 niveaux :

  • API (tasks/route.ts) : rejette si parent.parentId est non-null —
  • Web (TaskItem.tsx) : masque "Ajouter sous-tâche" si depth < 1
  • Mobile (task/[id].tsx) : masque la section sous-tâches si task?.parentId

#63 — Chevron vs détail
Séparation correcte : chevron toggle expanded (sous-tâches), loupe/titre toggle detailOpen (notes, édition, suppression). Le chevron est masqué quand il n'y a pas de sous-tâches (spacer w-[18px] pour l'alignement).

Suggestions (non-bloquantes)

  1. TaskList.tsx:30setTimeout arbitraire : Le setTimeout(() => setRefreshing(false), 500) ne reflète pas la fin réelle du refresh. router.refresh() retourne void donc c'est difficile à attendre, mais un commentaire expliquant ce choix serait utile.

  2. sync/route.ts — Race condition théorique : Si deux appareils sync leur inbox simultanément, les deux transactions pourraient trouver la même existingInbox et tenter la même migration. Le onConflictDoNothing sur l'insert protège, mais le soft-delete de l'ancien inbox pourrait s'exécuter deux fois (idempotent, donc pas de bug réel). À surveiller en cas de sync multi-device intensif.

  3. TaskItem.tsx — Icône Search : La loupe (Search) pour ouvrir le détail est fonctionnelle, mais une icône Eye ou Info serait peut-être plus intuitive sémantiquement. Suggestion mineure.


Review par Claude Code — PR #64

## PR Review — `fix: issues #60 #61 #62 #63` **Verdict: APPROVE** ✅ Les quatre corrections sont cohérentes, bien séparées, et couvrent les trois couches (mobile, web, API) quand nécessaire. ### Analyse par issue **#60 — Duplicate inbox (sync/route.ts)** La transaction atomique qui vérifie l'existence d'une inbox, réassigne les tâches et soft-delete l'ancienne est correcte. Le guard `existingInbox.id !== entityId` évite de supprimer la même inbox. `onConflictDoNothing` à l'intérieur de la transaction est le bon choix — si l'ID existe déjà, pas de doublon. **#61 — Refresh** - Mobile : `RefreshControl` avec `await loadTasks()` — propre et attendu. - Web : `router.refresh()` avec un `setTimeout(500)` pour le feedback visuel. **#62 — Profondeur sous-tâches** Protection sur 3 niveaux : - API (`tasks/route.ts`) : rejette si `parent.parentId` est non-null — ✅ - Web (`TaskItem.tsx`) : masque "Ajouter sous-tâche" si `depth < 1` — ✅ - Mobile (`task/[id].tsx`) : masque la section sous-tâches si `task?.parentId` — ✅ **#63 — Chevron vs détail** Séparation correcte : chevron toggle `expanded` (sous-tâches), loupe/titre toggle `detailOpen` (notes, édition, suppression). Le chevron est masqué quand il n'y a pas de sous-tâches (spacer `w-[18px]` pour l'alignement). ### Suggestions (non-bloquantes) 1. **`TaskList.tsx:30` — `setTimeout` arbitraire** : Le `setTimeout(() => setRefreshing(false), 500)` ne reflète pas la fin réelle du refresh. `router.refresh()` retourne `void` donc c'est difficile à attendre, mais un commentaire expliquant ce choix serait utile. 2. **`sync/route.ts` — Race condition théorique** : Si deux appareils sync leur inbox simultanément, les deux transactions pourraient trouver la même `existingInbox` et tenter la même migration. Le `onConflictDoNothing` sur l'insert protège, mais le soft-delete de l'ancien inbox pourrait s'exécuter deux fois (idempotent, donc pas de bug réel). À surveiller en cas de sync multi-device intensif. 3. **`TaskItem.tsx` — Icône Search** : La loupe (`Search`) pour ouvrir le détail est fonctionnelle, mais une icône `Eye` ou `Info` serait peut-être plus intuitive sémantiquement. Suggestion mineure. --- *Review par Claude Code — PR #64*
maximus merged commit 137dc83bf8 into master 2026-04-09 01:33:40 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/simpl-liste#64
No description provided.