Skip to content

Answer the owner's question: is my sensor OK, and is anything being done about it? - #35

Merged
openipc-ai merged 3 commits into
mainfrom
diagnose-ux
Sep 24, 2026
Merged

openipc-ai merged 3 commits into
mainfrom
diagnose-ux

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

A UX report on the guided defect run said the tab measures well and answers
neither half of the owner's question — "is my sensor OK, and can you fix it?"
It ended by downloading defects.txt with no verdict and nothing applied to
the camera: "an owner who pressed Find the bad pixels is left holding a text
file."
It also found four numbers on one result screen, two of which
disagreed.

Investigating it changed the design three times.

The disagreeing numbers were a real defect

The compare panel dropped the open frame's genuine kept scan and substituted
one built from diag.defects — right in the manual flow, where that is this
frame's reading, and wrong the moment the tally had replaced it with its own
result. The fifth capture became the conclusion, offered back as evidence for
itself:

in-all-5 = #{seen in 5} + #{seen in 4 and absent from capture 5}

The reported 397 was 388 plus nine sites that had missed a capture, listed
as having missed none. It runs the other way too — sites seen only in the last
capture came out at zero and vanished from the table, which is why the rows
stopped summing to the count above them. The same fault fired in the manual
flow the instant Mark was pressed.

No fixture could show it: every existing one plants the same shared sites in
every capture, so nothing is ever seen in exactly four and the two numbers
agree whatever the code does. The new one drops them from the last capture
only. Reverted, it reads "line says 0, table says 12".

A coordinate list cannot be applied to these cameras

The report asked for a "Hide them on this camera" button. The static
defect-pixel table is refused by the chip on these parts and the profile
carries no static table, so there is nowhere to send a list.

But the camera is already correcting them, and can say so

Read from a lab camera over HTTP, unauthenticated:

GET /api/v1/isp/profile.ini →  [ir_static_dpc]
                               DpcEnable   = "1"
                               DpcStrength = "50, 100, 210, ... 255, 220, 220, 152, ..."

So the verdict asks the camera instead of guessing, through a new sensor
provider, and offers to switch the corrector on only where it is off — behind
the hold-and-confirm a calibration already uses. Without a provider it says
such a corrector exists and that this page cannot see it, the way Plates and
Calibrate degrade.

It does not claim the corrector removes them from the video, because that
could not be measured: proving it needs a full-resolution snapshot beside the
raw frame, and the lab camera cannot make one — the JPEG encoder is a buffer
block short at isp.blkCnt: 4 and has no media memory at 5 or above, beside
the H.265 stream. The card says the switch is on and what it is for, and stops.

The rest

One number everywhere, from the threshold. The manual scan and its own
five-capture flow fold under "Advanced — one frame at a time", ending the two
parallel methods and the two buttons called Capture that a tester's script
tripped on. "Where to look" re-reads the frame instead of emptying the panel.
Setup advice is said once, not reprinted after every capture. Black level,
Clipping and Noise lead with a plain judgement and keep their figures behind a
disclosure — the first in this tree, so it is <details>/<summary>, which
bring the keyboard and the screen reader with them.

Four more defects found on the way: Mark silently flipped the exported header's
completeness claim; "the message above says why" was shown when nothing had
been posted above it; the deviation histogram sat under a five-capture headline
describing one frame; and changing the gate mid-run mixed captures read under
different gates into one tally.

Presentation

The rail was flex: 0 0 328px inside a row that never wraps, so at 400px the
picture got 71px of a 400px screen. Under 560px they stack. The tab bar was
nowrap inside flex: 0 0 auto with no overflow, so a fifth tab and the
buttons after it left the screen unreachable; it scrolls now. Tertiary text was
4.32:1 on the panel colour it mostly sits on — under AA, on the class
carrying every piece of explanatory prose — and is 6.12:1 now. Touch targets
reach 44px, where the 720px query had been shrinking them.

The suite had no layout coverage at all, because media queries ask the viewport
and the existing run is one 1280px browser. There is a second 400px pass now.
It earned itself immediately: the first version of the touch rules sat above
.re-seg.re-tight button in the file and lost on source order, so the tab bar
stayed at 25px and the new check said so.

