fix(adhoc): assignee photos never reached the server - #1177
Merged
renemadsen merged 4 commits intoSep 4, 2026
Conversation
RequireCreator rejected every UpdateTask from an assigned-but-not-creating worker, including the no-op the mobile client sends when that worker adds a photo. The client treats PermissionDenied as permanent, parks the outbox row, and its per-task head-of-line rule then blocks the photo upload queued behind it forever - so assignee photos never reached the server at all, and the worker's later comments on that task were silently dropped too. A CanSee-eligible non-creator is now accepted and ignored: no field is applied and PhotoIds is not reconciled (it soft-deletes by omission, so a stale client could otherwise wipe the creator's photos). Ignore rather than diff, because the client builds its request from a cache that may lag a concurrent edit and a diff would reject exactly the offline saves this exists to accept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPgYKU2f7RQ7HNmCZqF43g
The drawer was opened with the row object cached from the last index call and submits that snapshot's photoIds back on save, where ReconcilePhotosAsync soft-deletes by omission - so saving any edit silently wiped every photo added on mobile since the page was loaded. Latent while assignee photos never arrived; live as soon as they do. Fetches by id first, mirroring AdhocHistoryComponent.onRowClick. onCopyTask is untouched: its result is already the copy endpoint's fresh response. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPgYKU2f7RQ7HNmCZqF43g
getTaskSub$ was shared by both onViewTask and onEditTask. View row A, then Edit row B before A's HTTP call resolves, and B's assignment drops the reference to A's Subscription. AutoUnsubscribe only walks fields still referenced on the instance, so navigating away before A resolves leaves A's request in flight; its callback then fires after destruction and pops the drawer on an unrelated page. Split into viewTaskSub$ and editTaskSub$, one per call site, so AutoUnsubscribe cancels both on destroy with no further wiring. Not a subscription leak - getTask() is a plain apiBaseService.get() that self-completes after one emission - just a dropped reference defeating the existing cleanup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPgYKU2f7RQ7HNmCZqF43g
…pt-and-ignore path - Add the two unpinned UpdateTask acceptance cases: a web-created task (CreatedByWorkerId == 0) edited by its assigned gRPC worker, and a non-assigned worker with property access on an Everyone-rule task. - Reword the Overblik drawer refetch comment: it narrows the photo data-loss window, it does not close it, since visiblePhotos still reads the drawer-open-time task snapshot. - Log the accept-and-ignore outcome so it's distinguishable from a real update if the mobile UI ever widens what it lets an assignee send. - Note that RequireCreator's worker-0 guard is now only reached for web-created tasks, and rename callerAssignedWorkerIds (holds the task's, not the caller's, assignees) to taskAssignedWorkerIds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPgYKU2f7RQ7HNmCZqF43g
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Pictures taken by the worker a task is assigned to never appeared on web — only the creator's original pictures did.
They never reached the server at all.
RequireCreatorrejected everyUpdateTaskfrom an assigned-but-not-creating worker, including the no-op the mobile client sends when that worker adds a photo. The mobile client classifiesPermissionDeniedas permanent and parks the outbox row, and its per-task head-of-line rule then blocks everything queued behind it for that task — the photo upload itself, and that worker's later comments, permanently. Nothing in the app can move a row out offailed, so it never recovered.The irony:
SavePhotois alreadyCanSee-gated, so the assignee was always authorised to upload. They just never got there, because the no-op update queued in front of it was rejected.What this changes
1.
UpdateTaskaccepts and ignores aCanSee-eligible non-creator (BackendConfigurationAdhocService.cs). No field is applied andPhotoIdsis deliberately not passed toReconcilePhotosAsync— that reconciles by omission, so honouring a stale client's list would wipe the creator's photos. A non-creator who failsCanSeeis still rejected exactly as before.Ignore semantics, not a field diff, for two reasons: the client builds its request from a local cache that may legitimately lag a concurrent edit, so a diff would reject exactly the offline saves this exists to accept; and the request has no change on the wire to detect anyway (the new photo is still
pendingUploadand is filtered out before sending).Security: the new gate is a strict subset of what
RequireCreatoralready rejected, and its only non-throw exit is a read-onlyMapToModelAsync. It can only ever convert a reject into a read, never into a write. The web dashboard is statically excluded —AdhocControllerpassesDashboardHasFullAccess = trueas aconst, so it never enters the branch.2. The Overblik drawer refetches the task before opening (
adhoc-container.component.ts). It was opened with the row cached from the last index call and submits that snapshot'sphotoIdsback on save, whereReconcilePhotosAsyncsoft-deletes by omission — so saving any edit silently wiped photos added on mobile since page load. Mirrors the existingAdhocHistoryComponent.onRowClickpattern.onCopyTaskis untouched: its result is already a fresh server response.Known limitation, deliberately not fixed here
The refetch narrows the data-loss window (from "since the last index page load", potentially hours since Overblik does not poll, to "since the drawer opened") — it does not close it.
visiblePhotosreads the drawer-open snapshot, so a photo uploaded from a phone while the drawer sits open is still soft-deleted on save. The mobile creator has the same shape.The durable fix is to stop reconciling by omission — send
removedPhotoIds(the drawer already tracks them) or version-gate the update. That is a design decision and is left for a follow-up ticket.Testing
backend-configuration-pn: 28 suites, 417 tests, all passed.CreatedByWorkerId = 0and an assigned worker) andEveryone-rule tasks, which are the widest new acceptance surface. Both assert the persisted DB row, not just the returned model.UpdateTask_Throws_WhenCallerIsNotCreatoris kept unmodified and still passes — it now pins "a non-creator who cannot see the task is still rejected".Merge order
Merge and release this before the flutter-adhoc counterpart. The mobile PR replays previously-parked updates; against an unfixed server those are denied again and re-park, wasting the recovery on every affected device.
Design:
docs/superpowers/specs/2026-09-04-assignee-photos-not-visible-on-web-design.mdin the flutter-adhoc repo (§2 and §4).🤖 Generated with Claude Code