Skip to content

Fix/interface bugs - #36

Merged
anderstorstensson merged 18 commits into
mainfrom
fix/interface-bugs
Aug 26, 2026
Merged

anderstorstensson merged 18 commits into
mainfrom
fix/interface-bugs

Conversation

@anderstorstensson

Copy link
Copy Markdown
Member

No description provided.

persist_settings() rewrote the whole settings.json from the caller's list,
so call sites that only pass a subset of keys (class list upload, MATLAB
ZIP export) silently erased every other saved setting. Settings then
reverted to defaults on the next app start.

Add update_settings_file(), a read-merge-write helper (NULL removes a
key), and route persist_settings() through it so partial writes are safe
regardless of which keys a call site knows about.
Startup restore and auto-save of the class list were gated on
grepl("sqlite", save_format), which is FALSE for the value "both".
With 'Both SQLite and MAT' selected, class list edits were never written
to the database and never restored on the next start, even though
annotations were being saved to that same database. Use
save_format %in% c("sqlite", "both") like every other call site.

Add a testServer regression test covering both the auto-save and the
startup restore paths.
Predict rebaselines original_classifications to the prediction result,
so the current-vs-original diff used to protect manually reclassified
images forgot every relabel made before an earlier prediction. A second
Predict click therefore re-classified those images, silently overwriting
the user's corrections.

Also protect every file recorded in the changes log, which tracks all
manual relabels since the sample was loaded and survives sample caching,
and count skipped images against the files actually present.
External class review sets current_sample to the synthetic name
"__external_review__", but leaving class review mode only cleared the
class_review_* fields. Switching back to Sample Mode and loading a
sample (or closing the app after relabeling) then ran the auto-save
path, which wrote a bogus __external_review__ sample into the
annotations database, polluted the Class Review filter dropdowns, and
copied the external PNGs into the export folder.

