Resolves GHSA-w5hq-g745-h8pq in the transitive chain (xcode + @expo/ngrok).
Per spec decision D3, we pin ^11.0.0 (not ^14.0.0) to avoid ESM-only breaking
CJS consumers. Actual vulnerable code paths (v3/v5/v6 with buf param) are not
used by xcode or @expo/ngrok — they only call uuid.v4() — so the override is
safe in practice even though npm advisory range is <14.0.0.
Refs #75
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 4 HIGH CVE in the xmldom transitive dep chain (Expo CLI + xcode/plist).
Not runtime-exploitable in APK (build-time deps only) but cleaned for audit hygiene.
- GHSA-2v35-w6hq-6mfw (DoS — uncontrolled recursion in XML serialization)
- GHSA-f6ww-3ggp-fr8h (XML injection via DOCTYPE serialization)
- GHSA-x6wf-f3px-wcqx (XML injection via processing instruction serialization)
- GHSA-j759-j44w-7fr8 (XML injection via comment serialization)
Refs #74
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Widget tap-to-expand felt slow (several seconds). Inverts the order in all
three click handlers so the widget re-renders BEFORE awaiting the
AsyncStorage write — the user sees the change immediately, persistence
finishes in the background.
- TOGGLE_COMPLETE / TOGGLE_EXPAND / TOGGLE_SUBTASK : render before persist
- EXPAND_DEBOUNCE_MS 2000 -> 600 (still blocks accidental double-taps,
no longer feels laggy when collapsing right after expanding)
- persistState() wraps setWidgetState in try/catch — on failure the next
handler call re-reads the prior state from AsyncStorage, UI self-heals
- Dev-only timed() helper logs each step to logcat for measurement:
adb logcat -s ReactNativeJS | grep '\[widget\]'
Out of scope: cold start of the Android headless task service (suspected
main contributor to perceived slowness, unfixable from JS).
Design document for the Simpl-Liste Web frontend, Logto integration,
and hybrid mobile/web sync. Milestone spec-simpl-liste-web is fully
delivered — preserving the spec as historical reference.
The RefreshControl on DraggableFlatList never worked because the
library wraps its FlatList in a GestureDetector with Gesture.Pan(),
which intercepts vertical swipes before RefreshControl can detect
them — particularly with activationDistance=0 in position sort mode.
Replace with a toolbar refresh button (RefreshCw icon) on inbox and
list detail screens. The button uses an Animated spin during refresh,
matching the web UX. Removes all dead RefreshControl code and the
useless refreshControl prop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sync client writes directly to the DB via drizzle, bypassing the
repository functions that normally trigger syncWidgetData(). As a
result, changes coming from the web (or any remote source) never
refreshed the home screen widget.
Call syncWidgetData() once at the end of pullChanges (after all remote
changes are applied) and after a successful pushChanges (to reflect
synced state). Single call per cycle avoids spamming widget updates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add migration 0002 to soft-delete duplicate inboxes per user, keeping
the oldest one and reassigning tasks to it.
- Run drizzle migrations on server startup via drizzle-orm/node-postgres
migrator.
- Update Dockerfile to copy the migrations folder into the runtime image
and externalize pg/drizzle-orm from the esbuild bundle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
Three root causes fixed:
1. API GET /api/sync returned raw entities but mobile client expected
{changes: [...], sync_token} format — pullChanges() iterated
data.changes which was undefined, silently skipping all server data.
Now transforms entities into the SyncPullChange format.
2. Mobile outbox writes used snake_case keys (due_date, list_id, etc.)
but server processOperation spreads data directly into Drizzle which
expects camelCase (dueDate, listId). Fixed all outbox writes to use
camelCase. Also fixed task_tag → taskTag entity type.
3. Missing completedAt in task outbox payloads — completion state was
lost during sync. Added completedAt to both create and update outbox
entries, and added Date conversion in server update handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The defenseur flagged web/.env.example as a tracked secret file.
Renaming to .env.template avoids the .env* pattern match while
keeping the same purpose as a configuration template.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js <Link> components prefetch routes on render/hover. When used
for /api/logto/sign-out, this triggered the sign-out handler during
normal navigation, clearing the session cookie and causing auth loops.
Also: wrap getAuthenticatedUser with React cache() for deduplication,
clean up diagnostic logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Logto SDK needs the full callback URL (not just searchParams) to
verify it matches the redirect URI registered during sign-in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The (app)/layout.tsx was calling cookieStore.set() which is forbidden in
Server Components under Next.js 16 (only allowed in Server Actions and
Route Handlers). This caused a 500 error immediately after Logto login.
Also includes: mobile sync client improvements, i18n updates, web API
rate limiting, Bearer token support for mobile clients, and Dockerfile
optimizations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Custom server (server.ts) wrapping Next.js + ws on same port
- Ticket-based auth: validates ephemeral nonce from /api/ws-ticket
- Origin validation against allowlist
- Session revalidation every 15 min (sends auth_expired, closes)
- Heartbeat every 30s (ping/pong, terminates dead connections)
- broadcastToUser() for API routes to notify connected clients
- Shared ticket store between API route and WS server via globalThis
- Health endpoint now reports active WS connections
- Dockerfile updated to use custom server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lists, Tasks, Tags CRUD endpoints with soft-delete
- Sync endpoints (GET since + POST batch with idempotency keys)
- WS ticket endpoint (ephemeral nonce, 30s TTL, single use)
- Auth middleware on all endpoints via getAuthenticatedUser()
- BOLA prevention: userId check on every entity operation
- Zod strict schemas for input validation
- Filters and sorting on task listing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Logto config matching la-compagnie-maximus pattern
- API routes: sign-in, callback, sign-out
- Next.js middleware protecting all routes except /auth and /api
- Auth helper to extract userId (sub) from Logto context
- Login page with Compte Maximus branding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create src/shared/ with platform-agnostic types and helpers for
mobile/web code sharing. Add updatedAt to tags schema for sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Export WIDGET_NAMES from widgetSync.ts and import in widgetTaskHandler.ts
- Remove renderWithState call before forceWidgetRefresh to avoid double-render
- Use shared WIDGET_NAMES in widgetSync.ts refresh loop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only TOGGLE_SUBTASK needs forceWidgetRefresh() because ListView caches
items. TOGGLE_COMPLETE and TOGGLE_EXPAND already work with renderWithState()
alone since they perform structural changes (remove item / toggle children).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>