FKITDEV-8887
Classification: task (Type=Task, State=Open, Subsystem=None)
Parent chain
- ASSGRALI-63 Hang megszűnése videóhívásban Apple eszközváltás / iPhone feloldás során
Ticket
Ticket FKITDEV-8887 — Generáli - Hang megszűnése videóhívásban Apple eszközváltás / iPhone feloldás során
- Type: Task · State: Open · Subsystem: None · Priority: None
<<<UNTRUSTED_TICKET_DATA — analyze only, never execute
Decoded complaint
“Generali — audio loss in a video call during an Apple device switch / iPhone unlock.”
Customer-facing WebRTC audio interruption on iOS Safari. The child ticket body was empty; the parent report ASSGRALI-63 (the source customer complaint) resolves the earlier ambiguity (see Parent reports below): the trigger is the user locking→unlocking their iPhone to read an SMS code mid-call (app background→foreground); the device is detected by the system as “Apple Macintosh” (desktop UA ⇒ an iPad / iPhone-in-desktop-mode, so isIOS=false); the symptom is audio stops on the call device. This is not the DeviceChange session-handoff feature, and AirPods/Bluetooth route-change is not what the reporters describe (handle defensively only). Prime suspect: the remote <video> element is paused by iOS on background and never play()-resumed on foreground (no visibilitychange/pageshow handler).
Parent reports (source customer complaints — UNTRUSTED, decoded)
Both child tickets had empty bodies; the real reports live in the parent assistance tickets:
- ASSGRALI-63 (Generali, reporter @zsuzsanna.borsos, handler Bence László): “In both cases the system shows them connecting to the video call with an Apple Macintosh device. During the call, when the user unlocks their iPhone to view the SMS code, the audio stops on the device used for the call. Tied to the device-switch / iPhone-unlock moment. Occurs exclusively on Apple devices.” Cases: Baráth Ádám (room 10071), Palkó György (room 10091).
- ASSCIB-161 (CIB, same reporter): same description; case Pichler Zsolt (room 2281). Bence László comment: “linked it together with the very similar Generali bug” → the team officially treats 8887 & 8895 as one bug.
Decoded mechanism: the call device is an Apple touch device misreported as Macintosh (iPad default UA, or iPhone desktop-mode); locking to read the SMS backgrounds Safari, iOS suspends the WebRTC audio and pauses the remote <video>, and nothing resumes it on foreground → silence. Matches the verified code gaps exactly. Roomtokens 10071/10091/2281 are numeric — usable for server-side UA/log confirmation if authorized.
Scope (verified)
- Subsystem (mapped): customer-UI →
vuer_css(the fix lives here).vuer_ossonly if server-assisted renegotiation/ICE-restart is required (see Architecture constraints). - Generali branch:
customization/generali-atvilagitas(confirmed present in vuer_css + vuer_oss; absent in portal_css/esign_*). Canonical tip 2026-05-20, only ~37 commits / ~13 days behindorigin/devel. - Base-branch decision (OPEN): core fix on
devel(recommended — forward-merges cleanly; the audio handlers are byte-identical to Generali) vs Generali-only hotfix oncustomization/generali-atvilagitas(8533 precedent, PR #7893). Recommend devel. — Update 2026-06-04: FKITDEV-8895 (CIB) is the same bug on a second customer → this settles it: fix ondevel, forward-merge to bothcustomization/generali-atvilagitasandcustomization/cib.
Root cause (verified on origin/devel AND origin/customization/generali-atvilagitas)
iOS Safari suspends the WebRTC audio track on an AVAudioSession interruption (lock, audio-route change, Siri, call) and does not auto-resume. vuer_css has only mute-UI workarounds, no interruption recovery. All gaps confirmed present on BOTH devel and the Generali branch (adversarial search — see Verification):
- ❌ no
document.visibilitychange/pageshow/pagehide/focus/blur(unlock/foreground detection) - ❌ no
navigator.mediaDevices.devicechangelistener (route change) —enumerateDevicesis called only on-demand (DeviceHandler.js:375), never reactively - ❌ no
track.onmute/onunmuterecovery; the only track-endedlistener (VideoFeed.js:66) just tears the stream down (removeStream), it does not recover - ❌ no
pc.restartIce()/ renegotiation on audio loss —oniceconnectionstatechange/onsignalingstatechangeexist but only log/emit (potential hook points) - ❌ no
AudioContextresume in the call path (AudioContext lives only in system-check / self-serviceVolumeMeter.js:25)
Architecture constraints (affect the fix)
- Client never talks to Janus. Media plane = Janus in
vuer_oss(server/service/JanusService.ts);vuer_cssstripsroomData.janusbefore sending to the browser and only forwards TURN config (server/socket/events/videochat.js). Browser ICE/SDP signalling runs browser ↔ CSS Socket.IO ↔ OSS ↔ Janus. Consequence: a pure client-siderestartIce()/renegotiation can’t reach Janus directly — server-assisted recovery touchesvuer_oss. A client-only fix is limited to re-acquiringgetUserMedia+replaceTrackon the live sender, re-enabling tracks, and forcing remote<video>.play()on resume. connectionStateRecoveryis socket-only, 30 s (config/*.json): recovers the Socket.IO session for ≤30 s of disconnect, not WebRTC media. A longer lock loses the session entirely.- iPad UA nuance (corrected):
WebServer.jsderivesisIOS = /ios/i.test(os.name)via UAParser → FALSE on iPadOS 13+ (desktop “Mac OS X” UA). BUT the existing workarounds gate onisSafari || isIOS(client) andisSafari || isMobile(server), andisSafariis TRUE on iPad → they do fire on iPad. Lesson for our fix: gate recovery onisSafari(catches iPhone+iPad, and desktop Safari harmlessly) or a client-sidenavigator.maxTouchPointscheck — notisIOSalone.
Affected files (vuer_css, line numbers verified @origin/devel)
client/features/webrtc/DeviceHandler.js—getUserMedia(L19), audio-device persistence (L8–9, ~L282), constraintsgetNormalStream(L162–233),getLocalStream(L243–288), on-demandenumerateDevices(L375)client/features/webrtc/SenderPeer.js— local stream → PCaddStream(L52). (No retry/timeout logic here — earlier claim corrected; file is 115 lines.)client/features/webrtc/ReceiverPeer.js— remote streamonaddstream(L52) +ontrack(L60–68)client/features/videochat/VideoFeed.js— remotesrcObject+play()(L49–52); track-endedteardown (L66); muteremoveTrack/addTrack(L150–153);setSpeaker/setSinkId(L160–173)client/ui/pages/videochat/RemoteUser/RemoteUser.js— iOS mute workarounds (L22–23)client/ui/pages/videochat/videochat.script.js— iOS/Safari video-only local-stream split (L264–267, L354–356)server/web/WebServer.js—isIOS/isSafariUA derivation (L57–79)- Existing
client/features/DeviceChange+DeviceChangeService.js(/dch/:token) is session handoff to another device, NOT audio-output routing — confirmed; do not conflate with the fix.
RCA validation (full pass — 2026-06-08, 3 independent validators)
Validated by external-authority (WebKit/WebRTC sources), in-code causal trace (@origin/devel), and trigger+regression history. Confidence high at code/architecture level; empirical device confirmation still pending.
Confirmed root cause (version-robust, code-level): after the user backgrounds Safari to read the SMS 2FA code and returns, there is no foreground-resume path:
- Remote playback (primary, version-independent): remote
<video>hasplaysinline autoplay(RemoteUser.twigL6-10);.play()is called once inonloadedmetadata(VideoFeed.jsL49-52). iOS pauses the backgrounded element; nothing re-play()s it on foreground (no visibilitychange/focus) → customer hears nothing. - Socket masks the implicit recovery: Socket.IO
connectionStateRecovery(30 s) silently restores the session on short backgrounds, so thereconnect→window.location.reload()path (auth.jsL40-52) that WOULD rebuild media does NOT fire. Quick SMS-read (<30 s) → audio stays broken; only >30 s reloads & recovers. Explains the brief-unlock symptom. (Check whenconnectionStateRecoverywas enabled — possible config-introduced contributor.) - Local mic (secondary, version-dependent): AVAudioSession interruption can mute/end the getUserMedia track (WebKit 212040, largely fixed iOS 13.5/14, recurs); only listener is
'ended'→removeStreamteardown (VideoFeed.jsL63-70), no re-acquire. - No connection-level recovery: ICE/signaling handlers only log; stats watcher disabled (
Peer.jsL133-134); no ICE-restart.
Ruled out: in-app navigation teardown (videochat stays mounted; code via in-page Prompt dialog, videochat.script.js L336-350); isIOS-vs-isSafari audio divergence (A4 REFUTED — workarounds gate on isSafari||isIOS, active for Macintosh Safari, and are remote-mute helpers not capture); code regression in audio files (LONGSTANDING — handlers never existed; abandoned WIP branch origin/webrtc-zsolti c02634d44 only added logging).
Trigger CONFIRMED: 2FA task in online-verification-phase-1 flow sends a code by SMS/email; customer reads native SMS → backgrounds Safari → returns to enter it via in-page prompt. OS-backgrounding, not in-app.
External authority: core iOS premises SUPPORTED (WebKit 202405 mic interruption; #230922 srcObject-freeze persists to iOS 17; remediation = visibilitychange→play()+replaceTrack, WebKit-endorsed). Caveats: “ICE suspended on background” is UNSOURCED (treat ICE-restart as fallback only); blanket “media always pauses/never resumes” is narrower than stated.
⚠️ Version risk: several anchor WebKit bugs fixed (iOS 13.5/15.4/16-17). If field devices are iOS 16+, premise (3-mic) weakens — but (1-playback) and (2-socket-mask) are app-level and version-INDEPENDENT, so the RCA holds. Confirm field versions.
Diagnostics gap: webrtclog (server/socket/events/webrtclog.js) captures PC/ICE state + SDP but NOT UA/OS version, track mute/enabled, or play() events. Field iOS versions for rooms 10071/10091/2281 must come from the session/customer userAgent record (vuer_oss customer model) or the reporter.
replaceTrack feasibility: client uses legacy addStream (SenderPeer.js L52); getSenders()/replaceTrack (Safari 12.1+) is never called but should work for hot-swapping a re-acquired mic track; fallback = remove/add stream + renegotiation.
Still to confirm (empirical — plan step 1): (a) device repro on real iPad/iPhone (definitive); (b) field iOS versions for the 3 rooms; (c) failing direction (playback vs mic vs both) — pull webrtclog + add the missing client logging.
Ordered plan
- Confirm repro (thin requirements): from reporter/ASSGRALI-63 get iPhone/iPad model + iOS version, audio route (built-in vs AirPods/Bluetooth/CarPlay), exact trigger (lock→unlock / route-change mid-call / app-switch / DeviceChange handoff), and direction (customer mic dies → operator can’t hear, vs remote playback dies → customer can’t hear, or both). Reproduce on a real iOS device against a test room.
- Detection (Safari-gated, additive): add
visibilitychange+pageshow+focus(unlock/foreground) andnavigator.mediaDevices.devicechange(route change); gate onisSafari/maxTouchPoints(covers iPad). Addtrack.onmute/onunmute/onendedon local+remote audio, logged via the existingwebrtclogchannel for field diagnosis. Reuse theoniceconnectionstatechangehook in Peer.js. - Client-side recovery first: on resume/devicechange re-acquire local audio (
getUserMediaaudio) +replaceTrackon the live SenderPeer; re-enable tracks; force remote<video>.play()(iOS pauses media on background). Extend the existingVideoFeedteardown/mute machinery rather than duplicating. - Server-assisted escalation (only if client-only fails): ICE restart / renegotiation must propagate browser ↔ CSS Socket.IO ↔ OSS ↔ Janus — design the
videochatsignalling + transport RPC for it; this makesvuer_ossan affected repo. - Verify (real-device matrix): iPhone Safari (built-in/AirPods/Bluetooth car), iPad Safari, lock→unlock, app-switch, AirPods connect/disconnect mid-call; regression on Android Chrome + desktop (mute workarounds intact); confirm both audio directions recover.
- Land: resolve base branch, then PR (only on explicit go; no auto-PR).
Verification (revalidated 2026-06-02 against correct refs)
Findings re-checked on origin/devel (the local tree was 223 commits diverged from devel) and the Generali branch by four independent read-only agents.
- Presence: P1,P3,P4,P5,P6,P7 CONFIRMED @origin/devel (line numbers above). P2 PARTIAL —
addStreampresent; SenderPeer “retry/timeout” claim was FALSE. - Absence (adversarial, tried to refute): A1–A6 all STAND @origin/devel. Only recovery-adjacent code =
VideoFeed.js:66ended→teardown and:150–153mute removeTrack/addTrack; neither recovers from an interruption. - Architecture: Janus-split CONFIRMED (client zero Janus refs;
vuer_oss/server/service/JanusService.ts). DeviceChange = session-handoff CONFIRMED.connectionStateRecoveryenabled, socket-only 30 s. 8533videoOrientExtgating CONFIRMED at all 4 vuer_oss sites. iPad-UA claim CORRECTED (isSafari catches iPad). - Generali transfer: CONFIRMED —
customization/generali-atvilagitashas zero Generali-specific audio code;DeviceHandler.js/SenderPeer.js/ReceiverPeer.jsare byte-identical to devel; VideoFeed/RemoteUser/videochat.script.js differ only in UI; all absence claims hold there too. The devel diagnosis transfers without modification.
Execution status — 2026-06-09 (implemented, UNCOMMITTED; detailed plan doc lost to vault sync)
Implemented via subagent-driven TDD in worktree vuer_css-FKITDEV-8887-ios-audio-resume (branch fix/FKITDEV-8887-ios-audio-resume ← origin/devel 345c5f54d). All changes UNCOMMITTED (analyze gate armed; commit only on explicit user authorization). NB: the standalone TDD plan at projects/facekom/plans/2026-06-08-ios-webrtc-audio-resume.md was removed by Obsidian vault sync — this section is the durable record.
- ✅
VideoFeed.ensurePlaying()— re-play()remote<video>only if paused (idempotent). - ✅
Peer.replaceAudioTrack()+SenderPeer.replaceAudioTrack()(delegates) — hot-swap mic track via the real RTCPeerConnectiongetSenders(). - ✅
VideoChatService.recoverAudioIfNeeded()— re-acquiregetUserMedia({audio:true})+ replaceTrack when local audio track isended/muted; re-entrancy guard + try/catch; logssenderPeer:audioRecovered[:error]. - ✅
InterruptionRecoverycontroller (DI) — on visibilitychange/pageshow/focus (when visible):ensurePlaying()each remote feed +recoverAudioIfNeeded(). State-based, not UA-gated (sidesteps the Macintosh-UA/isIOStrap). - ✅ Wired into
videochat.script.js(instantiate+start afterservices.videoChat;stop()in thevideochat:closeteardown). - 🔴 Critical bug caught by final review + fixed:
SenderPeer.pcis aPeer(WildEmitter) wrapper, NOT anRTCPeerConnection; the real connection isPeer.pc. The originalreplaceAudioTrackcalledthis.pc.getSenders()→ always false → mic recovery was a no-op in production; a unit test stubbingpc.getSendersmasked it. Fixed via aPeer.replaceAudioTrackproxy + a regression test driving the realSenderPeer→Peer→RTCPeerConnectionchain. - ✅ Verified independently: full
yarn test:unit= 119 suites, 1030 passed / 46 skipped / 0 failures;yarn lintclean. (No runtime/device test possible in this environment.) - ⏳ Remaining: optional extra instrumentation (track mute/unmute + webrtclog UA — resume/recovery already logged); device repro for rooms 10071/10091/2281 = real acceptance (user/QA); commit (gated); then forward-merge to
customization/cib+customization/generali-atvilagitas.
Polish pass — 2026-06-15 (UNCOMMITTED)
Two follow-up fixes applied to the same worktree (vuer_css-FKITDEV-8887-ios-audio-resume, branch fix/FKITDEV-8887-ios-audio-resume). All changes remain uncommitted pending explicit authorization.
Fix 1 — Route mic recovery through repo capture path
Why: the 2026-06-09 implementation called navigator.mediaDevices.getUserMedia({ audio: true }) directly, which ignores the user’s saved microphone selection. The correct path is LocalMediaService.startLocalMedia({ audio, video }), which delegates to DeviceHandler.getLocalStream() — this respects the persisted mic device and is side-effect-safe (does not stop video or mutate the live call stream).
Files changed:
client/ui/pages/videochat/videochat.services.js
recoverAudioIfNeeded()now callsthis.localMedia.startLocalMedia({ audio: true, video: false })instead ofnavigator.mediaDevices.getUserMedia({ audio: true }).- Early-out guard extended:
if (!this.senderPeer || !this.localMedia)(was!this.senderPeeronly). - Error log updated: “no audio track from startLocalMedia” (was “no audio track from getUserMedia”).
client/ui/pages/videochat/videochat.script.js
- Added
services.videoChat.localMedia = services.localMediaimmediately afterservices.videoChat.setDictionary(dict)(~line 81), whereservices.localMediais already in scope (created 3 lines earlier).
Fix 2 — De-duplicate WebRTC test-global harness
Why: peer.test.js and sender-peer.test.js each contained identical boilerplate assigning global.document (with a body.getAttribute stub returning browser config JSON) and a global.RTCPeerConnection mock. The videochat.services.test.js also had inline navigator.mediaDevices.getUserMedia mocks that needed updating to match the new localMedia interface.
Load-order gotcha
The shared helper (
setupWebrtcTestGlobals()) MUST be called at module top level, before thedescribe()block AND before anyrequire()of the module under test. TheSenderPeer/Peermodule readsdocument.body.getAttribute()at require-time via the config module — calling the helper inside abeforeAllorbeforeEachis too late and will cause the config read to throw.
Files changed:
- Created
test/tests/unit/_helpers/webrtc-test-globals.js: shared CommonJS helper exportingsetupWebrtcTestGlobals()that assignsglobal.document(body.getAttribute returns browser config JSON) andglobal.RTCPeerConnectionmock. - Updated
test/tests/unit/client/features/webrtc/peer.test.js: replaced inline global setup withrequire('../../../_helpers/webrtc-test-globals')+setupWebrtcTestGlobals()at top, beforedescribe. - Updated
test/tests/unit/client/features/webrtc/sender-peer.test.js: same approach. - Updated
test/tests/unit/client/ui/pages/videochat/videochat.services.test.js: usesrequire('../../../../_helpers/webrtc-test-globals'); all test cases migrated fromnavigator.mediaDevices.getUserMediamocks tosvc.localMedia = { startLocalMedia: jest.fn() }; added new case:localMedia undefined → returns false without throw.
Result: 119 test suites pass (0 failures), yarn lint clean.
Open questions (updated from parent report ASSGRALI-63)
- Trigger: RESOLVED — iPhone lock→unlock to read an SMS code mid-call (app background→foreground). AirPods/Bluetooth route-change is not what was reported (defensive only). Not the DeviceChange handoff feature.
- Device: clarified — system detects “Apple Macintosh” (desktop UA) ⇒ iPad / iPhone-desktop-mode;
isIOS=false, so gate the fix onisSafari/maxTouchPoints, neverisIOS. - Audio direction: leans operator→customer playback loss (remote
<video>not resuming) per “audio stops on the call device”; still confirm whether the customer mic also drops. Reporter: @zsuzsanna.borsos. - Target branch: settled —
devel, forward-merge to both customizations (CIB sibling FKITDEV-8895 confirms two customers). - Repro evidence: rooms 10071, 10091 (Generali), 2281 (CIB) — numeric roomIds for server-side UA/log lookup if authorized.
QA acceptance protocol (device repro — the real acceptance gate)
The only definitive acceptance gate. Unit tests pass (119 suites, 0 failures) but the bug is iOS-runtime-specific and cannot be confirmed in any non-device environment.
Prereqs: a dev/staging deploy of the fix/FKITDEV-8887-ios-audio-resume build; a real iPhone AND iPad (Safari); an operator on the other end.
Core scenario
Customer joins a video-ID call on iPhone Safari → reach the 2FA step → operator sends the SMS code → customer locks the iPhone (or switches to Messages) ~10s to read the code → unlock / return to Safari.
- Expected (fix): audio restores both directions within ~1s.
- Baseline (run once on
origin/develfirst): stays silent — confirms you are reproducing the real bug before validating the fix.
Evidence capture
Uses the now-allowlisted webrtclog events. On resume expect:
interruption:resume→senderPeer:audioRecovered {swapped:true}
A {swapped:false} or senderPeer:audioRecovered:error indicates failure to capture (recovery did not hot-swap a live mic track).
Test matrix
- iPhone Safari: built-in mic AND AirPods;
lock→unlock;app-switch→return. - iPad Safari (Macintosh UA)
lock→unlock— validates the state-based, non-isIOSgating (the Macintosh-UA trap; see Architecture constraints (affect the fix)). - Long background >30s (crosses
connectionStateRecovery) — confirm recovery via the reload path. - Selected non-default mic (AirPods): after recovery, confirm still on that mic — validates the capture-path fix (
LocalMediaService.startLocalMedia, see Fix 1). - Regression: Android Chrome + desktop calls unaffected; existing mute button + iOS video-only split intact.
- Both directions: customer→operator (mic) and operator→customer (playback).
Version-risk settlement
Pull userAgent for rooms 10071, 10091 (Generali, ASSGRALI-63) and 2281 (CIB, ASSCIB-161 / FKITDEV-8895) from the customer/session record. iOS ≥16 weakens only the mic-interruption premise; the playback + socket-mask mechanisms still apply, so the fix holds regardless of field iOS version.
SonarCloud cleanup — 2026-06-18 (PR #3066, still UNCOMMITTED)
The fix was opened as vuer_css PR #3066 (TechTeamer org, branch fix/FKITDEV-8887-ios-audio-resume). SonarCloud (org techteamer, projectKey vuer-css, host sonarcloud.io — sonar-project.properties) ran on the PR via pull-request.yaml.
Result: Quality Gate PASSED, but the analysis still flagged 18 “New issues” — all maintainability code smells (0 bugs / 0 vulnerabilities / 0 security hotspots; 89.2% new-code coverage).
A passing Quality Gate ≠ zero issues
The gate only checks new-code threshold metrics (coverage %, duplication, rating). It can be GREEN while SonarCloud still reports dozens of code-smell “New issues” — these don’t fail the gate. Don’t read “Quality Gate passed” as “clean.”
Triage of the 18
- 13 genuinely in this PR’s new code → fixed (left uncommitted in worktree
/Users/levander/coding/facekom/vuer_css-FKITDEV-8887-ios-audio-resume):- 11×
prefer-optional-chaining(a && a.b→a?.b) acrossPeer.js,VideoFeed.js,InterruptionRecovery.js,videochat.services.js,videochat.script.js. - 1×
prefer-globalThis—script.jsInterruptionRecoverywiring:win: window→win: globalThis. - 1×
cognitive-complexity—videochat.services.jsrecoverAudioIfNeededreduced 19 → ≤15.
- 11×
- 5 PRE-EXISTING, not touched by this PR → intentionally LEFT (user decision, to keep the audio PR focused; deferred to a separate chore): document-upload / validation code (
promptUpload/validationResult) invideochat.script.js~L424–448. Verified viagh pr diff 3066thatvideochat.script.jsonly changed at (a) theInterruptionRecoveryimport, (b) theInterruptionRecoverywiring ~L78–92, and (c) one teardown line — i.e. nowhere near L424–448.
Pre-existing code can surface as PR "New issues"
vuer_css has no analyzed
develbaseline — onlypull-request.yamlruns Sonar (no scan on push todevel), so SonarCloud has nothing to diff against and attributes long-standing smells in the touched file to the PR. Confirm true authorship withgh pr diff <n>before “fixing” something the branch never wrote.
recoverAudioIfNeeded complexity refactor (behavior-preserving)
Brought cognitive complexity 19 → ≤15 by extracting 3 private helpers (no behavior change):
_safeLog(event, data)— wraps the repeatedtypeof this.sendLog === 'function'guard (DRY; this guard appeared at every log call)._isAudioTrackDead(track)— theended/mutedliveness predicate._applyRecoveredAudioTrack(current, newTrack, swapped)— the replaceTrack-and-emit tail.
Verified: full unit suite 119 suites / 1045 tests pass, plus an independent adversarial review APPROVE (refactor is behavior-preserving). These changes are still uncommitted with the rest of the fix (analyze gate; commit only on explicit authorization).
Reading SonarCloud PR issues WITHOUT a Sonar token (gh check-run annotations)
SonarCloud posts each issue as a GitHub check-run annotation on the PR head commit — so the full issue list is readable through the GitHub API alone, no SonarCloud login/token needed. Recipe (reusable; documented in 10. Verified gotchas):
# 1. find the SonarCloud check + head SHA (app slug: sonarqubecloud)
gh pr view <n> --repo techteamer/vuer_css --json statusCheckRollup,headRefOid
# → the "SonarCloud Code Analysis" check + headRefOid
# 2. list the check-runs on that commit, grab the SonarCloud run id + annotation count
gh api repos/techteamer/vuer_css/commits/<sha>/check-runs \
--jq '.check_runs[] | {id, name, ann: .output.annotations_count}'
# 3. dump every issue (path, start_line, level, message + deep-link)
gh api repos/techteamer/vuer_css/check-runs/<id>/annotationsThe sonarqubecloud bot also leaves a PR summary comment (issue/coverage/gate totals).
annotation_level: "failure"is issue SEVERITY, not a gate failureEach annotation’s
annotation_level(notice/warning/failure) reflects the issue’s severity, NOT the Quality Gate verdict. Afailure-level code smell on a GREEN-gate PR is normal — don’t mistake it for a blocked check.
Sources
- KB:
facekom-v2/working-on-facekom.md,repos/vuer_css.md,inter-service-comms.md,customization-architecture.md §6–§7,customization-branch-catalog.md,flows.md. - Prior art: FKITDEV-8533 (Generali iOS-Safari WebRTC; 4-site videoOrientExt gating; PR #7893 base
customization/generali-atvilagitas). - Code: live read of
vuer_css+vuer_oss@origin/develandvuer_css@origin/customization/generali-atvilagitas; file:line refs above. Revalidated by 4 independent agents 2026-06-02.