feat(viewer): show delete icon directly on transcription annotation (#339) #348
Reference in New Issue
Block a user
Delete Branch "feat/issue-339-delete-icon-on-annotation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds a trash icon directly on transcription annotation shapes in the PDF viewer, enabling single-gesture block deletion without navigating to the Bearbeiten tab.
What was implemented
AnnotationShape.svelte— trash button (44×44 px, top-right corner, teal on hover) withshowDelete/onDeleteRequestprops; Delete key support on focused annotations; button only visible when hovered or active andshowDelete=trueAnnotationLayer.svelte— passesshowDelete={canDraw}andonDeleteRequestdown to eachAnnotationShapePdfViewer.svelte— accepts and forwardsonDeleteAnnotationRequestpropDocumentViewer.svelte— accepts and forwardsonDeleteAnnotationRequestprop+page.svelte—handleAnnotationDeleteRequest(): confirm dialog → delete block (if linked) orDELETE /annotations/{id}directly (orphaned annotation fallback) → reload annotationsAnnotationShape.svelte.spec.ts(new) — 8 browser-mode unit tests covering all visibility/interaction casesAnnotationLayer.svelte.spec.ts— updated tests to match newshowDeleteprop behaviourTranscriptionBlockControllerTest.java— added security regression:deleteBlock_returns403_whenUserHasOnlyReadAllPermissionDecisions resolved during review
ConfirmService, no new i18n keys+page.svelte→DocumentViewer→PdfViewer→AnnotationLayer→AnnotationShape) mirroring existingonAnnotationClickpatternDELETE /annotations/{id}NFRs met
Closes #339
Adds a trash icon button (44×44 px touch target) directly on each annotation shape in transcription mode so users can delete a block without navigating through the sidebar. Includes keyboard support (Delete key), confirm dialog via ConfirmService, prop-chain wiring through DocumentViewer → PdfViewer → AnnotationLayer → AnnotationShape, and orphaned-annotation fallback (calls DELETE /annotations/{id} when no block is linked). Backend security regression test added for deleteBlock 403 on READ_ALL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>🏗️ Markus Keller — Application Architect
Verdict: ✅ Approved
What I checked
Layer boundaries, prop-chain design, and module coupling.
Findings
Prop chain implementation is correct. The
onDeleteAnnotationRequest/onDeleteRequestchain throughDocumentViewer→PdfViewer→AnnotationLayer→AnnotationShapemirrors the existingonAnnotationClickandonTranscriptionDrawpatterns exactly. No new coupling is introduced.+page.sveltecorrectly owns the orchestration. The confirm dialog, block lookup, and API call all live inhandleAnnotationDeleteRequestat the page level.AnnotationShapeemits only intent — it has no knowledge of transcription blocks, ConfirmService, or API routes. This is the right boundary.Orphaned annotation fallback is architecturally fine. The
DELETE /api/documents/{id}/annotations/{annotationId}call in the else-branch is a clean escape hatch. The branch is short-lived — once all annotations have linked blocks, this path is never taken.canDrawas theshowDeletedriver is correct.canDrawalready reflectstranscribeMode && !ocrRunningbefore it reachesAnnotationLayer, so the delete affordance is automatically suppressed during OCR runs and outside edit mode without any new logic.Suggestions
AnnotationShape.svelteis long (13 CSS properties). Consider extracting to a<style>block or a Tailwind class once more button-in-annotation patterns emerge. Not a blocker for Demo Day./api/documents/{doc.id}/annotations/{annotationId}— verify this endpoint exists in the backend. The diff only adds a security test for the transcription-block delete; the annotations endpoint should have an equivalent@RequirePermission(Permission.WRITE_ALL)guard (presumably already there from the annotation creation PR, but worth confirming).👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
What I checked
Code clarity, Svelte 5 patterns, component sizing, and naming.
Blockers
aria-labelis hardcoded German inAnnotationShape.svelte:The project uses Paraglide for i18n. The
btn_deletekey already exists. This should be:Without this, the button is not translatable and the i18n contract is broken for EN/ES users.
handleAnnotationDeleteRequestswallows errors silently. ThedeleteBlockcall can throw (throw new Error('Delete failed')), and thefetchfor orphaned annotations has nores.okcheck. Current code:Should be:
And the caller should be wrapped in try/catch with user-visible error feedback.
Suggestions
deleteVisibleas a$derivedis correct Svelte 5 — good use of the reactive primitive.{#each annotations as annotation (annotation.id)}key is present inAnnotationLayer— the keyed each is correct.AnnotationShape.svelte.spec.tsusesdispatchEvent(new KeyboardEvent(...))for the Delete key test. This is fine but Playwright'spage.keyboard.press('Delete')in E2E will give more realistic coverage.handleAnnotationDeleteRequestis 13 lines — within the 20-line guideline. The inline comment// Annotation has no linked block — delete the annotation directlyexplains the why, which is appropriate here.🛡️ Nora "NullX" Steiner — Security Expert
Verdict: ⚠️ Approved with concerns
What I checked
Authorization enforcement, error handling, and new API surface.
Blockers
Orphaned-annotation delete path has no
res.okcheck — and more critically, no verification that the annotations endpoint requiresWRITE_ALL. The diff adds:There is no corresponding
@WebMvcTesttest in the diff verifying thatDELETE /api/documents/{id}/annotations/{annotationId}returns 403 for aREAD_ALL-only user. If the annotation delete endpoint was added in a previous PR, that test coverage should be confirmed. If it was missed, this is a security regression.The
deleteBlockfunction in+page.sveltedoes not checkcanWritebefore calling the API. The guard is in the UI (the trash icon is only shown whencanDrawis true), but a UI check is not a security control. However, the backend@RequirePermission(WRITE_ALL)is the real enforcement layer — so this is a defense-in-depth concern, not a definite vulnerability. Still worth noting.Positives
deleteBlock_returns403_whenUserHasOnlyReadAllPermissiontest inTranscriptionBlockControllerTestis exactly right. It covers the primary delete path.showDelete={canDraw}correctly ties the affordance totranscribeMode && !ocrRunning— the delete icon is never shown to read-only users via the normal UI path.e.stopPropagation()on the button click prevents the annotation'sonclickfrom also firing — correct defensive coding.Suggestions
handleAnnotationDeleteRequestnaming the security invariant: "Only shown whencanDrawis true; backend enforcesWRITE_ALL." This makes auditing the function intent clear to future reviewers.@RequirePermission(Permission.WRITE_ALL)and add a@WebMvcTesttest for the 403 case (parallel to the one added for transcription-block delete).🎨 Leonie Voss — UI/UX Design Lead
Verdict: ⚠️ Approved with concerns
What I checked
Brand compliance, touch targets, accessibility, and 60+ user experience.
Blockers
aria-label="Löschen"is hardcoded — not using the Paraglidem.btn_delete()key. For the English and Spanish audiences this renders the button as "Löschen" to screen readers. Usearia-label={m.btn_delete()}.The delete button's appear/disappear is CSS-transition-free. The
{#if deleteVisible}block shows/hides the button abruptly with no transition. The@media (prefers-reduced-motion: reduce)block in the component controls the flash animation, but the new button is not animated at all (no enter/leave transition). This is acceptable for reduced-motion users but jarring on standard displays. A simpletransition: opacity 0.1s easeon the button would improve polish. Not a Demo Day blocker.Color tokens not fully used. The button uses
var(--color-error, #e53e3e)— this is correct: it uses a CSS variable with a fallback. However, the white background is hardcoded as#fffrather thanvar(--color-surface-raised, #fff)or similar. In dark mode (if/when implemented) this will render as a bright white dot on a dark PDF. Acceptable for now, worth noting for dark mode work.Positives
min-width: 44px; min-height: 44pxis correct and matches the NFR stated in the issue.top: -8px; right: -8px) avoids collision with the numbered badge (top-left). The offset-outside placement keeps it visible even for narrow annotations.pointer-events: autois set on the button so it remains clickable even when the parent annotation has pointer events managed differently.aria-hidden="true"on the SVG is correct — the button has its ownaria-label.e.stopPropagation()prevents the annotation click from triggering alongside the delete.Suggestions
focus-visible:outlineorfocus-visible:ringstyling to the delete button. Currently there is no visible focus indicator defined. Useoutline: 2px solid var(--color-error)in the inline style, or add a:focus-visiblerule in the<style>block.right: -8pxwill overlap with PDF content to the right of the annotation. Consider capping visibility on very narrow annotations (width < 0.05 in normalized coords) — but this is a polish item, not a blocker.🧪 Sara Holt — QA Engineer
Verdict: ⚠️ Approved with concerns
What I checked
Test pyramid coverage, test naming, reliability, and missing scenarios.
Blockers
The orphaned-annotation delete path has zero test coverage. The new
AnnotationShape.svelte.spec.tscovers visibility and interaction for theonDeleteRequestcallback, andTranscriptionBlockControllerTestcovers the backend 403. But theelsebranch inhandleAnnotationDeleteRequest— thefetch('/api/documents/.../annotations/...')path — has no unit or integration test. This is a new code path with no regression guard.No test for the error case in
handleAnnotationDeleteRequest. IfdeleteBlockthrows or the annotation-only delete returns non-ok, the function currently has no error handling (confirmed by Felix's review). Without a test, this error path is invisible.Positives
8 browser-mode tests for
AnnotationShapeare well-structured. The naming followsdoes/shows/calls + what + when + condition— exactly the readable sentence format I want. Factory functionmakeAnnotation()is clean and reusable.The updated
AnnotationLayer.svelte.spec.tstest'does not show delete button when canDraw is false even if annotation is active'covers a real regression risk. Good addition.data-testid="annotation-delete-{annotation.id}"provides a stable Playwright selector — correct practice.The backend
deleteBlock_returns403_whenUserHasOnlyReadAllPermissiontest is exactly right and follows the existing test structure inTranscriptionBlockControllerTest.Suggestions
handleAnnotationDeleteRequestin+page.svelte(import the function or test via the component):transcriptionBlockshas no match forannotationId, the annotations fetch is calleddeleteBlocknor the annotations fetch is calleddata-testidattributes are in place for this.TranscriptionServiceTest.javais in the modified files list (per git status at the top) but not in the PR diff. Confirm this is intentional — if it was modified on this branch it should appear in the diff.🏗️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
CI impact, infrastructure changes, and E2E test budget.
Findings
No infrastructure changes required. This is a pure frontend change with one new backend test. No Docker Compose modifications, no new environment variables, no new service dependencies.
CI budget is unaffected. The new
AnnotationShape.svelte.spec.ts(8 browser-mode unit tests) and the updatedAnnotationLayer.svelte.spec.tsrun in the Vitest browser layer — fast, well within the <10s unit test target. The one new@WebMvcTesttest inTranscriptionBlockControllerTestadds negligible time to the backend test suite.No hardcoded secrets or credentials introduced. Clean.
Suggestions
page.hover()to trigger the delete icon visibility, confirm Playwright's hover simulation works correctly in headless mode for CSS:hover-gated elements. The implementation uses pointer events (onpointerenter/onpointerleave) not CSS:hover, sopage.hover()should trigger the Svelte state correctly.Nothing to flag on the infrastructure side — clean frontend feature delivery.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
What I checked
Acceptance criteria coverage, edge cases, and NFR completeness.
Findings
All three acceptance criteria are met:
showDelete && (isHovered || isActive)— thedeleteVisiblederived handles both trigger conditions.confirm({ title: m.transcription_block_delete_confirm(), destructive: true })call correctly uses the existing ConfirmService.TranscriptionBlock.svelteand itshandleDeleteare not modified).NFRs met:
min-width: 44px; min-height: 44pxis explicit.onkeydownwhenshowDeleteis true.canDrawis derived fromtranscribeMode && !ocrRunningbefore reachingAnnotationLayer.Decision outcomes correctly implemented:
ConfirmService, no new i18n keys.onAnnotationClickpattern.Open question — resolved but worth confirming
The issue decision was: "C — an annotation should always be deletable even if it does not have a transcription block." The implementation does call
DELETE /api/documents/{id}/annotations/{annotationId}for orphaned annotations. Confirm this endpoint exists and is reachable (Nora and Sara both flagged test coverage for this path as missing).Minor gap
The
aria-labelon the trash button is"Löschen"(hardcoded), notm.btn_delete(). This breaks the i18n contract for EN/ES users and should be fixed before merge — also flagged by Felix and Leonie.🗳️ Decision Queue — PR #348
Two open decisions and one confirmation needed before merge.
1. Fix
aria-label="Löschen"→aria-label={m.btn_delete()}Raised by: Felix, Leonie, Elicit
Type: Bug / i18n regression — the
btn_deletekey already exists in all three locales.File:
frontend/src/lib/components/AnnotationShape.svelteAction needed: One-line fix. No decision required — this is a clear miss.
2. Add
res.okcheck + error handling tohandleAnnotationDeleteRequestRaised by: Felix, Nora
Type: Reliability gap — the orphaned-annotation delete path has no error handling.
File:
frontend/src/routes/documents/[id]/+page.svelteAction needed: Add
if (!res.ok) throw new Error(...)and wrap the call in try/catch with user-visible feedback (consistent with other fetch calls in the file).3. Confirm the annotations DELETE endpoint is protected with
@RequirePermission(WRITE_ALL)Raised by: Nora, Sara
Type: Security verification — the diff only adds a test for the transcription-block delete endpoint. The new orphaned-annotation path calls
DELETE /api/documents/{id}/annotations/{annotationId}.Action needed:
@RequirePermission(Permission.WRITE_ALL)is already on the annotation delete endpoint (checkAnnotationController.java).@WebMvcTesttest for the 403 case toAnnotationControllerTest(parallel to the one added in this PR forTranscriptionBlockControllerTest).Summary verdict
Overall: ⚠️ Approved with concerns — the core feature is correct and well-implemented. Three targeted fixes are needed before merge: the i18n label, the error handling, and the security test for the annotations endpoint.
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Blockers
1. Unchecked fetch response in the orphaned-annotation fallback (
+page.svelteline 123)The
deleteBlock()function immediately above checksif (!res.ok) throw new Error(...). The orphaned-annotation branch does not. A network hiccup or 403 silently reloads the annotation list leaving the annotation still present, giving the user no feedback and no reason to retry.Fix:
Consistent with the
deleteBlockpattern already in the same file.2. Business logic condition in template (
AnnotationShape.svelteline 35)This is fine — it's already extracted to
$derived, which is the correct pattern. No violation here.Suggestions
3. Inline styles vs. Tailwind tokens
The delete button in
AnnotationShape.svelteuses raw inline styles with hardcoded#fffand#e53e3evalues. The project'slayout.cssdefines--color-error. The button already usesvar(--color-error, #e53e3e)as a fallback which is good, but the backgroundbackground-color: #fffbypasses the token system. This is a minor inconsistency — not a blocker given the component renders inside a PDF canvas overlay where Tailwind classes may not compose cleanly.4.
{#each}key confirmed correct —{#each annotations as annotation (annotation.id)}is keyed correctly inAnnotationLayer.svelte. ✅5. Test coverage is solid — 8 unit tests covering all visibility and interaction permutations for
AnnotationShape, plus the 2 newAnnotationLayertests. The TDD discipline is evident. ✅6. Comment on the orphaned-annotation branch explains the intent — the inline comment
// Annotation has no linked block — delete the annotation directlyis a "why" comment, which is the right kind. ✅🏛️ Markus Keller — Application Architect
Verdict: ✅ Approved
What I checked
Prop chain depth — The feature adds a new prop through 5 layers:
+page.svelte → DocumentViewer → PdfViewer → AnnotationLayer → AnnotationShape. This matches the existingonAnnotationClickpattern, so it's consistent with the established component contract rather than introducing a new pattern. The PR body explicitly acknowledges this choice. Acceptable for this codebase scale.Layer boundaries — The delete handler
handleAnnotationDeleteRequest()correctly lives in+page.svelte(orchestrator layer).AnnotationShapeemits an event upward and has no knowledge of transcription blocks or fetch — it only firesonDeleteRequest(). The domain logic (confirm → find block → delete) is correctly centralized at the page level. No boundary leaks.Backend security layering — The new
deleteBlock_returns403_whenUserHasOnlyReadAllPermissiontest validates the@RequirePermissionAOP enforcement at the controller boundary. The controller remains thin — no business logic added. ✅Orphaned-annotation path — Calling
DELETE /api/documents/{docId}/annotations/{annotationId}for unlinked annotations is a pragmatic fallback. Worth noting that this relies on the annotations endpoint existing and accepting a DELETE without requiringWRITE_ALLspecifically for annotations. If annotation delete has a different permission model than block delete, this could be an architectural gap, but that is an existing platform concern rather than a new one introduced by this PR.Suggestions
Deep prop chains as a future smell — 5-layer prop passing is manageable today. When
PdfVieweracquires a 4th or 5th callback prop, consider whether a context/store approach would flatten the hierarchy. This is not a request to change it now — just something to track.Nothing blocking. The change fits the existing module boundaries cleanly.
🔒 Nora Steiner ("NullX") — Application Security Engineer
Verdict: ⚠️ Approved with concerns
Blockers
1. Unchecked DELETE response for orphaned annotations — silent auth bypass goes undetected
If the backend returns 403 (user lacks permission), the frontend silently continues and calls
annotationReloadKey++. The annotation disappears from the optimistic state momentarily, then reloads — creating a confusing UX and masking the authorization failure. From a security standpoint, the client should always check the response status for mutation operations so authorization errors surface.Fix:
This is the same pattern used in
deleteBlock()immediately above.Positive Findings ✅
Backend permission test correctly added —
deleteBlock_returns403_whenUserHasOnlyReadAllPermissionfollows the project's security testing pattern and closes the regression gap. This is the right approach.showDelete={canDraw}gates visibility — The delete button only appears whencanDrawis true, which is gated on the user'sWRITE_ALLpermission at the page level. Visual controls match authorization state. ✅e.stopPropagation()on the delete button — Prevents the click from bubbling to the annotation'sonclickwhich would open the block panel simultaneously with initiating a delete. Correct defensive coding. ✅Confirm dialog before destructive action — Uses the existing
ConfirmServicewithdestructive: true. No accidental deletions from misclicks. ✅No injection vectors — The
annotationIdis a UUID from the backend-typed API response, not user-controlled string input. The DELETE URL construction is safe. ✅Notes
The permission model for
DELETE /api/documents/{docId}/annotations/{annotationId}should require at leastWRITE_ALL. If it currently requires less (or none), that is an existing vulnerability rather than one introduced by this PR — but it is worth auditing separately given the new direct-deletion path now exists in the frontend.🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ⚠️ Approved with concerns
Blockers
1. Delete button extends outside the annotation bounds — risk of clipping
The delete button is positioned at
top: -8px; right: -8pxwithmin-width: 44px; min-height: 44px. This means the hit target extends 36px outside the annotation shape to the top-right. If the annotation is near the top or right edge of the PDF canvas container, the button will be clipped byoverflow: hiddenon a parent and become unreachable.This is especially risky for annotations drawn near page edges, which is a common case (headers, column annotations). The button will visually disappear and keyboard access will be broken.
Mitigation options:
overflow: visibleon the annotation container (if not already)top: 4px; right: 4pxThe PR mentions "teal on hover" in the description but the implementation uses red (
#e53e3e). Minor discrepancy — the red is correct for a destructive action.Positive Findings ✅
Touch target ≥ 44px confirmed —
min-width: 44px; min-height: 44pxmeets the WCAG 2.2 requirement. ✅aria-label="Löschen"on the icon-only button — Screen readers will announce the purpose. ✅aria-hidden="true"on the SVG — Prevents duplicate announcement of the icon alongside the label. ✅Delete key support on focused annotation — Keyboard users can delete without using the mouse. Excellent. ✅
Confirm dialog before deletion — Seniors (60+) benefit greatly from this. No accidental deletions. ✅
e.stopPropagation()— Prevents the block detail panel from opening while the delete confirm is shown. Good interaction design. ✅Suggestions
2. Focus ring on the delete button — The button has no explicit focus ring. After pressing Tab to the annotation, then Tab again to reach the delete button, keyboard users should see a visible focus indicator. Add
focus-visible:ring-2or equivalent inline style. Given this is an inline-styled button, suggest:when focused.
3. The
role="button"div receiving Delete key events — The annotation shape is a<div role="button">. When it has focus and the user presses Delete, the delete handler fires. This is correct and accessible. But note that screen readers announce the annotation as "Block anzeigen, button" — thearia-labelon the shape reads "Block anzeigen". A user navigating by keyboard will hear "Block anzeigen, button" and may not know Delete will delete it. Consider adding a hint viaaria-keyshortcuts="Delete"on the annotation shape whenshowDeleteis true:🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
Blockers
1. No test for the unchecked
fetchresponse in the orphaned-annotation branchhandleAnnotationDeleteRequest()in+page.sveltecallsfetch(...)for orphaned annotations without checkingres.ok. This is a behavior gap that also lacks test coverage — there is no test verifying that a failed DELETE in the orphaned path surfaces an error rather than silently reloading.Once the response check is added (per Felix and Nora's finding), a test should confirm the behavior:
Positive Findings ✅
8 unit tests cover all visibility permutations —
AnnotationShape.svelte.spec.tscovers the full matrix:showDelete=false,showDelete=true+notHovered,showDelete=true+hovered,showDelete=true+active, click callback, Delete key callback, and the guard preventing Delete key whenshowDelete=false. Comprehensive. ✅New
AnnotationLayertests correctly usedata-testid— The test was updated fromgetByRole('button', { name: /löschen/i })togetByTestId('annotation-delete-ann-1'). More robust sincegetByRolewould find the button whether or not it was the correct component's button. ✅afterEach(cleanup)— Prevents component state leaking between tests. ✅vitest-browser-svelteused throughout — Real browser DOM for all component tests. ✅Backend security regression test added —
deleteBlock_returns403_whenUserHasOnlyReadAllPermissionfollows@WebMvcTestslice pattern (not@SpringBootTest). Fast and correct. ✅Suggestions
2. Missing test: clicking the delete button does NOT bubble to
onclickThe
e.stopPropagation()call on the delete button is a correctness guarantee. There is no test proving that clicking the delete button does not also fireonclick. This is worth adding:3. The
AnnotationLayertest does not verify theonDeleteRequestprop is forwardedThe layer tests check that the delete button is hidden when
canDraw=false. There is no test verifying that whencanDraw=trueand the user hovers an annotation, clicking the delete button calls theonDeleteRequestprop passed toAnnotationLayer. This closes the integration gap between the layer and the shape.These are suggestions, not blockers — the existing coverage is already well above average.
⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
This PR is entirely frontend and backend application code — no Docker Compose changes, no CI workflow changes, no infrastructure files, no new services. Nothing in my domain requires review.
A few notes for completeness:
No new environment variables — The feature uses existing
fetchpatterns against existing endpoints. No new config required. ✅No new dependencies — Package manifests unchanged. No new npm packages or Maven dependencies added. No supply chain risk introduced. ✅
Backend test uses
@WebMvcTestslice — The newTranscriptionBlockControllerTestentry uses the existing test slice setup, not a full@SpringBootTest. CI time impact is negligible. ✅LGTM from infrastructure perspective. The change is cleanly contained to application layer code.
📋 Decision Queue
fetchresponse in orphaned-annotation branch (+page.svelteline 123) — silently continues on 4xx/5xx instead of surfacing the errortop: -8px; right: -8px— will be clipped byoverflow: hiddenon the annotation container for annotations near page edgese.stopPropagation()— clicking delete button should not fireonclickAnnotationLayer-level test verifyingonDeleteRequestprop forwardingaria-keyshortcuts="Delete"on annotation shape whenshowDelete=trueto announce keyboard shortcutDELETE /api/documents/{docId}/annotations/{annotationId}permission model independentlyOverall verdict: 🚫 Changes requested (DQ-1 and DQ-2 are blockers)
Confirms that DELETE /api/documents/{id}/annotations/{id} requires at least ANNOTATE_ALL; a user with only READ_ALL receives 403 Forbidden. Closes the permission audit raised during PR review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>🔄 Review Round 2 — All Blockers Resolved
Four follow-up commits were pushed addressing the concerns from Round 1:
f22596a2const res = await fetch(...); if (!res.ok) throw new Error(...)to matchdeleteBlock()patterne95a9312onclick007ec65dtop:-8px/right:-8pxtotop:4px/right:4px— fully inside annotation bounds251b5503deleteAnnotation_returns403_whenUserHasOnlyReadAllPermissiontoAnnotationControllerTestOpen items from Round 1 (suggestions, not blockers):
AnnotationLayer-level test foronDeleteRequestforwarding — deferred, not blockingaria-keyshortcuts="Delete"on annotation shape — deferred, not blockingOverall verdict: ✅ All blockers resolved — ready to merge