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_oss only 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 behind origin/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 on customization/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 on devel, forward-merge to both customization/generali-atvilagitas and customization/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.devicechange listener (route change) — enumerateDevices is called only on-demand (DeviceHandler.js:375), never reactively
  • ❌ no track.onmute/onunmute recovery; the only track-ended listener (VideoFeed.js:66) just tears the stream down (removeStream), it does not recover
  • ❌ no pc.restartIce() / renegotiation on audio loss — oniceconnectionstatechange / onsignalingstatechange exist but only log/emit (potential hook points)
  • ❌ no AudioContext resume in the call path (AudioContext lives only in system-check / self-service VolumeMeter.js:25)

Architecture constraints (affect the fix)

  • Client never talks to Janus. Media plane = Janus in vuer_oss (server/service/JanusService.ts); vuer_css strips roomData.janus before 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-side restartIce()/renegotiation can’t reach Janus directly — server-assisted recovery touches vuer_oss. A client-only fix is limited to re-acquiring getUserMedia + replaceTrack on the live sender, re-enabling tracks, and forcing remote <video>.play() on resume.
  • connectionStateRecovery is 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.js derives isIOS = /ios/i.test(os.name) via UAParser → FALSE on iPadOS 13+ (desktop “Mac OS X” UA). BUT the existing workarounds gate on isSafari || isIOS (client) and isSafari || isMobile (server), and isSafari is TRUE on iPad → they do fire on iPad. Lesson for our fix: gate recovery on isSafari (catches iPhone+iPad, and desktop Safari harmlessly) or a client-side navigator.maxTouchPoints check — not isIOS alone.