Out of scope

The sensor badge reading "HiSilicon imx335" on a Goke board is the DNG's own
camera-model tag — the editor shows what the file says. The /api/v1/peers 404
is the WebUI shell.

A tester put it plainly: the owner's question is "is my sensor OK, and can you
fix it?", and the tab answered neither part. It ended on a button that
downloaded a list of coordinates, with nothing to say whether the number in it
was bad news -- "an owner who pressed Find the bad pixels is left holding a
text file".

So the run ends on a verdict, and everything else is the working.

Whether it is a lot of stuck pixels is judged two ways, because neither is
enough alone. The arrangement decides what was found at all: a clumped set is
the picture, not the sensor, and that verdict already existed. The count is
then read as a FRACTION of the sensor, against the only published acceptance
figure this tree has -- Sony selling an IMX415 as good with up to 800 white
pixels in the dark, which on a 3864x2192 part is 9.4e-5, rounded here to a
hundredth of a percent. A fraction rather than a count so it carries to sensors
of other sizes.

It is a weak yardstick and the copy says so rather than pretending to a pass
mark. It is also strongly conditional, which matters more: the lab camera
reports 882 sites at 0.5 s and 3921 at 7 s -- dark current doing exactly what
it should -- so a verdict blind to exposure would call one healthy sensor both
fine and faulty within a minute. The exposure and the gain are in the sentence
for that reason.

The other half of the question is the camera's, so the camera is asked. Its
image profile carries the switch for its own defect corrector, and the editor
reads it through a new `sensor` provider: already correcting, or off with an
offer to turn it on behind the same hold-and-confirm a calibration uses.
Without a provider the card says such a corrector exists and that this page
cannot see it, which is the degradation Plates and Calibrate already do.

What is NOT on offer is sending this scan's coordinates to the camera, which is
what the report asked for. The static defect table is refused by the chip on
these parts and the profile carries none, so there is nowhere to put a list.
The corrector the camera runs is automatic and finds its own.

Read from the lab camera, unauthenticated, which is where the copy comes from:

    GET /api/v1/isp/profile.ini
    [ir_static_dpc]
    DpcEnable   = "1"
    DpcStrength = "50, 100, 210, 235, 240, 245, 250, 255, 220, 220, 152, ..."

Note the section is named for the profile in force -- a camera on its night
profile calls it ir_static_dpc -- so the name is found rather than assumed, and
a patch is written back to the section it was read from.

The rest of what the tester listed:

- One number. "Seen in N of 5" comes from the threshold and is used by the
  verdict, the sentence and the button alike; the single-frame count moved
  under the details.
- Two five-capture methods on one tab, and two buttons called Capture -- their
  script pressed the wrong one first. The manual scan and its own keep-and-
  compare flow fold away under "Advanced -- one frame at a time", and the
  wizard's button says "Take picture N of 5".
- "Where to look" threw the reading away and did not take another. It re-reads
  the frame now. The check that required the old behaviour has been rewritten
  rather than deleted, and says why.
- Step 1's seven lines of setup, the "Covered --" finding and a nine-line
  paragraph about dark current were reprinted after every capture. They are
  said once, and between captures there is a one-line reminder and the count.
- Black level, Clipping and Noise lead with a plain judgement and keep their
  figures behind a disclosure. The black-level warning in particular read as a
  fault with no remedy; it now says what it costs the picture and that it is
  the camera's own tuning, not something this page can change.

The disclosure is the first in this tree, so it is <details> and <summary> --
they bring the keyboard and the screen reader with them, and a div with a click
handler brings neither. It is styled off .re-shead, which was already a
heading-and-rule row at fifteen call sites and is the right shape for one.

----

And the numbers it rests on, which did not agree.

A tester ran the guided defect hunt and found four numbers on the result
screen, two of which disagreed: "388 of which have turned up every time" over
a table whose IN ALL 5 row said 397. Both mean the same thing, so one of them
was wrong.