Clear the loaded review data when leaving class review mode, and guard
save_to_cache() and the session-end save loop against class review
state as defense in depth.
The Rematch Unmatched handler assigned build_worms_match_rows() results
(9 columns) into an explicit list of 7 column names, so [<-.data.frame
always errored ('provided 9 variables to replace 7 variables') and the
button never updated the match table. Assign by the result's own column
names instead, which also keeps the positional alignment correct.
save_sample_annotations() swallowed every error into the same FALSE it
returns for an empty changes log, so a failed save (e.g. MAT backend
with Python unavailable) was indistinguishable from 'nothing to save'
and surfaced in the UI as a benign warning. Let errors propagate — all
callers (Save button, navigation autosave, session cleanup) already
wrap the call in tryCatch with proper error reporting. The temp folder
is now cleaned up via on.exit so it is removed on failure too.
The Save button derived adc_folder solely from the ROI file map and
aborted with 'Cannot find ROI data folder' whenever the sample had no
.roi file — even for SQLite saves, which never use adc_folder. PNG-only
samples could therefore only be saved implicitly via navigation
autosave, which already had a PNG-folder fallback.

Expose find_sample_png_dir() from the sample loading module, use it as
the same fallback in the Save button handler, and only require an ADC
folder when a MAT export is actually requested. The 'no changes'
notification now reflects that failures are reported separately (save
errors propagate and show as errors since the previous commit).
The drag-select rubber band (#selection-box) is position:fixed, i.e.
viewport-anchored, but was positioned with pageX/pageY (document
coordinates). Once the gallery was scrolled, the box appeared offset
below the cursor by the scroll distance and selected the wrong images.
Use clientX/clientY to match the fixed positioning.

updateCardStyle() compared card.data('relabeled') to the string 'true',
but jQuery's .data() auto-converts 'true' to a boolean, so the check
always failed and deselecting a relabeled image dropped its yellow
border. Read the attribute directly instead.
Three gallery fixes:

- Empty image lists: images[1:0, ] on a 0-row data frame returns a
  phantom 1-row all-NA frame (1:0 is c(1, 0)), so the 'No images to
  display' branch was unreachable and page_info printed 'Page 1/1
  (1-0 of 0)'. Return an explicit empty pagination result instead.

- Page navigation desync: the pagination clamp was display-only, so
  after the image list shrank (e.g. relabeling away the last page of a
  filtered class) rv$current_page stayed out of range and the first
  prev-page click did nothing. Navigate from the clamped page actually
  displayed.

- Missing score column: a classification CSV without a score column
  made the caption code evaluate if (logical(0)), killing the whole
  gallery render. Backfill score in load_from_csv() like the other
  optional columns, and guard the call site.
With zero images loaded, the annotation-mode indicator computed
round((0/0)*100) = NaN, and sprintf('%d%%', NaN) is an R error, so the
whole title bar failed to render (taking the mode-switch link with it).
Show 0% when there are no images.
load_from_cache() had two related defects:

- It never cleared rv$cached_validation_classifications (unlike
  finalize_sample_load), so switching a cached sample to validation
  mode could restore the previous sample's classifications into it —
  every tile showed 'Not found' and a save would have written sample
  A's rows under sample B's name.

- It mutated classifications/current_sample before checking that the
  ROI file still exists, so a failed cache load (moved/unmounted data)
  left the gallery pointing at the previous sample's image folder.

Validate the image source first, clear the stale validation snapshot,
and reset the page to 1 like every other sample-entry path.
A failed or empty model fetch (endpoint down, sleeping HF space) cleared
the cfg_prediction_model widget, and Apply then persisted the empty
value - opening Settings while offline and saving anything wiped the
model and left the Predict button permanently disabled. Failure paths
now keep the previously configured model as the only choice, and
apply_settings falls back to the stored model when the select sends
NULL. Deliberately clearing the Gradio URL still clears the model.
The app modules use base R's %||% (added in 4.4.0). On older R this
surfaced as a raw 'could not find function' error at runtime instead of
an installation-time message.
- An emptied instrument filter exported the entire database: NULL from
  a cleared multi-select was treated as 'no filter', the opposite of
  the widget's 'remove to exclude' semantics. When more than one
  instrument exists (i.e. the filter widget was shown), an empty
  selection now aborts with a warning. With a single instrument the
  widget is never rendered and NULL still means no filter.

- Classes added during PNG import were appended to rv$class2use but
  never pushed to the 'Relabel to:' dropdown, so they were unusable
  until the class editor was opened. Refresh the dropdown like every
  other class2use mutation site.

- Dashboard export loops did a bare 'next' when the skip-class filter
  emptied a sample, so the exported/failed/skipped counts did not sum
  to the samples processed. Count those samples as skipped.
With 'Rename classes' checked, the AphiaID map was keyed by the new
accepted names while the taxonomy lookups passed to save_worms_map()
stayed keyed by the old class names, so save_class_taxonomy_db()
resolved NA for every renamed class - their scientific_name,
accepted_name and accepted_aphia_id columns were left empty and the
EcoTaxa hierarchy export was blank for exactly those classes. Re-key
the match rows by the same effective names as the map.
The summary table gated on any score being present, but computed
mean/min/max per class with na.rm = TRUE, so a class whose images all
lack scores (e.g. manually relabeled before prediction) rendered as
NaN%, Inf% and -Inf%. Such classes now show NA.
- Random/prev/next updated the server-side sample dropdown with a bare
  updateSelectizeInput(selected=), which can only select values already
  on the client (capped at ~1000 options). With larger filtered lists
  the dropdown went blank and (for random) the following Load click was
  swallowed. Route all of them through update_sample_list(), which does
  a full server-side update and already consumes pending_sample_select.

- populate_from_index() forced the year filter back to the earliest
  year and the instrument filter to All on every rescan, so saving an
  unrelated setting yanked the user's filters. Preserve current
  selections when still valid (isolated reads, so the scan observer
  gains no new reactive dependencies).

- The annotated-samples cache observer persisted the reactive value
  even when the startup scan had bailed early and it still held its
  empty initial value, stripping the on-disk index of all annotation
  markers. Skip persisting until samples have been discovered.

- Local sample loads emitted two 'N images' toasts with different
  counts (pre- and post-extraction filter). Only the post-filter count
  is shown now, matching dashboard loads.

- Session-end autosave passed the current sample's PNG folder for
  every cached sample, so PNG/MAT export silently failed for all but
  the last-viewed one. Cache entries now carry their own PNG folder.
  The cleanup also no longer deletes the persistent dashboard PNG
  cache (same startsWith guard as every other cleanup site), and an
  empty annotator is normalized to 'Unknown' in both autosave paths
  instead of writing ''.
@anderstorstensson
anderstorstensson merged commit da9712a into main Aug 26, 2026
11 of 12 checks passed
@anderstorstensson
anderstorstensson deleted the fix/interface-bugs branch August 26, 2026 19:15
anderstorstensson added a commit that referenced this pull request Aug 27, 2026
* docs: add NEWS entries for interface bug review (#36)

* perf: make gallery image selection instant

Selecting images (click, drag-select, or the Select Page/All and
Deselect All buttons) re-rendered the entire page of image cards on the
server for every selection change, greying out the gallery while Shiny
recomputed the HTML and the browser rebuilt the DOM - purely to apply a
border color the client-side JS had already drawn.

- gallery renderUI now isolates rv$selected_images; click/drag styling
  is handled client-side as before, so selection changes no longer
  invalidate the render
- Select Page / Select All / Deselect All sync card styling via a new
  setSelectedCards custom message instead of a full re-render
- relabeling and all sample/mode switches also reassign
  rv$classifications in the same observer, so those re-renders pick up
  the cleared selection with no extra sync
- remaining renders are faster: per-card which() scan over
  original_classifications replaced with a named-vector lookup built
  once per render, and per-card selection membership vectorized

* fix: keep gallery card size constant across selection states

The selected (3px blue) and relabeled (3px yellow) borders made cards
4px wider/taller than the default 1px border, so selecting an image
could push the last card of a flex row onto the next line, shifting
image positions mid-selection.

Move all card visual state from inline styles (server render +
updateCardStyle JS) into .image-card CSS rules where border + padding
always sum to 8px, so state toggles change color only, never geometry.
This also lets the JS drop updateCardStyle and the data-relabeled
attribute entirely: deselecting simply reveals the class-based yellow
border again.
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