Affected files (vuer_css, line numbers verified @origin/devel)

  • client/features/webrtc/DeviceHandler.jsgetUserMedia (L19), audio-device persistence (L8–9, ~L282), constraints getNormalStream (L162–233), getLocalStream (L243–288), on-demand enumerateDevices (L375)
  • client/features/webrtc/SenderPeer.js — local stream → PC addStream (L52). (No retry/timeout logic here — earlier claim corrected; file is 115 lines.)
  • client/features/webrtc/ReceiverPeer.js — remote stream onaddstream (L52) + ontrack (L60–68)
  • client/features/videochat/VideoFeed.js — remote srcObject+play() (L49–52); track-ended teardown (L66); mute removeTrack/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.jsisIOS/isSafari UA 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:

  1. Remote playback (primary, version-independent): remote <video> has playsinline autoplay (RemoteUser.twig L6-10); .play() is called once in onloadedmetadata (VideoFeed.js L49-52). iOS pauses the backgrounded element; nothing re-play()s it on foreground (no visibilitychange/focus) → customer hears nothing.
  2. Socket masks the implicit recovery: Socket.IO connectionStateRecovery (30 s) silently restores the session on short backgrounds, so the reconnectwindow.location.reload() path (auth.js L40-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 when connectionStateRecovery was enabled — possible config-introduced contributor.)
  3. 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'removeStream teardown (VideoFeed.js L63-70), no re-acquire.
  4. No connection-level recovery: ICE/signaling handlers only log; stats watcher disabled (Peer.js L133-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 = visibilitychangeplay()+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

  1. 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.
  2. Detection (Safari-gated, additive): add visibilitychange+pageshow+focus (unlock/foreground) and navigator.mediaDevices.devicechange (route change); gate on isSafari/maxTouchPoints (covers iPad). Add track.onmute/onunmute/onended on local+remote audio, logged via the existing webrtclog channel for field diagnosis. Reuse the oniceconnectionstatechange hook in Peer.js.
  3. Client-side recovery first: on resume/devicechange re-acquire local audio (getUserMedia audio) + replaceTrack on the live SenderPeer; re-enable tracks; force remote <video>.play() (iOS pauses media on background). Extend the existing VideoFeed teardown/mute machinery rather than duplicating.
  4. Server-assisted escalation (only if client-only fails): ICE restart / renegotiation must propagate browser ↔ CSS Socket.IO ↔ OSS ↔ Janus — design the videochat signalling + transport RPC for it; this makes vuer_oss an affected repo.
  5. 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.
  6. 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 — addStream present; SenderPeer “retry/timeout” claim was FALSE.
  • Absence (adversarial, tried to refute): A1–A6 all STAND @origin/devel. Only recovery-adjacent code = VideoFeed.js:66 ended→teardown and :150–153 mute 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. connectionStateRecovery enabled, socket-only 30 s. 8533 videoOrientExt gating CONFIRMED at all 4 vuer_oss sites. iPad-UA claim CORRECTED (isSafari catches iPad).
  • Generali transfer: CONFIRMED — customization/generali-atvilagitas has zero Generali-specific audio code; DeviceHandler.js/SenderPeer.js/ReceiverPeer.js are 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 RTCPeerConnection getSenders().
  • VideoChatService.recoverAudioIfNeeded() — re-acquire getUserMedia({audio:true}) + replaceTrack when local audio track is ended/muted; re-entrancy guard + try/catch; logs senderPeer:audioRecovered[:error].
  • InterruptionRecovery controller (DI) — on visibilitychange/pageshow/focus (when visible): ensurePlaying() each remote feed + recoverAudioIfNeeded(). State-based, not UA-gated (sidesteps the Macintosh-UA/isIOS trap).
  • ✅ Wired into videochat.script.js (instantiate+start after services.videoChat; stop() in the videochat:close teardown).
  • 🔴 Critical bug caught by final review + fixed: SenderPeer.pc is a Peer (WildEmitter) wrapper, NOT an RTCPeerConnection; the real connection is Peer.pc. The original replaceAudioTrack called this.pc.getSenders() → always false → mic recovery was a no-op in production; a unit test stubbing pc.getSenders masked it. Fixed via a Peer.replaceAudioTrack proxy + a regression test driving the real SenderPeer→Peer→RTCPeerConnection chain.
  • ✅ Verified independently: full yarn test:unit = 119 suites, 1030 passed / 46 skipped / 0 failures; yarn lint clean. (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 calls this.localMedia.startLocalMedia({ audio: true, video: false }) instead of navigator.mediaDevices.getUserMedia({ audio: true }).
  • Early-out guard extended: if (!this.senderPeer || !this.localMedia) (was !this.senderPeer only).
  • 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.localMedia immediately after services.videoChat.setDictionary(dict) (~line 81), where services.localMedia is 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 the describe() block AND before any require() of the module under test. The SenderPeer/Peer module reads document.body.getAttribute() at require-time via the config module — calling the helper inside a beforeAll or beforeEach is too late and will cause the config read to throw.

Files changed:

  • Created test/tests/unit/_helpers/webrtc-test-globals.js: shared CommonJS helper exporting setupWebrtcTestGlobals() that assigns global.document (body.getAttribute returns browser config JSON) and global.RTCPeerConnection mock.
  • Updated test/tests/unit/client/features/webrtc/peer.test.js: replaced inline global setup with require('../../../_helpers/webrtc-test-globals') + setupWebrtcTestGlobals() at top, before describe.
  • 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: uses require('../../../../_helpers/webrtc-test-globals'); all test cases migrated from navigator.mediaDevices.getUserMedia mocks to svc.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 on isSafari/maxTouchPoints, never isIOS.
  • 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: settleddevel, 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/devel first): 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:resumesenderPeer: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-isIOS gating (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.iosonar-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.ba?.b) across Peer.js, VideoFeed.js, InterruptionRecovery.js, videochat.services.js, videochat.script.js.
    • prefer-globalThisscript.js InterruptionRecovery wiring: win: windowwin: globalThis.
    • cognitive-complexityvideochat.services.js recoverAudioIfNeeded reduced 19 → ≤15.
  • 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) in videochat.script.js ~L424–448. Verified via gh pr diff 3066 that videochat.script.js only changed at (a) the InterruptionRecovery import, (b) the InterruptionRecovery wiring ~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 devel baseline — only pull-request.yaml runs Sonar (no scan on push to devel), so SonarCloud has nothing to diff against and attributes long-standing smells in the touched file to the PR. Confirm true authorship with gh 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 repeated typeof this.sendLog === 'function' guard (DRY; this guard appeared at every log call).
  • _isAudioTrackDead(track) — the ended/muted liveness 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>/annotations

The sonarqubecloud bot also leaves a PR summary comment (issue/coverage/gate totals).

annotation_level: "failure" is issue SEVERITY, not a gate failure

Each annotation’s annotation_level (notice/warning/failure) reflects the issue’s severity, NOT the Quality Gate verdict. A failure-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/devel and vuer_css @ origin/customization/generali-atvilagitas; file:line refs above. Revalidated by 4 independent agents 2026-06-02.