The compare panel drops the open frame's kept scan out of scanSet and puts a
synthetic entry in its place, built from diag.defects. That is right in the
manual flow, where diag.defects IS this frame's own reading. It stops being
right the moment the tally has replaced diag.defects with its own result: the
fifth capture then becomes the conclusion, offered back as evidence for itself.

The arithmetic, exactly. With c(k) the number of real captures holding site k,
the synthetic entry contributes [c(k) >= 4], so the table counted
  in-all-5 = #{c = 5} + #{c = 4 and absent from capture 5}
and the nine sites in that second term had missed a capture while being
reported as having missed none. It runs the other way too: a site seen only in
the last capture came out at zero and vanished from the table altogether, which
is why the rows stopped summing to the count above them -- the fourth number.
The same fault fires in the manual flow the instant Mark is pressed, and asking
for "5 of 5" afterwards asserted that 397 sites turned up in all five when 388
had.

So the tally runs over the captures and never over its own answer: if this
frame has been kept, scanSet already holds its real reading; stand in for it
only when it has not been kept and diag is a genuine single-frame scan.

Four more found while in here:

- Mark dropped `partial` when it rebuilt fromTally, so pressing it turned the
  exported header's "# complete" line from NO to "yes, as far as the scan could
  see" on a click that changed no evidence -- while the warning on screen, which
  reads a different flag, went on saying the opposite.
- "That capture did not arrive. The message above says why." was shown when
  takeFrame returned in silence because the editor was busy, which posts no
  message anywhere. That case has its own sentence now, and the step button is
  disabled until there is a frame and the editor is idle -- it used to be live
  before the first frame had loaded, which is how the tester reached it.
- The deviation histogram is one frame's, and after a tally it rode along on
  the spread of diag under a headline about five captures.
- Changing "where to look" mid-run left scanSet holding captures read under
  different gates, which the tally counted as comparable. The run owns the gate
  while it is open.

The regression test needed a fixture the suite could not produce: every
existing one plants the same shared sites in every capture, so nothing is ever
seen in exactly four and the two numbers agree whatever the code does. The new
one drops the shared sites from the last capture only. Watched failing with the
fix reverted.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add actionable sensor diagnosis and correct defect tallies

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Ends guided defect scans with a consistent verdict and camera correction status.
• Fixes tally evidence reuse, truncation metadata, and gate changes during active runs.
• Improves diagnostic clarity, accessibility, responsive layout, and narrow-viewport regression
 coverage.
Diagram

graph TD
  A["Raw Captures"] --> B["Defect Scan"] --> C["Capture Tally"] --> D{"Sensor Verdict"} --> E["Diagnosis Panel"]
  C --> F["Compare Details"]
  H["Camera Profile"] --> G["Sensor Provider"] --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Host-supplied verdict policy
  • ➕ Allows sensor-specific acceptance limits instead of applying one published fraction universally.
  • ➕ Lets deployments account for product grade, exposure policy, and operational requirements.
  • ➖ Adds configuration and validation burden to every host integration.
  • ➖ Can produce inconsistent verdict language across cameras and installations.
2. Direct camera API integration
  • ➕ Could remove the host adapter for deployments sharing one camera API.
  • ➕ Would make profile reads and correction writes self-contained in the editor.
  • ➖ Couples the editor to authentication, transport, and vendor-specific endpoints.
  • ➖ Reduces portability and makes safe patch, rollback, and testing behavior harder to isolate.

Recommendation: Keep the provider-based camera integration and cautious default fraction used by this PR. It preserves the editor’s host boundary, supports explicit rollback and confirmation, and degrades honestly when camera access is unavailable. A future optional host-supplied verdict threshold would be worthwhile for deployments with validated sensor-specific limits, while direct camera API coupling should be avoided.

Files changed (8) +1198 / -108

Enhancement (4) +192 / -16
editor.cssShip accessible, responsive editor styling +60/-8

Ship accessible, responsive editor styling

• Updates the distributed stylesheet with higher-contrast tertiary text, native disclosure styling, larger touch targets, and a stacked phone layout. The top toolbar becomes horizontally scrollable so all tabs and actions remain reachable on narrow screens.

dist/editor.css

iqprofile.jsShip defect-correction profile helpers +36/-0

Ship defect-correction profile helpers

• Adds helpers that discover day or infrared static-DPC sections, parse correction enablement and strength ranges, and generate the minimal patch required to enable correction.

dist/iqprofile.js

editor.cssImprove editor accessibility and phone layout +60/-8

Improve editor accessibility and phone layout

• Raises explanatory-text contrast, styles native details disclosures, and enlarges compact controls. Adds a 560px breakpoint that stacks the image and inspector while preserving image height and scrollable toolbar access.

src/editor.css

iqprofile.jsParse and enable camera defect correction +36/-0

Parse and enable camera defect correction

• Introduces profile helpers for locating static defect-correction sections, reading enablement and strength values, and creating a section-preserving enable patch. Both day and infrared profile section names are supported.

src/iqprofile.js

Tests (1) +131 / -3
ui-check.htmlCover tally correctness and narrow-screen behavior +131/-3

Cover tally correctness and narrow-screen behavior

• Adds a 400px browser pass that verifies stacking, usable image size, toolbar reachability, and touch-target dimensions. Adds a four-of-five defect fixture that reproduces the compare-table discrepancy and updates gate-change coverage to require automatic rescanning.

tests/ui-check.html

Other (3) +875 / -89
editor.jsShip actionable and internally consistent sensor diagnosis +426/-43

Ship actionable and internally consistent sensor diagnosis

• Adds final sensor verdicts, camera defect-correction status and safe enablement, collapsible diagnostic details, and clearer guided-run states. It also fixes tally self-reuse, truncation propagation, stale gate results, misleading histograms, repeated guidance, and capture timing messages in the distributed editor.

dist/editor.js

editor.jsMake sensor diagnosis verdict-driven and actionable +426/-43

Make sensor diagnosis verdict-driven and actionable

• Reworks the guided defect run to end with a contextual sensor verdict and camera correction state, including temporary enablement with confirm-or-revert behavior. Corrects capture tally inputs and export completeness, prevents mixed scan gates, refreshes stale results, and presents black level, clipping, and noise as plain judgments with collapsible measurements.

src/editor.js

ui-check.mjsRun UI checks at desktop and phone widths +23/-3

Run UI checks at desktop and phone widths

• Launches a second headless browser with a 400px viewport, collects its independent results, and merges both reports. Ensures both browser processes are terminated on completion or failure.

tools/ui-check.mjs

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. The contrast comment contradicts itself ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The palette comment identifies the former tertiary color as #a2a6b2 and assigns it a 4.32:1
contrast, then assigns the same color a 6.12:1 contrast against the same raised background. This
appears whenever maintainers consult the documented palette, obscuring that the replaced color was
#878a94 and making the recorded contrast measurement unreliable.
Code

src/editor.css[R13-15]

+ * The tertiary was #a2a6b2, which is 4.32:1 on the raised #232734 that panels
+ * are drawn in -- under the 4.5:1 minimum, and it carries every piece of
+ * explanatory prose in the editor. #a2a6b2 is 6.12:1 there and 6.77:1 on the
Evidence
The added comment contains two incompatible ratios for the same foreground and background colors,
violating the requirements that numeric and tree-related documentation claims be accurate. The
surrounding palette declaration also identifies #a2a6b2 as the current tertiary color, while the
PR diff shows #878a94 was the value replaced by this change.

CLAUDE.md: Documented Numeric Claims Must Be Measured and Attributed: CLAUDE.md: Documented Numeric Claims Must Be Measured and Attributed: CLAUDE.md: Documented Numeric Claims Must Be Measured and Attributed: CLAUDE.md: Documented Numeric Claims Must Be Measured and Attributed
CLAUDE.md: Comments and Documentation Must Contain Tree-Verified Claims: CLAUDE.md: Comments and Documentation Must Contain Tree-Verified Claims: CLAUDE.md: Comments and Documentation Must Contain Tree-Verified Claims: CLAUDE.md: Comments and Documentation Must Contain Tree-Verified Claims
src/editor.css[11-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The palette comment says `#a2a6b2` has both `4.32:1` and `6.12:1` contrast against `#232734`, while the diff shows that the former tertiary color was actually `#878a94`.
## Fix Focus Areas
- src/editor.css[11-16]
- dist/editor.css[11-16]
## Recommended Fix
Change the former tertiary color in the source comment to `#878a94`, verify each stated ratio with the WCAG contrast formula, and regenerate the committed distribution copy.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Closed editors still alter the camera ✓ Resolved 🐞 Bug ☼ Reliability
Description
The defect-correction rollback uses an independent dpcHold timeout that destroy() does not
cancel. Closing the editor during the confirmation period therefore allows a later sensor.revert()
to modify the camera after the operator has left the workflow.
Code

src/editor.js[R1379-1382]

+					dpcHold = setTimeout(async () => {
+						dpcHold = null;
+						try { await sensor.revert(); } catch (e) { /* going anyway */ }
+						if (mode === 'diagnose') buildDiagnose();
Evidence
Enabling correction creates a separate timer whose callback calls sensor.revert(), but teardown
only stops the existing generic hold. The teardown comment explicitly establishes that camera
rollback countdowns must not outlive their editor.

src/editor.js[1376-1383]
src/editor.js[5072-5093]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new defect-correction hold survives editor destruction and later invokes the camera provider from a closed workflow.
## Fix Focus Areas
- src/editor.js[1379-1383]
- src/editor.js[5072-5093]
## Recommended Fix
Add a shared cancellation helper for `dpcHold`, invoke it from `destroy()`, and guard the timeout callback against running after teardown. Apply the same lifecycle discipline used by the existing calibration hold.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Marking results drops one capture ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new kept || diag.fromTally branch rebuilds all solely from scanSet after the Mark action
sets diag.fromTally. When the current manual frame was included synthetically rather than kept,
marking removes that frame from the comparison and changes the capture count, vote totals, and
threshold without changing the underlying evidence.
Code

src/editor.js[R2345-2348]

+		const kept = scanSet.some((v) => v.id === state.openId);
+		const all = kept || diag.fromTally
+			? scanSet.slice()
+			: scanSet.filter((v) => v.id !== state.openId)
Evidence
Before marking, an unkept current frame is appended to all; the Mark handler then replaces
diag.defects and sets fromTally without adding that frame to scanSet. The next render takes
scanSet.slice(), so M and every table row are recomputed from one fewer capture.

src/editor.js[2345-2358]
src/editor.js[2476-2488]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Converting a manual comparison to marked tally results loses the unkept current-frame entry on the subsequent render.
## Fix Focus Areas
- src/editor.js[2345-2358]
- src/editor.js[2476-2488]
## Recommended Fix
Persist the exact capture set used to create the tally, or preserve the current frame's original scan separately before replacing `diag.defects`. On rerender, use that original evidence while avoiding substitution of the tally survivors as a capture.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (2)
4. Kept correction is switched off anyway ✓ Resolved 🐞 Bug ≡ Correctness
Description
The Keep path calls sensor.keep() and then assigns dpcHold = null without cancelling the timeout
represented by that handle. When the owner presses “Keep it on” and the original hold duration
expires, the still-scheduled callback invokes sensor.revert(), undoing the confirmed camera
correction while the rebuilt card can still claim it is on.
Code

src/editor.js[R1373-1376]

+			try {
+				if (dpcHold) { await sensor.keep(); dpcHold = null; dpc.enabled = true; }
+				else {
+					await sensor.patch(enableDefectCorrection(dpc));
Evidence
The temporary-enable path schedules a timeout whose callback calls sensor.revert(), but the
confirmation branch only nulls dpcHold rather than passing it to clearTimeout, leaving the
callback active. The established calibration implementation cancels its local hold before confirming
with the host, showing that cancelling the timer and confirming the setting are both required.

src/editor.js[1370-1388]
src/editor.js[1371-1383]
src/editor.js[3034-3058]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Pressing “Keep it on” clears only the variable holding the DPC timeout ID, leaving the timeout scheduled to invoke `sensor.revert()` after `sensor.keep()` succeeds and undo the setting the owner explicitly kept.
## Fix Focus Areas
- src/editor.js[1371-1384]
## Recommended Fix
In the keep-action handler, cancel the outstanding timeout with `clearTimeout(dpcHold)` before awaiting `sensor.keep()`, then clear the stored handle. Guard against a race with an already-firing timer by serializing the keep and expiry completion paths or checking an operation generation in the timeout callback, so a superseded or confirmed hold cannot revert the setting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Incomplete scans can be called normal ✓ Resolved 🐞 Bug ≡ Correctness
Description
sensorVerdict() decides the final status from diag.defects.length without receiving or checking
fromTally.partial. When one of the source scans overflowed, finishHunt() deliberately keeps only
the confirmed visible subset but the new normal branch can still label that incomplete subset a
normal sensor result before the separate partial warning is appended.
Code

src/editor.js[R1427-1430]

+		if (frac <= NORMAL_FRACTION)
+			return ['re-ok', `${n} stuck pixels — ${share} of this sensor, scattered at ` +
+				'random, which is what a sensor\u2019s own faults look like. That is a ' +
+				'normal number: parts of this class are sold as good with up to about a ' +
Evidence
The engine distinguishes the complete defect count from the capped coordinate list. The guided run
carries truncation into fromTally.partial, but the verdict uses only the final point-list length
and is rendered before the partial warning.

src/engine.js[232-235]
src/editor.js[1538-1551]
src/editor.js[1399-1435]
src/editor.js[1764-1788]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A guided hunt with a truncated source capture has an unknown number of unexamined defects, yet the final verdict compares only the retained confirmed coordinates with the normal threshold. This can produce a primary “normal” result even though the scan explicitly knows it is incomplete.
## Fix Focus Areas
- src/editor.js[1399-1435]
- src/editor.js[1538-1551]
- src/editor.js[1764-1788]
## Recommended Fix
Pass the tally completeness state into the verdict calculation and return an indeterminate/warning verdict whenever any source capture was truncated. Keep the confirmed-count information and partial-run explanation, but do not classify the sensor as normal or abnormal from an incomplete census.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Some cameras stay on “Asking” forever ✓ Resolved 🐞 Bug ☼ Reliability
Description
readDpc() stores the null returned by readDefectCorrection() when a profile has no recognized
static_dpc / *_static_dpc section, while dpcCard() treats that value as a request still in
progress. Once such a profile has been read, dpcAsked prevents retries and every rebuild continues
to show “Asking the camera…” instead of reporting that correction data is unavailable.
Code

src/editor.js[1307]

+		try { dpc = readDefectCorrection(parseIni(await sensor.profile())); }
Evidence
The parser explicitly returns null when no matching correction section exists, and readDpc()
stores that result after setting dpcAsked. dpcCard() then maps the same falsy value to its
pending message, while the request guard prevents any retry, leaving no path to display the
completed-but-unavailable result.

src/iqprofile.js[64-79]
src/editor.js[1302-1310]
src/editor.js[1330-1334]
src/iqprofile.js[64-70]
src/editor.js[1330-1339]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A profile with no recognized `static_dpc` / `*_static_dpc` section is a completed lookup, but `readDefectCorrection()` returns `null` and `dpcCard()` interprets that value as an in-progress request. Since `dpcAsked` suppresses another request, the panel remains on “Asking the camera…” instead of reporting that correction data is unavailable.
## Fix Focus Areas
- src/editor.js[1302-1339]
- src/iqprofile.js[64-79]
## Recommended Fix
Represent loading, unavailable, successful, and failed correction-data reads as distinct states. Retain `undefined` while the profile request is pending, convert a successful `null` parser result into an explicit unavailable state, and render an explanatory unavailable message rather than the loading message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Later scans report an old camera setting ✓ Resolved 🐞 Bug ≡ Correctness
Description
dpcAsked and dpc are initialized once for the editor but are never reset by startHunt(),
despite the correction status being presented as part of each completed guided run. After an owner
completes one run, changes the camera setting elsewhere, and starts another run, the final card
reuses the old profile result instead of querying the camera again.
Code

src/editor.js[R1300-1302]

+	/* The camera's own answer, read once per run and remembered: it is a
+	 * fetch, and the panel rebuilds on every step. */
+	let dpc = null, dpcAsked = false, dpcHold = null;
Evidence
The new state is described as being read once per run, but its declaration is outside the hunt
lifecycle. startHunt() resets scan state without resetting either DPC value, while completed hunts
always call readDpc() and render the cached card.

src/editor.js[1300-1310]
src/editor.js[1438-1451]
src/editor.js[1757-1773]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The DPC profile result is cached for the complete editor lifetime, but it is shown as the camera's current answer at the end of each guided run. A later run can therefore display stale correction state after the setting changes outside the editor.
## Fix Focus Areas
- src/editor.js[1300-1310]
- src/editor.js[1438-1450]
- src/editor.js[1757-1773]
## Recommended Fix
Reset the DPC result and request guard when a new guided hunt starts, then fetch the profile again when that run reaches its final verdict. Preserve the existing in-flight guard so repeated panel rebuilds during one run do not start duplicate requests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

8. Mobile tab test misses unreachable tabs ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The narrow test compares each tab’s viewport-relative right edge against .re-top.scrollWidth,
which is the total scrollable content width rather than the visible edge. An offscreen tab therefore
passes as long as it lies somewhere within the overflowing content, and the following assertion
checks only the overflow-x style without scrolling to the tab.
Code

tests/ui-check.html[R39-46]

+	t('every tab is reachable rather than off the edge',
+		tabs.length > 0 && tabs.every((b) => {
+			const r = b.getBoundingClientRect();
+			return r.left >= -1 && r.right <= host.querySelector('.re-top').scrollWidth + 1;
+		}), tabs.map((b) => b.textContent).join('|'));
+	const top = host.querySelector('.re-top');
+	t('and the bar can be scrolled to them when it overflows',
+		top.scrollWidth <= top.clientWidth + 1 || getComputedStyle(top).overflowX === 'auto',
Evidence
scrollWidth measures the full content extent, whereas button rectangles are measured relative to
the viewport. The test does not mutate scrollLeft or otherwise exercise the new scrolling behavior
introduced by the mobile CSS.

tests/ui-check.html[38-47]
src/editor.css[225-227]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new narrow-layout test claims every tab is reachable, but it only confirms that the tab lies inside the scrollable content width and that horizontal overflow is configured. It never scrolls the tab bar and verifies that each tab enters the visible client area.
## Fix Focus Areas
- tests/ui-check.html[38-47]
- src/editor.css[225-227]
## Recommended Fix
For each tab, set the top bar's scroll position or call `scrollIntoView()` on the tab, then assert the tab's bounding rectangle lies within the top bar's client rectangle. Retain a separate assertion that overflow is enabled when content exceeds the available width.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can choose which labels appear on a finding, and whether they show icons or text

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/editor.css Outdated
Comment thread src/editor.js
Comment thread src/editor.js
Comment thread src/editor.js Outdated
Comment thread src/editor.js
Comment thread src/editor.js
Comment thread src/editor.js
Comment thread tests/ui-check.html Outdated
Three presentation faults, all of them editor-wide rather than anything to do
with the tab they were found on.

The inspector rail is flex: 0 0 328px with no min-width, inside a row that
never wraps. At 400px that leaves the picture 71px of a 400px screen -- 43px of
it once the stage's own padding is taken -- so a phone showed a strip beside a
panel. Under 560px the two stack instead: the rail goes full width below the
picture, the stage keeps a floor of 42vh, and the rail's left border becomes a
top one. The DOM is already picture-then-rail, so nothing moves.

The tab bar is white-space: nowrap inside .re-top > * { flex: 0 0 auto }, with
no overflow declared anywhere. A fifth tab and the Capture and Download buttons
after it simply left the screen, with nothing to scroll to reach them. The bar
scrolls now.

And the tertiary text colour, #878a94, is 4.32:1 on the raised #232734 that
panels are drawn in -- under the 4.5:1 minimum, on the class that carries every
piece of explanatory prose in the editor. It passes on the rail, at 4.78:1,
which is presumably how it survived. #a2a6b2 is 6.12:1 on the panel and 6.77:1
on the rail. Measured with the WCAG formula, checked against known pairs first
(white on black 21.00, #767676 on white 4.54); two other figures were in
circulation for this and both were wrong.

Touch targets: nothing in the editor reached the 44px guideline -- 32 for a
button, 28 for a segment, 25 for the main tab bar, 16 for a reset. Worse, the
720px query REDUCED button padding and stripped their labels, making them
smaller on exactly the devices most likely to be touched. Under 560px the
interactive minimum is 44px, and that query no longer shrinks anything.

The check for this needed a second browser. The rules are media queries and a
media query asks the viewport, so shrinking a host element inside the existing
1280px run cannot reach them; the harness now launches a 400px pass that runs a
short layout-only page and reports separately. It earned itself immediately:
the first version of the touch-target rules sat ABOVE .re-seg.re-tight button
in the file and lost to it on source order at equal specificity, so the tab bar
stayed at 25px and the new check said so.

Also, re-ok and re-warn were told apart by an icon colour and a 1px border at
60% alpha -- and most call sites handed both variants the same warning
triangle, so a good answer arrived wearing one. There is a tick now.
"Diagnose" is a verb with no object. It never said what was being diagnosed, so
an owner wondering whether their sensor has stuck pixels had no reason to guess
they were behind it -- and the tab's whole job, after the guided run went in,
is to answer exactly that question. It is "Bad pixels" now.

The other labels on that bar are Develop, Calibrate, Plates and Focus: three of
them name the thing you are working on. This one does too.

The mode key stays `diagnose`. It is internal, it appears at around forty
sites, and renaming it would be churn with nothing visible at the end of it.
The checks move to the new label, which is also the first real exercise of the
scrollable tab bar from the commit below -- "Bad pixels" is two characters
wider than what it replaces.

----

Review of the three commits below found eight things, and the two that matter
most were in the part that writes to the camera.

Pressing "Keep it on" assigned null to the hold's timer handle without
cancelling the timer it named. The clock ran on, and when it expired it put
back a change the owner had just confirmed -- while the card, reading
dpc.enabled, went on saying the correction was on. It is cleared first now,
before keep() is called at all.

And that hold outlived the editor. Closing the page inside it left a timer that
still fired and still called revert(), changing a camera whose operator had
gone. destroy() cancels it and puts the change back at once instead: the hold
exists because somebody is there to keep it, so once nobody is, there is
nothing left to wait for.

Two in the tally, one of them mine from two commits down. "Use scanSet when the
frame is kept or the reading is a tally" is right after a guided run, where
keepCurrentScan has already put the frame in. In the manual flow it has not,
and the reading on screen stands in for it -- so Mark threw that stand-in away,
dropped the capture count by one, and moved the votes and the suggested
threshold with it, on a click that changed no evidence. A tally carries the
frame's own keys now. And the verdict did not know about truncation: a run
where a capture filled the store has a short list, by an unknown amount in a
known place, so the fraction taken from it could be read as "normal" off the
very evidence that went missing. It says what it does not know instead.

Three about the camera card. A profile with no correction section read as a
request still in flight, and since dpcAsked blocks the retry the card said
"Asking the camera..." for as long as the editor stayed open; that is an answer
now, with its own sentence. The reading was never asked for twice, so a second
run reported the first one's state though the setting can change between them.
And the palette comment named the wrong colour -- a blanket replace of the old
value rewrote the sentence describing it, leaving a comment claiming #a2a6b2
was both 4.32:1 and 6.12:1 against the same background.

The eighth was a check, and it was worthless in the way its own comment now
admits: it compared each tab's right edge against the bar's scrollWidth, which
is the width of the whole scrollable content -- an offscreen tab is inside that
by definition, so it would have passed with the bar still overflowing off the
screen, which is the fault it was written for. It scrolls to each tab and
checks it lands in the visible box.
@openipc-ai
openipc-ai merged commit 7aa9616 into main Sep 24, 2026
1 check passed
@openipc-ai
openipc-ai deleted the diagnose-ux branch September 24, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant