Skip to content

Searchable drop-downs#142

Merged
ewowi merged 12 commits intomainfrom
repo-includes
Mar 12, 2026
Merged

Searchable drop-downs#142
ewowi merged 12 commits intomainfrom
repo-includes

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Mar 8, 2026

Summary by CodeRabbit

  • New Features

    • Split performance metrics into Effects, Drivers, and overall LPS.
    • Searchable dropdown component for selecting effects, palettes and files.
    • New pure-type light definitions and virtual→physical mapping types.
    • Modular PhysicalLayer/VirtualLayer APIs with double-buffering support.
    • 1D/2D blur utilities for virtual layers.
  • Improvements

    • Expanded drawing primitives: lines, circles, text, blending and palette previews.
    • Coordinate normalization refactor and separate WebGL renderer.
    • Added category metadata for many nodes and palettes.
  • Tests

    • New native and web test suites validating headers, mapping, utilities and merge logic.

@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 152f132c-3d28-4d21-8b12-3057aff751b4

📥 Commits

Reviewing files that changed from the base of the PR and between 002f973 and ec450e6.

📒 Files selected for processing (4)
  • interface/src/routes/moonbase/monitor/Monitor.svelte
  • interface/src/routes/moonbase/monitor/monitor_webgl.ts
  • lib/framework/WWWData.h
  • platformio.ini

Walkthrough

Adds pure physical types (LightsHeader, PhysMap), refactors PhysicalLayer/VirtualLayer APIs and mapping flow, splits monitor WebGL into utility + WebGL module, splits analytics into three LPS metrics with cycle instrumentation, adds node category metadata and UI dropdown, and introduces unit tests and utilities.

Changes

Cohort / File(s) Summary
Physical types & layer API
src/MoonLight/Layers/LightsHeader.h, src/MoonLight/Layers/PhysMap.h, src/MoonLight/Layers/PhysicalLayer.h, src/MoonLight/Layers/PhysicalLayer.cpp
Adds LightsHeader and PhysMap pure-type headers; moves/light-related typedefs out of PhysicalLayer.h; exposes PhysicalLayer public API (lights, mapping flags, mutexes, per-pin config) and triggers pass2 via requestMapVirtual.
VirtualLayer features
src/MoonLight/Layers/VirtualLayer.h, src/MoonLight/Layers/VirtualLayer.cpp
Refactors mapping to use PhysMap, expands VirtualLayer API (mapping table ownership, set/get, drawing primitives, blur utilities, isMapped, lifecycle hooks).
Mapping control & Node flow
src/MoonBase/Nodes.h, src/MoonBase/NodeManager.h, src/MoonLight/Modules/ModuleDrivers.h, src/MoonLight/Modules/ModuleEffects.h
Adds Node::category(), NodeManager::addNodeValue(), splits requestMappings into separate requestMapPhysical/requestMapVirtual flows, updates module registration calls and guards onSizeChanged.
Analytics split & instrumentation
lib/framework/AnalyticsService.h, lib/framework/ESP32SvelteKit.h, lib/framework/ESP32SvelteKit.cpp, lib/framework/SystemStatus.cpp, src/main.cpp
Replaces single lps with lps_all/lps_effects/lps_drivers; adds cycle-accurate accumulation around effects and drivers loops and updates telemetry emission and reset logic.
UI/Types/Stores
interface/src/lib/types/models.ts, interface/src/lib/stores/analytics.ts, interface/src/routes/system/metrics/SystemMetrics.svelte, interface/src/routes/system/status/SystemStatus.svelte
Analytics type and store updated to three LPS fields; metrics chart and status view changed to display three datasets (all/effects/drivers).
WebGL monitor split
interface/src/routes/moonbase/monitor/monitor.ts, interface/src/routes/moonbase/monitor/monitor_webgl.ts, interface/src/routes/moonbase/monitor/Monitor.svelte
monitor.ts reduced to pure normalizePosition utility; full WebGL pipeline moved to monitor_webgl.ts exposing scene state and rendering API; Monitor.svelte imports new modules and adapts calls.
UI components & utilities
interface/src/lib/components/moonbase/SearchableDropdown.svelte, interface/src/lib/components/moonbase/FieldRenderer.svelte, interface/src/lib/stores/moonbase_utilities.ts
Adds reusable SearchableDropdown component, positionDropdown and extractEmojis utilities; FieldRenderer adapted to use dropdown for palettes and select-file UI.
Data merge utility
interface/src/routes/moonbase/module/module.ts, interface/src/routes/moonbase/module/Module.svelte
Extracts updateRecursive into module.ts with in-place recursive merge semantics; Module.svelte now imports it.
Tests & docs
test/test_native/test_layers.cpp, interface/src/test/*.test.ts, docs/system/status.md
Adds native doctest suite for LightsHeader/PhysMap and TS tests for updateRecursive, normalizePosition, extractEmojis; docs updated to reflect split LPS metrics.
Config & palettes
platformio.ini, src/MoonLight/Modules/palettes.h, src/MoonLight/Modules/ModuleLightsControl.h
Updates FastLED reference and adds -Isrc include flags; simplifies palette name strings and infers palette category from emoji in names.
Node metadata additions
src/MoonLight/Nodes/**/ (many D_*, E_*, L_*, M_*, *_Sandbox headers)
Adds static category() methods across drivers, effects, layouts, modifiers to supply category strings used by UI registration.
Searchable dropdown & positioning
interface/src/lib/components/moonbase/SearchableDropdown.svelte, interface/src/lib/stores/moonbase_utilities.ts
Exports dropdown API (values/isSelected/onSelect/toggle) and positioning/emoji helpers used by FieldRenderer.

Sequence Diagram(s)

sequenceDiagram
    participant EffectsTask as Effects Task
    participant PhysicalLayer as PhysicalLayer
    participant VirtualLayer as VirtualLayer
    participant ChannelBuf as Channel Buffers
    participant DriversTask as Drivers Task

    EffectsTask->>PhysicalLayer: onLayout/loop (pass1)
    PhysicalLayer->>PhysicalLayer: mapLayout() (build physical map)
    PhysicalLayer->>PhysicalLayer: requestMapVirtual = true
    PhysicalLayer->>VirtualLayer: onLayoutPost()
    VirtualLayer->>VirtualLayer: createMappingTable() (pass2)
    VirtualLayer->>ChannelBuf: write mapped channel data (channelsE/channelsD)
    DriversTask->>PhysicalLayer: loopDrivers()
    PhysicalLayer->>ChannelBuf: read channels -> hardware
Loading
sequenceDiagram
    participant UI as UI
    participant NodeMgr as NodeManager
    participant NodeType as Node<T>
    participant Control as Control JSON

    UI->>NodeMgr: addNodeValue<T>(control)
    NodeMgr->>NodeType: T::getNameAndTags()
    NodeMgr->>NodeType: T::category()
    NodeMgr->>Control: append { name, category } to control.values
    Control-->>UI: UI renders categorized options (SearchableDropdown)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through headers, tidy and bright,
Split maps and pixels by moonlit night,
Three LPS streams now drum and play,
Dropdowns search the palettes' way,
A rabbit cheers: compile, test, delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Repository includes and unit tests' clearly describes the main purpose of the PR: adding includes, extracting utilities, and introducing unit tests across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch repo-includes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 46-49: The physical and virtual remap requests must be coupled:
when requesting a physical remap (requestMapPhysical) also request the virtual
remap (requestMapVirtual) so pass 1 and pass 2 run together; update the
API/state in PhysicalLayer (the uint8_t flags
requestMapPhysical/requestMapVirtual and the startup of loopDrivers()) to either
merge into a single requestMapAll flag or ensure any set of requestMapPhysical
automatically sets requestMapVirtual, and document this dependency so
VirtualLayer::onLayoutPre can rely on layerP->lights.header.size being
synchronized during pass 2.

In `@src/MoonLight/Layers/VirtualLayer.cpp`:
- Around line 363-377: Loop counters and the x parameter in VirtualLayer::blur1d
(and the similar routines around lines 385-421) use uint16_t which wraps above
65,535; change the parameter type x and the row/column loop counters from
uint16_t to a wider unsigned type (e.g., uint32_t or size_t) and update the
corresponding declarations in VirtualLayer.h to match, then ensure all uses of
size.x and size.y comparisons use the same wider type so getRGB, setRGB and
addRGB calls address the full layout without overflow while preserving the
existing unmapped-path bounds-checking behavior.

In `@src/MoonLight/Layers/VirtualLayer.h`:
- Around line 129-131: The unmapped one-to-one path currently performs a
multiply/compare bounds check ((indexV + 1) *
layerP->lights.header.channelsPerLight <= layerP->lights.maxChannels) which adds
a branch and alters semantics by dropping out-of-range writes; remove this
bounds check from the direct pass-through branch so callback(indexV) is invoked
branchlessly for unmapped layouts, and if you want a safety net implement an
optional debug-only check gated by a compile-time or runtime debug flag (e.g.,
WRAP_DEBUG_BOUNDS) that performs the multiply/compare and logs or asserts
instead of silently dropping writes; update the VirtualLayer.h branch where
callback(indexV) is called and ensure any mapping-aware paths (e.g.,
lightPreset_RGB2040) remain unchanged.

In `@test/test_native/test_layers.cpp`:
- Around line 18-22: The test uses the offsetof macro in the "LightsHeader field
offsets" test case but <cstddef> isn't included, causing compilation errors; add
a top-level include for <cstddef> in the test file (near the existing includes
for "doctest.h", "MoonLight/Layers/LightsHeader.h",
"MoonLight/Layers/PhysMap.h") so offsetof is declared and the test compiles.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1ded911d-e674-44de-afa1-6e56d89d9c0c

📥 Commits

Reviewing files that changed from the base of the PR and between 4067cfd and 88a500e.

📒 Files selected for processing (8)
  • platformio.ini
  • src/MoonLight/Layers/LightsHeader.h
  • src/MoonLight/Layers/PhysMap.h
  • src/MoonLight/Layers/PhysicalLayer.cpp
  • src/MoonLight/Layers/PhysicalLayer.h
  • src/MoonLight/Layers/VirtualLayer.cpp
  • src/MoonLight/Layers/VirtualLayer.h
  • test/test_native/test_layers.cpp

requestMapPhysical implicitly calls requestMapVirtual
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/MoonLight/Layers/PhysicalLayer.h (1)

93-94: Update the monitorPass comment to match the actual behavior.

addLight() stores positions during pass 1 regardless of monitorPass. What monitorPass actually changes here is whether pass 1 resets/reassigns physical pin state. I’d reword this comment so it describes that distinction directly.

💡 Suggested wording
-  // When true, addLight() also stores positions in channelsE for transmission to the UI Monitor.
+  // When true, pass 1 is being run for monitor/layout inspection only;
+  // physical pin assignment state is preserved instead of being rebuilt.
   bool monitorPass = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/PhysicalLayer.h` around lines 93 - 94, The comment for
the bool monitorPass is inaccurate: update it to state that addLight() always
stores positions in channelsE during pass 1, and that monitorPass controls
whether pass 1 also resets or reassigns the physical pin state; reference
monitorPass, addLight(), pass 1, channelsE and the physical pin state so the
comment clearly explains that monitorPass toggles pin state reset/reassignment
rather than whether positions are recorded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 93-94: The comment for the bool monitorPass is inaccurate: update
it to state that addLight() always stores positions in channelsE during pass 1,
and that monitorPass controls whether pass 1 also resets or reassigns the
physical pin state; reference monitorPass, addLight(), pass 1, channelsE and the
physical pin state so the comment clearly explains that monitorPass toggles pin
state reset/reassignment rather than whether positions are recorded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5d529798-0258-4610-9fd0-c551af1f75e2

📥 Commits

Reviewing files that changed from the base of the PR and between 88a500e and 8bed3a7.

📒 Files selected for processing (5)
  • src/MoonBase/Nodes.h
  • src/MoonLight/Layers/PhysicalLayer.cpp
  • src/MoonLight/Layers/PhysicalLayer.h
  • src/MoonLight/Modules/ModuleDrivers.h
  • test/test_native/test_layers.cpp
💤 Files with no reviewable changes (1)
  • src/MoonLight/Modules/ModuleDrivers.h

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/MoonLight/Layers/PhysicalLayer.h (1)

121-122: Consider adding unit tests when gamma correction is implemented.

As per coding guidelines, gamma8() is a pure, side-effect-free function that should have unit tests. While a no-op implementation doesn't need testing, once real gamma correction logic is added, tests should be included.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/PhysicalLayer.h` around lines 121 - 122, The
gamma8(uint8_t b) declaration is currently a no-op but will be a pure,
side-effect-free mapping once implemented; add unit tests for the function
(tests for input/output mapping) once you implement the real gamma correction.
Create tests exercising edge cases (0 and 255), a few representative mid-values,
and verify correct rounding/quantization behavior and monotonicity; additionally
include a regression test that the function preserves length and type and is
deterministic (same input => same output). Use the test framework already in the
repo and reference the gamma8 symbol from PhysicalLayer.h when locating the
implementation to test.
interface/src/routes/system/metrics/SystemMetrics.svelte (1)

120-142: Consider using theme colors for visual consistency.

The new LPS datasets use hard-coded RGB values while other charts (heap, PSRAM, filesystem, temperature) use daisyColor() for theme-aware styling. This creates visual inconsistency when users switch themes.

🎨 Suggested theme-aware colors
 {
-  label: 'All lps',
-  borderColor: 'rgb(30, 80, 200)',
-  backgroundColor: 'rgba(30, 80, 200, 0.15)',
+  label: 'All lps',
+  borderColor: daisyColor('--color-primary'),
+  backgroundColor: daisyColor('--color-primary', 15),
   borderWidth: 2,
   data: $analytics.lps_all,
   yAxisID: 'y'
 },
 {
-  label: 'Effects lps',
-  borderColor: 'rgb(200, 30, 30)',
-  backgroundColor: 'rgba(200, 30, 30, 0.15)',
+  label: 'Effects lps',
+  borderColor: daisyColor('--color-secondary'),
+  backgroundColor: daisyColor('--color-secondary', 15),
   borderWidth: 2,
   data: $analytics.lps_effects,
   yAxisID: 'y'
 },
 {
-  label: 'Drivers lps',
-  borderColor: 'rgb(30, 160, 30)',
-  backgroundColor: 'rgba(30, 160, 30, 0.15)',
+  label: 'Drivers lps',
+  borderColor: daisyColor('--color-accent'),
+  backgroundColor: daisyColor('--color-accent', 15),
   borderWidth: 2,
   data: $analytics.lps_drivers,
   yAxisID: 'y'
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/system/metrics/SystemMetrics.svelte` around lines 120 -
142, The three LPS datasets ('All lps', 'Effects lps', 'Drivers lps' using data:
$analytics.lps_all, $analytics.lps_effects, $analytics.lps_drivers) are using
hard-coded RGB values; replace those hard-coded borderColor and backgroundColor
values with theme-aware calls to daisyColor() (matching the pattern used by the
other charts) so the chart respects light/dark themes—update the borderColor and
backgroundColor for each dataset to use appropriate daisyColor variants (e.g.,
primary/ danger/ success or similar) and keep borderWidth and yAxisID unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.cpp`:
- Around line 139-143: Telemetry counters lps_all, lps_effects_cycles, and
lps_drivers_cycles are updated from multiple cores (e.g., in the block around
layerP.loop() and in driverTask) and can race with ESP32SvelteKit::_loop() which
resets them; make those counters thread-safe by changing their types to
std::atomic<uint32_t> (or protect increments/resets with a short critical
section) and update all references (increments in the loop where layerP.loop()
and esp_cpu_get_cycle_count() are used and resets in ESP32SvelteKit::_loop()) to
use atomic operations or the same critical section to avoid inconsistent
telemetry.

In `@src/MoonLight/Layers/VirtualLayer.h`:
- Line 133: The log call uses a misspelled variable name indevV causing
compilation failure; change the argument in the EXT_LOGD(ML_TAG, "%d", indevV)
call to the correct variable name indexV so the logging statement references the
defined variable (EXT_LOGD, ML_TAG, indexV).

---

Nitpick comments:
In `@interface/src/routes/system/metrics/SystemMetrics.svelte`:
- Around line 120-142: The three LPS datasets ('All lps', 'Effects lps',
'Drivers lps' using data: $analytics.lps_all, $analytics.lps_effects,
$analytics.lps_drivers) are using hard-coded RGB values; replace those
hard-coded borderColor and backgroundColor values with theme-aware calls to
daisyColor() (matching the pattern used by the other charts) so the chart
respects light/dark themes—update the borderColor and backgroundColor for each
dataset to use appropriate daisyColor variants (e.g., primary/ danger/ success
or similar) and keep borderWidth and yAxisID unchanged.

In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 121-122: The gamma8(uint8_t b) declaration is currently a no-op
but will be a pure, side-effect-free mapping once implemented; add unit tests
for the function (tests for input/output mapping) once you implement the real
gamma correction. Create tests exercising edge cases (0 and 255), a few
representative mid-values, and verify correct rounding/quantization behavior and
monotonicity; additionally include a regression test that the function preserves
length and type and is deterministic (same input => same output). Use the test
framework already in the repo and reference the gamma8 symbol from
PhysicalLayer.h when locating the implementation to test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0150b0e1-957a-4cd6-90f5-08389a4d0ece

📥 Commits

Reviewing files that changed from the base of the PR and between 8bed3a7 and 32a6c1b.

📒 Files selected for processing (13)
  • interface/src/lib/stores/analytics.ts
  • interface/src/lib/types/models.ts
  • interface/src/routes/system/metrics/SystemMetrics.svelte
  • interface/src/routes/system/status/SystemStatus.svelte
  • lib/framework/AnalyticsService.h
  • lib/framework/ESP32SvelteKit.cpp
  • lib/framework/ESP32SvelteKit.h
  • lib/framework/SystemStatus.cpp
  • lib/framework/WWWData.h
  • src/MoonLight/Layers/PhysicalLayer.h
  • src/MoonLight/Layers/VirtualLayer.h
  • src/MoonLight/Modules/ModuleEffects.h
  • src/main.cpp

- Virtual layer: forEachLightIndex without check: up to developers to protect
- Module effects/drivers: addNode onSizeChanged only if
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/system/status.md`:
- Line 5: Replace the misspelled word "theoritical" with "theoretical" in the
sentence describing performance in status.md (the phrase "Effects and drivers is
the theoritical speed" should read "Effects and drivers is the theoretical
speed"); update the surrounding grammar if needed to keep the sentence clear and
consistent with the rest of the doc.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2e764e92-2d6c-449a-beb5-30e3a9c21137

📥 Commits

Reviewing files that changed from the base of the PR and between 32a6c1b and 57cfccd.

📒 Files selected for processing (4)
  • docs/system/status.md
  • src/MoonLight/Layers/VirtualLayer.h
  • src/MoonLight/Modules/ModuleDrivers.h
  • src/MoonLight/Modules/ModuleEffects.h

firmware
========
- latest FastLED

frontend
=======
- Field renderer: add custom palette dropdown
- palettes: remove hearts
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
interface/src/lib/components/moonbase/FieldRenderer.svelte (2)

145-148: Register the outside-click handler only when the palette dropdown is relevant.

FieldRenderer is reused for every property type, but this mounts a global window listener for every instance whether it ever renders a palette or not. On pages with many fields, every click now fans out through a stack of no-op handlers. Prefer attaching it only while property.type === 'palette' && paletteOpen, or move the palette UI into its own component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/lib/components/moonbase/FieldRenderer.svelte` around lines 145
- 148, The global mousedown listener is currently registered for every
FieldRenderer instance in onMount regardless of whether a palette is present;
update the logic so the window.addEventListener/removeEventListener for
closePaletteOnOutsideClick is only attached when the field is a palette and the
palette is open (i.e., when property.type === 'palette' && paletteOpen).
Implement this by watching paletteOpen (or using a reactive statement) inside
FieldRenderer.svelte to add the listener when it becomes true and remove it when
false, or alternatively extract the palette UI into its own component so only
that component mounts the window listener; reference the existing onMount and
closePaletteOnOutsideClick symbols when making the change.

225-247: Finish the listbox pattern or remove the ARIA override.

The entries are rendered as buttons, but role="option" only makes sense inside a role="listbox" with matching trigger and keyboard behavior. Right now assistive tech gets mixed semantics compared with the native <select> this replaced. Either keep these as plain buttons, or wire up the full listbox pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/lib/components/moonbase/FieldRenderer.svelte` around lines 225
- 247, The rendered entries use role="option" but are inside a plain div and use
button semantics; either remove the ARIA override from the option buttons (drop
role="option" and keep them as plain buttons with click handlers in the
property.values loop) or implement the full listbox pattern: make the dropdown
container act as a listbox (add role="listbox" and proper
aria-labelledby/aria-activedescendant), wire the trigger to manage
aria-expanded/aria-haspopup, manage keyboard navigation
(ArrowUp/ArrowDown/Home/End, Enter/Space to select, Escape to close) updating
value and aria-selected on the item elements, and ensure paletteOpen, value, and
onChange handlers (the onclick closure that sets value=index; paletteOpen=false;
onChange(event)) are used to synchronize state and focus behavior; pick one
approach and update FieldRenderer.svelte accordingly (affecting the div
container and the button elements rendered from property.values).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/lib/components/moonbase/FieldRenderer.svelte`:
- Around line 211-250: The palette control ignores the disabled prop allowing
the dropdown to open and to change value; update the button and option handlers
to honor disabled: in the main toggle button (which manipulates paletteOpen) and
in each option button (which sets value, closes paletteOpen, and calls
onChange), add checks for a disabled boolean (and set aria-disabled/disabled
attributes on the buttons) so clicks do nothing when disabled, and ensure
onChange is only invoked when not disabled; reference the variables paletteOpen,
value, onChange, property.values, and the option rendering block to locate where
to add these guards and attributes.

---

Nitpick comments:
In `@interface/src/lib/components/moonbase/FieldRenderer.svelte`:
- Around line 145-148: The global mousedown listener is currently registered for
every FieldRenderer instance in onMount regardless of whether a palette is
present; update the logic so the window.addEventListener/removeEventListener for
closePaletteOnOutsideClick is only attached when the field is a palette and the
palette is open (i.e., when property.type === 'palette' && paletteOpen).
Implement this by watching paletteOpen (or using a reactive statement) inside
FieldRenderer.svelte to add the listener when it becomes true and remove it when
false, or alternatively extract the palette UI into its own component so only
that component mounts the window listener; reference the existing onMount and
closePaletteOnOutsideClick symbols when making the change.
- Around line 225-247: The rendered entries use role="option" but are inside a
plain div and use button semantics; either remove the ARIA override from the
option buttons (drop role="option" and keep them as plain buttons with click
handlers in the property.values loop) or implement the full listbox pattern:
make the dropdown container act as a listbox (add role="listbox" and proper
aria-labelledby/aria-activedescendant), wire the trigger to manage
aria-expanded/aria-haspopup, manage keyboard navigation
(ArrowUp/ArrowDown/Home/End, Enter/Space to select, Escape to close) updating
value and aria-selected on the item elements, and ensure paletteOpen, value, and
onChange handlers (the onclick closure that sets value=index; paletteOpen=false;
onChange(event)) are used to synchronize state and focus behavior; pick one
approach and update FieldRenderer.svelte accordingly (affecting the div
container and the button elements rendered from property.values).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: be3cb441-da3d-42f0-80e4-bc4a4c56c88d

📥 Commits

Reviewing files that changed from the base of the PR and between 57cfccd and 7ce8d5d.

📒 Files selected for processing (4)
  • interface/src/lib/components/moonbase/FieldRenderer.svelte
  • lib/framework/WWWData.h
  • platformio.ini
  • src/MoonLight/Modules/palettes.h
✅ Files skipped from review due to trivial changes (1)
  • src/MoonLight/Modules/palettes.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • platformio.ini

ewowi added 3 commits March 9, 2026 17:49
firmware
========
- pio.ini: latest FastLED

frontend
=======
- FieldRenderer: improve palette dropdown
- Module: extract unit test on updateRecursive
- Monitor: extract unit test on normalizePosition
frontend
=======
- FieldRenderer: import SearchableDropdown, add selectedNode, use in selectFile and palette
- Add SearchableDropdown
- moonbase utilities: add positionDropdown and extractEmojis

backend
=======
- Nodemanager: add addNodeValue(name, category)
- Nodes: Node::category
- ModuleDrivers/Effects: addControlValue -> addNodeValue
- Lights control: palette: add category to value
- All Nodes: add category()
@ewowi
Copy link
Collaborator Author

ewowi commented Mar 10, 2026

@coderabbitai resume review

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

@ewowi: Sure! Resuming the review of PR #142.


🧠 Learnings used
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-03T20:55:04.582Z
Learning: Applies to {src/routes/moonbase/**/*.{ts,tsx},src/lib/components/moonbase/**/*.{ts,tsx},src/lib/stores/moonbase_*.{ts,tsx},src/lib/types/moonbase_*.{ts,tsx}} : Add unit tests for pure, side-effect-free TypeScript functions in MoonLight-specific frontend files
✅ Actions performed

Reviews resumed.

@ewowi
Copy link
Collaborator Author

ewowi commented Mar 10, 2026

@coderabbitai, please review 85c68f3, 2e8ff85 and 10c4331

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
REPO="MoonModules/MoonLight"

echo "=== COMMIT 1: 85c68f332c0e48a31d787bb079334c47eae23cc4 ==="
gh api repos/$REPO/commits/85c68f332c0e48a31d787bb079334c47eae23cc4 --jq '{message: .commit.message, files: [.files[] | {filename, status, additions, deletions, patch}]}'

echo ""
echo "=== COMMIT 2: 2e8ff854d1b62b0730c75026b493f9e7a7736397 ==="
gh api repos/$REPO/commits/2e8ff854d1b62b0730c75026b493f9e7a7736397 --jq '{message: .commit.message, files: [.files[] | {filename, status, additions, deletions, patch}]}'

echo ""
echo "=== COMMIT 3: 10c433197c64047d8eb823ccfb1a4cab39b70483 ==="
gh api repos/$REPO/commits/10c433197c64047d8eb823ccfb1a4cab39b70483 --jq '{message: .commit.message, files: [.files[] | {filename, status, additions, deletions, patch}]}'

Length of output: 127631


@ewowi: Here's a review of the three commits.


Commit 1 — 85c68f332: Frontend extraction + tests

✅ Strengths

  • Extracting updateRecursive into interface/src/routes/moonbase/module/module.ts and normalizePosition into monitor.ts (WebGL code → monitor_webgl.ts) is an excellent separation of concerns and directly enables the unit testing pattern from the project guidelines.
  • The 18-case moonbase_module.test.ts suite covers all branches of updateRecursive well (primitives, arrays, nested objects, pruneMissing, p/name identity change, etc.).
  • The 8-case moonbase_monitor.test.ts suite verifies axis inversion, dimension-collapse behaviour, and the 2D-strip case.
  • Fixing the import path in moonbase_utilities.test.ts (./moonbase_utilities../lib/stores/moonbase_utilities) was a necessary correction.

Commit 2 — 2e8ff854: Code style fixes

✅ No issues. for (let key in …)for (const key in …) is the correct ESLint/TypeScript convention since the loop variable is never reassigned. Formatting-only changes elsewhere are clean.


Commit 3 — 10c433197: SearchableDropdown + category system

✅ Strengths

  • The new SearchableDropdown.svelte is a solid reusable component, replacing duplicated custom dropdown logic in FieldRenderer.svelte. Search + category tabs + tag cloud is a great UX improvement.
  • Extracting positionDropdown and extractEmojis into moonbase_utilities.ts is the right move.
  • The category() static method addition across all Node classes is a large but clean mechanical change.
  • blur1d/blurRows/blurColumns loop variables changed from uint16_tnrOfLights_t is the correct fix for consistency with size.x/size.y types.
  • Doc typo fix: "theoritical" → "theoretical" ✅.
  • Improved monitorPass comment in PhysicalLayer.h is much clearer.

⚠️ Issues found:

1. positionDropdown and extractEmojis are missing unit tests

Both functions in moonbase_utilities.ts are pure and side-effect-free (well, positionDropdown touches the DOM, but extractEmojis is fully pure). Per the project's CLAUDE.md guideline, extractEmojis in particular should have unit tests added alongside moonbase_utilities.test.ts.

// Example cases worth adding:
it('extracts single emoji', () => {
  expect(extractEmojis('🔥 Fire')).toEqual(['🔥']);
});
it('extracts multiple emojis', () => {
  expect(extractEmojis('🐙♫ Effect')).toEqual(['🐙', '♫']);
});
it('returns empty array for no emojis', () => {
  expect(extractEmojis('Plain text')).toEqual([]);
});

2. _idx collision risk in SearchableDropdown.svelte

$: filtered = values
  .map((v: any, i: number) => ({ ...v, _idx: i }))

If any source object in values already has an _idx property, the spread will override it silently with the iteration index. Since _idx is also passed to the item slot as part of val, callers could observe unexpected _idx values. Consider using a non-colliding property name (e.g., __dropdownIndex) or keeping the index separate from the spread.

3. ARIA: role="option" on <button> elements

In SearchableDropdown.svelte:

<button
  type="button"
  role="option"
  aria-selected={isSelected(val, val._idx)}
  ...
>

A <button> has the implicit ARIA role of "button". Overriding it with role="option" is non-conformant — option elements are expected to be descendants of a listbox or select, and screen readers may behave unexpectedly. The containing <div role="listbox"> is correct, but consider using <div role="option"> (styled and made keyboard-navigable) instead of <button role="option">, or alternatively accept the button pattern and remove role="option" in favour of a different selection signal. At minimum, keyboard navigation (ArrowUp/ArrowDown/Enter/Escape) is currently absent from the dropdown.

4. Dynamic Tailwind class top-[4.75rem] may be purged

In SearchableDropdown.svelte:

$: tagTop = hasCategoryRow ? 'top-[4.75rem]' : 'top-10';

This dynamically composed class string won't be picked up by Tailwind's static content scanner, meaning it could be missing from the production CSS bundle. You should add it to the Tailwind safelist in tailwind.config.*, or restructure to use a static class.

5. Minor: ModuleEffects.h — inline LiveScript entry construction

The .sc file handler constructs {name, category} JSON objects inline instead of using addNodeValue:

JsonObject entry = control["values"].as<JsonArray>().add<JsonObject>();
entry["name"] = (const char*)file.path();
entry["category"] = "LiveScript";

This is reasonable since .sc files aren't typed Node subclasses, so a helper can't template on them. Just noting it's intentionally different.


Summary table:

# Severity File Issue
1 Medium moonbase_utilities.ts extractEmojis lacks unit tests
2 Low SearchableDropdown.svelte _idx property collision risk
3 Medium SearchableDropdown.svelte ARIA role="option" on <button> + no keyboard nav
4 Medium SearchableDropdown.svelte Dynamic Tailwind class top-[4.75rem] may be purged

🧠 Learnings used
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-03T20:55:04.582Z
Learning: Applies to {src/routes/moonbase/**/*.{ts,tsx},src/lib/components/moonbase/**/*.{ts,tsx},src/lib/stores/moonbase_*.{ts,tsx},src/lib/types/moonbase_*.{ts,tsx}} : Add unit tests for pure, side-effect-free TypeScript functions in MoonLight-specific frontend files

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-26T14:59:22.091Z
Learning: In MoonModules/MoonLight PR `#114`: ModuleDevices deadlock caused by: (1) StatefulService.h line 204-205 bug - after 500ms mutex timeout, continues WITHOUT acquiring mutex, causing race conditions and JSON corruption. (2) receiveUDP() while-loop processes own UDP broadcasts via loopback, creating circular call chain: sendUDP→broadcast→receiveUDP→updateDevices→update→handlers→sendUDP. (3) updateDevices() holds _accessMutex during expensive sort/JSON operations with 15 devices, can exceed 500ms with nested calls. Solution: Fix StatefulService to wait forever or reboot on timeout; skip own broadcasts by checking message.name == ownHostname; optimize updateDevices to sort outside mutex.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 136
File: src/MoonBase/Modules/ModuleDevices.h:148-148
Timestamp: 2026-03-04T10:07:41.583Z
Learning: In MoonModules/MoonLight, the `// 🌙` (MoonBase addition) and `// 💫` (MoonLight addition) comment markers are ONLY required when modifying upstream SvelteKit files (i.e., files that originate from the upstream ESP32-sveltekit project). Do NOT require or suggest these markers for MoonBase-specific or MoonLight-specific files (such as `src/MoonBase/Modules/ModuleDevices.h` or other files in `src/MoonBase/` or `src/MoonLight/`). The markers indicate where upstream files have been patched.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
interface/src/routes/moonbase/monitor/Monitor.svelte (1)

94-94: ⚠️ Potential issue | 🟠 Major

Pass depth to setMatrixDimensions for 3D matrices.

The depth variable is parsed from the header (line 92) but not passed to setMatrixDimensions. For 3D LED matrices, this will cause incorrect MVP calculations as matrixDepth will default to 1.

🔧 Proposed fix
-		setMatrixDimensions(width, height);
+		setMatrixDimensions(width, height, depth);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/moonbase/monitor/Monitor.svelte` at line 94, The code
parses a depth variable for 3D matrices but calls setMatrixDimensions(width,
height) without passing depth; update the call to setMatrixDimensions(width,
height, depth) (and ensure the setMatrixDimensions function signature and any
callers accept an optional third parameter) so matrixDepth is correctly set for
3D LED matrices, referencing the setMatrixDimensions invocation and the parsed
depth variable.
🧹 Nitpick comments (9)
interface/src/routes/moonbase/monitor/monitor_webgl.ts (1)

63-76: Consider null checks for buffer creation.

gl.createBuffer() can return null. While uncommon, adding null checks would prevent cryptic errors if the WebGL context is in a degraded state.

🛡️ Optional defensive fix
 	// Set up position buffer
-	positionBuffer = gl.createBuffer();
+	positionBuffer = gl.createBuffer()!;
+	if (!positionBuffer) throw new Error('Failed to create position buffer');
 	const positionAttributeLocation = gl.getAttribLocation(program, 'aPosition');
 	gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
 	gl.enableVertexAttribArray(positionAttributeLocation);
 	gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);

 	// Set up color buffer
-	colorBuffer = gl.createBuffer();
+	colorBuffer = gl.createBuffer()!;
+	if (!colorBuffer) throw new Error('Failed to create color buffer');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts` around lines 63 - 76,
positionBuffer and colorBuffer are assigned via gl.createBuffer(), which can
return null; add null checks after each createBuffer call (for positionBuffer
and colorBuffer) and avoid calling
gl.bindBuffer/gl.enableVertexAttribArray/gl.vertexAttribPointer if the buffer is
null — instead log an error (or throw/return early) with context (e.g., "failed
to create position buffer" / "failed to create color buffer") so the degraded
WebGL context is handled safely and subsequent calls (like gl.bindBuffer and
gl.vertexAttribPointer) do not receive a null buffer.
interface/src/lib/components/moonbase/SearchableDropdown.svelte (1)

106-119: Consider adding keyboard navigation for accessibility.

The dropdown has good ARIA attributes but lacks keyboard support (Escape to close, Arrow keys to navigate, Enter to select). This is optional for initial implementation but would improve accessibility compliance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte` around lines
106 - 119, Add keyboard navigation to the SearchableDropdown component:
implement a keydown handler (e.g., handleKeydown) wired to the dropdown root
(dropdownEl) and the trigger button so that Escape closes the menu (set
open=false or call toggle()), ArrowDown/ArrowUp move focus/active index through
the option list (maintain an activeIndex state and focus the corresponding
option DOM node), and Enter selects the focused option (call the existing
selection handler). Use the existing symbols toggle(), open, dropdownEl and the
component's option rendering logic to update focus/activeIndex and selection;
ensure handler is attached/removed on mount/unmount and respects disabled state.
interface/src/test/moonbase_utilities.test.ts (1)

1-45: Add tests for extractEmojis utility.

The extractEmojis function is a pure, side-effect-free utility that should have test coverage. Consider adding tests such as:

describe('extractEmojis', () => {
	it('extracts single emoji', () => {
		expect(extractEmojis('Hello 🌙')).toEqual(['🌙']);
	});

	it('extracts multiple emojis', () => {
		expect(extractEmojis('🔥 Fire 💧 Water')).toEqual(['🔥', '💧']);
	});

	it('returns empty array for no emojis', () => {
		expect(extractEmojis('No emojis here')).toEqual([]);
	});

	it('handles ZWJ sequences', () => {
		expect(extractEmojis('Family: 👨‍👩‍👧')).toHaveLength(1);
	});
});

Also note: Based on learnings, test files should be co-located with source files (e.g., src/lib/stores/moonbase_utilities.test.ts), though this appears to be an existing pattern in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/test/moonbase_utilities.test.ts` around lines 1 - 45, Add unit
tests for the pure utility function extractEmojis in the same test file
moonbase_utilities.test.ts by creating a describe('extractEmojis', ...) block
that asserts expected behavior for single emoji (e.g., 'Hello 🌙' -> ['🌙']),
multiple emojis (e.g., '🔥 Fire 💧 Water' -> ['🔥','💧']), no emojis (e.g., 'No
emojis here' -> []), and handling of ZWJ sequences (e.g., 'Family: 👨‍👩‍👧'
yields one entry); place the new tests alongside the existing initCap and
getTimeAgo tests and import extractEmojis from ../lib/stores/moonbase_utilities
so the test file remains co-located with source.
interface/src/test/moonbase_module.test.ts (1)

1-2: Consider co-locating test with source file.

The test file is placed in src/test/ but per coding guidelines, frontend test files should be co-located with the source they test. Consider moving to src/routes/moonbase/module/module.test.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/test/moonbase_module.test.ts` around lines 1 - 2, Move the test
file so it sits in the same directory as the module under test and rename it to
follow the co-located test naming convention (e.g., module.test.ts); update the
test's import of updateRecursive to a relative import that points to the local
module file instead of the current shared test path, and adjust any test-runner
globs/config if they currently only scan the centralized test directory so
co-located tests are picked up.
src/MoonLight/Modules/ModuleLightsControl.h (1)

299-303: Extract the palette category rule into a helper and test it.

This introduces pure backend string-classification logic directly inside setupDefinition(). Please move it to a small helper such as paletteCategory(const char*) and cover the mapping with a native unit test so future palette additions don't silently drift.

♻️ Suggested refactor
-      const char* n = palette_names[i];
-      if (strstr(n, "⚡️")) object["category"] = "FastLED";
-      else if (strstr(n, "🌙")) object["category"] = "MoonModules";
-      else if (strstr(n, "💫")) object["category"] = "MoonLight";
-      else object["category"] = "WLED";
+      object["category"] = paletteCategory(palette_names[i]);

As per coding guidelines, **/*.h: Add unit tests for simple, pure, side-effect-free functions and structs in the backend (e.g. math utilities, coordinate operations, string helpers).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Modules/ModuleLightsControl.h` around lines 299 - 303, Extract
the emoji-based category logic from setupDefinition() into a pure helper
function paletteCategory(const char*) that returns "FastLED", "MoonModules",
"MoonLight" or "WLED" based on the same strstr checks currently using
palette_names[i], then call paletteCategory(n) from setupDefinition() instead of
inlining the if/else; add a native unit test that calls paletteCategory with
representative inputs (strings containing "⚡️", "🌙", "💫" and a string without
those emojis) asserting the expected returned category values to prevent
regressions.
src/MoonLight/Layers/VirtualLayer.cpp (1)

380-421: Yield periodically inside the 2D blur passes.

blurRows() and blurColumns() now do full-grid get/set/add work with no yield point. On larger fixtures that has the same starvation profile as the earlier long-running effects.

♻️ Possible change
 void VirtualLayer::blurRows(fract8 blur_amount) {
   uint8_t keep = 255 - blur_amount;
   uint8_t seep = blur_amount >> 1;
   for (nrOfLights_t row = 0; row < size.y; row++) {
+    addYield(10);
     CRGB carryover = CRGB::Black;
     for (nrOfLights_t col = 0; col < size.x; col++) {
       CRGB cur = getRGB(Coord3D(col, row));
       CRGB part = cur;
@@
 void VirtualLayer::blurColumns(fract8 blur_amount) {
   uint8_t keep = 255 - blur_amount;
   uint8_t seep = blur_amount >> 1;
   for (nrOfLights_t col = 0; col < size.x; ++col) {
+    addYield(10);
     CRGB carryover = CRGB::Black;
     for (nrOfLights_t row = 0; row < size.y; row++) {
       CRGB cur = getRGB(Coord3D(col, row));
       CRGB part = cur;

Based on learnings, “nested loops without yielding starve effectTask; add taskYIELD() inside an outer loop.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/VirtualLayer.cpp` around lines 380 - 421, The blur
passes (VirtualLayer::blurRows and VirtualLayer::blurColumns) perform full-grid
getRGB/setRGB/addRGB work in tight nested loops and must yield periodically to
avoid starving effectTask; insert a taskYIELD() call inside each outer loop
(after finishing each row in blurRows and after finishing each column in
blurColumns, i.e., after the inner loop and before the next iteration) so the
long-running blur operations yield to the scheduler.
src/MoonLight/Layers/VirtualLayer.h (2)

98-98: Consider unit tests for XYZUnModified.

This is a pure, side-effect-free function that maps 3D coordinates to a flat index. Per coding guidelines for header files, it's a good candidate for unit tests to validate edge cases (e.g., {0,0,0}, boundary coordinates, size = {1,1,1}).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/VirtualLayer.h` at line 98, Add unit tests for the pure
function XYZUnModified to validate its 3D-to-flat-index mapping; create tests
that construct Coord3D positions and layer sizes and assert XYZUnModified
returns expected nrOfLights_t values for key cases including origin {0,0,0}, max
boundary coordinates (size.x-1,size.y-1,size.z-1), intermediate coordinates, and
degenerate size {1,1,1}; also include tests for single-axis sizes (e.g.,
size.x>1,size.y=1,size.z=1) and verify behavior with typical small sizes to
catch overflow or arithmetic/order issues in the expression position.x +
position.y * size.x + position.z * size.x * size.y.

129-134: Bounds check correctly removed for branchless hot path.

The bounds check has been appropriately commented out to maintain performance for large LED counts (100K+). This aligns with the project's explicit design decision to prioritize performance over defensive checks in the unmapped path.

Consider removing the commented-out code entirely to keep the codebase clean, as it's now purely historical context.

♻️ Optional cleanup
     } else {                                                                                      // no mapping table — direct pass-through
-      // if ((indexV + 1) * layerP->lights.header.channelsPerLight <= layerP->lights.maxChannels) {  // bounds check
-        callback(indexV);                                                                         // no presetCorrection here (lightPreset_RGB2040 has a mapping)
-      // } else
-      //   EXT_LOGD(ML_TAG, "%d", indexV);
+      callback(indexV);  // no presetCorrection here (lightPreset_RGB2040 has a mapping)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/VirtualLayer.h` around lines 129 - 134, Remove the
now-dead commented-out bounds check clutter around the unmapped hot path: delete
the commented lines that reference the conditional using indexV,
layerP->lights.header.channelsPerLight and layerP->lights.maxChannels (and the
EXT_LOGD(ML_TAG, "%d", indexV) line), and leave the concise comment and the
direct call to callback(indexV) so the else branch is clean and documents the
branchless pass-through behavior.
src/MoonLight/Layers/PhysicalLayer.h (1)

121-122: Consider adding unit tests for gamma8.

gamma8 is a pure function (takes a uint8_t, returns a uint8_t) with no side effects. As per coding guidelines for **/*.h files, pure functions should have unit tests. If the implementation is non-trivial (e.g., lookup table or mathematical transformation), adding a test in test/test_native/test_layers.cpp would ensure correctness across edge cases (0, 127, 255).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/PhysicalLayer.h` around lines 121 - 122, gamma8 is a
pure, no-side-effect function that currently lacks unit tests; add unit tests
that call gamma8(uint8_t) and assert expected outputs for edge cases (0, 127,
255) and a couple of intermediate values, covering any lookup-table or formula
behavior. Implement the tests in the native layer test module (e.g.,
test_layers.cpp) using the project test harness, include necessary headers to
access MoonLight::Layers::gamma8, and ensure the test suite runs as part of CI.
If gamma8's intended behavior is identity for now, assert b == gamma8(b) for
those cases; if a non-trivial mapping is expected, assert the documented
expected values. Ensure tests compile and pass locally before pushing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/lib/stores/moonbase_utilities.ts`:
- Around line 64-72: The function extractEmojis currently returns all regex
matches (matches) including duplicates while its docstring promises unique
emojis; update extractEmojis to deduplicate the matches found by emojiRegex
before returning (e.g., iterate matches and push to a result array or use a Set
while preserving first-seen order), keep the return type string[] and ensure the
function still handles null match results by returning an empty array when
matches is falsy.

---

Outside diff comments:
In `@interface/src/routes/moonbase/monitor/Monitor.svelte`:
- Line 94: The code parses a depth variable for 3D matrices but calls
setMatrixDimensions(width, height) without passing depth; update the call to
setMatrixDimensions(width, height, depth) (and ensure the setMatrixDimensions
function signature and any callers accept an optional third parameter) so
matrixDepth is correctly set for 3D LED matrices, referencing the
setMatrixDimensions invocation and the parsed depth variable.

---

Nitpick comments:
In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte`:
- Around line 106-119: Add keyboard navigation to the SearchableDropdown
component: implement a keydown handler (e.g., handleKeydown) wired to the
dropdown root (dropdownEl) and the trigger button so that Escape closes the menu
(set open=false or call toggle()), ArrowDown/ArrowUp move focus/active index
through the option list (maintain an activeIndex state and focus the
corresponding option DOM node), and Enter selects the focused option (call the
existing selection handler). Use the existing symbols toggle(), open, dropdownEl
and the component's option rendering logic to update focus/activeIndex and
selection; ensure handler is attached/removed on mount/unmount and respects
disabled state.

In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts`:
- Around line 63-76: positionBuffer and colorBuffer are assigned via
gl.createBuffer(), which can return null; add null checks after each
createBuffer call (for positionBuffer and colorBuffer) and avoid calling
gl.bindBuffer/gl.enableVertexAttribArray/gl.vertexAttribPointer if the buffer is
null — instead log an error (or throw/return early) with context (e.g., "failed
to create position buffer" / "failed to create color buffer") so the degraded
WebGL context is handled safely and subsequent calls (like gl.bindBuffer and
gl.vertexAttribPointer) do not receive a null buffer.

In `@interface/src/test/moonbase_module.test.ts`:
- Around line 1-2: Move the test file so it sits in the same directory as the
module under test and rename it to follow the co-located test naming convention
(e.g., module.test.ts); update the test's import of updateRecursive to a
relative import that points to the local module file instead of the current
shared test path, and adjust any test-runner globs/config if they currently only
scan the centralized test directory so co-located tests are picked up.

In `@interface/src/test/moonbase_utilities.test.ts`:
- Around line 1-45: Add unit tests for the pure utility function extractEmojis
in the same test file moonbase_utilities.test.ts by creating a
describe('extractEmojis', ...) block that asserts expected behavior for single
emoji (e.g., 'Hello 🌙' -> ['🌙']), multiple emojis (e.g., '🔥 Fire 💧 Water' ->
['🔥','💧']), no emojis (e.g., 'No emojis here' -> []), and handling of ZWJ
sequences (e.g., 'Family: 👨‍👩‍👧' yields one entry); place the new tests
alongside the existing initCap and getTimeAgo tests and import extractEmojis
from ../lib/stores/moonbase_utilities so the test file remains co-located with
source.

In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 121-122: gamma8 is a pure, no-side-effect function that currently
lacks unit tests; add unit tests that call gamma8(uint8_t) and assert expected
outputs for edge cases (0, 127, 255) and a couple of intermediate values,
covering any lookup-table or formula behavior. Implement the tests in the native
layer test module (e.g., test_layers.cpp) using the project test harness,
include necessary headers to access MoonLight::Layers::gamma8, and ensure the
test suite runs as part of CI. If gamma8's intended behavior is identity for
now, assert b == gamma8(b) for those cases; if a non-trivial mapping is
expected, assert the documented expected values. Ensure tests compile and pass
locally before pushing.

In `@src/MoonLight/Layers/VirtualLayer.cpp`:
- Around line 380-421: The blur passes (VirtualLayer::blurRows and
VirtualLayer::blurColumns) perform full-grid getRGB/setRGB/addRGB work in tight
nested loops and must yield periodically to avoid starving effectTask; insert a
taskYIELD() call inside each outer loop (after finishing each row in blurRows
and after finishing each column in blurColumns, i.e., after the inner loop and
before the next iteration) so the long-running blur operations yield to the
scheduler.

In `@src/MoonLight/Layers/VirtualLayer.h`:
- Line 98: Add unit tests for the pure function XYZUnModified to validate its
3D-to-flat-index mapping; create tests that construct Coord3D positions and
layer sizes and assert XYZUnModified returns expected nrOfLights_t values for
key cases including origin {0,0,0}, max boundary coordinates
(size.x-1,size.y-1,size.z-1), intermediate coordinates, and degenerate size
{1,1,1}; also include tests for single-axis sizes (e.g.,
size.x>1,size.y=1,size.z=1) and verify behavior with typical small sizes to
catch overflow or arithmetic/order issues in the expression position.x +
position.y * size.x + position.z * size.x * size.y.
- Around line 129-134: Remove the now-dead commented-out bounds check clutter
around the unmapped hot path: delete the commented lines that reference the
conditional using indexV, layerP->lights.header.channelsPerLight and
layerP->lights.maxChannels (and the EXT_LOGD(ML_TAG, "%d", indexV) line), and
leave the concise comment and the direct call to callback(indexV) so the else
branch is clean and documents the branchless pass-through behavior.

In `@src/MoonLight/Modules/ModuleLightsControl.h`:
- Around line 299-303: Extract the emoji-based category logic from
setupDefinition() into a pure helper function paletteCategory(const char*) that
returns "FastLED", "MoonModules", "MoonLight" or "WLED" based on the same strstr
checks currently using palette_names[i], then call paletteCategory(n) from
setupDefinition() instead of inlining the if/else; add a native unit test that
calls paletteCategory with representative inputs (strings containing "⚡️", "🌙",
"💫" and a string without those emojis) asserting the expected returned category
values to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d4e62ac-ea1e-41d6-bafb-83058bdd6f01

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce8d5d and 10c4331.

📒 Files selected for processing (43)
  • docs/system/status.md
  • interface/src/lib/components/moonbase/FieldRenderer.svelte
  • interface/src/lib/components/moonbase/SearchableDropdown.svelte
  • interface/src/lib/stores/moonbase_utilities.ts
  • interface/src/routes/moonbase/module/Module.svelte
  • interface/src/routes/moonbase/module/module.ts
  • interface/src/routes/moonbase/monitor/Monitor.svelte
  • interface/src/routes/moonbase/monitor/monitor.ts
  • interface/src/routes/moonbase/monitor/monitor_webgl.ts
  • interface/src/test/moonbase_module.test.ts
  • interface/src/test/moonbase_monitor.test.ts
  • interface/src/test/moonbase_utilities.test.ts
  • lib/framework/WWWData.h
  • platformio.ini
  • src/MoonBase/NodeManager.h
  • src/MoonBase/Nodes.h
  • src/MoonLight/Layers/PhysicalLayer.h
  • src/MoonLight/Layers/VirtualLayer.cpp
  • src/MoonLight/Layers/VirtualLayer.h
  • src/MoonLight/Modules/ModuleDrivers.h
  • src/MoonLight/Modules/ModuleEffects.h
  • src/MoonLight/Modules/ModuleLightsControl.h
  • src/MoonLight/Nodes/Drivers/D_ArtnetIn.h
  • src/MoonLight/Nodes/Drivers/D_ArtnetOut.h
  • src/MoonLight/Nodes/Drivers/D_AudioSync.h
  • src/MoonLight/Nodes/Drivers/D_FastLEDAudio.h
  • src/MoonLight/Nodes/Drivers/D_FastLEDDriver.h
  • src/MoonLight/Nodes/Drivers/D_Hub75.h
  • src/MoonLight/Nodes/Drivers/D_IMU.h
  • src/MoonLight/Nodes/Drivers/D_Infrared.h
  • src/MoonLight/Nodes/Drivers/D_ParallelLEDDriver.h
  • src/MoonLight/Nodes/Drivers/D__Sandbox.h
  • src/MoonLight/Nodes/Effects/E_FastLED.h
  • src/MoonLight/Nodes/Effects/E_MoonLight.h
  • src/MoonLight/Nodes/Effects/E_MoonModules.h
  • src/MoonLight/Nodes/Effects/E_MovingHeads.h
  • src/MoonLight/Nodes/Effects/E_WLED.h
  • src/MoonLight/Nodes/Effects/E__Sandbox.h
  • src/MoonLight/Nodes/Layouts/L_MoonLight.h
  • src/MoonLight/Nodes/Layouts/L_SE16.h
  • src/MoonLight/Nodes/Layouts/L__Sandbox.h
  • src/MoonLight/Nodes/Modifiers/M_MoonLight.h
  • src/MoonLight/Nodes/Modifiers/M__Sandbox.h
✅ Files skipped from review due to trivial changes (1)
  • docs/system/status.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • platformio.ini
  • src/MoonBase/Nodes.h

frontend
=======
- SearchableDropDown: add handleKeydown

firmware
========
- latest FastLED

backend
=======
- remove gamma8()
- unit test: add xyzUnModified
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
interface/src/routes/moonbase/monitor/Monitor.svelte (1)

66-95: ⚠️ Potential issue | 🟠 Major

Reset the layout request gate when a new header arrives.

createScene(el) clears the current geometry, but done stays true. After any reconnect or layout/header change, handleChannels() will no longer call requestLayout(), so positions are never repopulated and the monitor can stay empty.

🔧 Proposed fix
 const handleHeader = (header: Uint8Array) => {
 	console.log('Monitor.handleHeader', header);
+	done = false;
 
 	let view = new DataView(header.buffer);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/moonbase/monitor/Monitor.svelte` around lines 66 - 95,
When a new header arrives handleHeader currently recreates scene via
createScene(el) but never resets the layout gate `done`, so subsequent
handleChannels calls won't call requestLayout(); update handleHeader (near
createScene/el usage) to reset the layout request gate by setting `done = false`
(or invoking the appropriate reset function) immediately after creating the
scene so handleChannels can trigger requestLayout() again on reconnect/layout
changes.
src/MoonLight/Layers/PhysicalLayer.cpp (1)

105-121: ⚠️ Potential issue | 🟠 Major

Consume remap requests atomically.

requestMappings() writes these flags from other tasks while driverTask is inside loopDrivers(). The plain load / = false pattern here can overwrite a new request that arrives during mapLayout(), which leaves layout or virtual mappings stale until some later update retriggers them. Use atomics, or guard both producers and this consumer with the same critical section, and consume the flags with exchange(false)-style semantics.

🔒 Suggested fix
--- a/src/MoonLight/Layers/PhysicalLayer.h
+++ b/src/MoonLight/Layers/PhysicalLayer.h
@@
+  `#include` <atomic>
@@
-  uint8_t requestMapPhysical = false;
-  uint8_t requestMapVirtual = false;
+  std::atomic_bool requestMapPhysical{false};
+  std::atomic_bool requestMapVirtual{false};
-  if (requestMapPhysical) {
+  if (requestMapPhysical.exchange(false, std::memory_order_acq_rel)) {
     EXT_LOGD(ML_TAG, "mapLayout physical requested");
 
     pass = 1;
     mapLayout();
 
-    requestMapPhysical = false;
-    requestMapVirtual = true;  // pass 2 must always follow pass 1 so the virtual mapping table reflects the new physical layout
+    requestMapVirtual.store(true, std::memory_order_release);  // pass 2 must always follow pass 1 so the virtual mapping table reflects the new physical layout
   }
 
-  if (requestMapVirtual) {
+  if (requestMapVirtual.exchange(false, std::memory_order_acq_rel)) {
     EXT_LOGD(ML_TAG, "mapLayout virtual requested");
 
     pass = 2;
     mapLayout();
-
-    requestMapVirtual = false;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/PhysicalLayer.cpp` around lines 105 - 121,
requestMapPhysical and requestMapVirtual are being cleared with plain stores
which can race with requestMappings(); change them to atomic consume semantics
so a new request arriving during mapLayout() isn't lost — e.g. make
requestMapPhysical/requestMapVirtual atomic<bool> (or protect with the same
mutex used by requestMappings()/driverTask/loopDrivers()) and replace the plain
"= false" with an atomic exchange(false) (or perform the flag test-and-clear
under the mutex) around the mapLayout() sequence (pass = 1/2 and mapLayout()) so
producers calling requestMappings() cannot overwrite a concurrent request;
locate symbols requestMapPhysical, requestMapVirtual, requestMappings(),
driverTask, loopDrivers(), mapLayout(), and pass to apply the change.
🧹 Nitpick comments (5)
interface/src/test/moonbase_utilities.test.ts (1)

47-63: Good test coverage for extractEmojis.

The test suite covers the key scenarios. Consider adding a test case for duplicate emojis to verify the uniqueness behavior documented in the function:

💡 Optional: Test for duplicate emoji filtering
+	it('returns unique emojis only', () => {
+		expect(extractEmojis('🔥🔥🔥 triple fire')).toEqual(['🔥']);
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/test/moonbase_utilities.test.ts` around lines 47 - 63, Add a
unit test to verify that extractEmojis filters duplicates: update the
describe('extractEmojis') tests to include a case (e.g., "filters duplicate
emojis") that calls extractEmojis with a string containing repeated emoji
characters (and a mix with non-emoji text) and asserts the returned array only
contains unique emoji entries (or matches the documented uniqueness behavior);
reference the existing extractEmojis function and the describe('extractEmojis')
block when adding this new it(...) test case.
interface/src/lib/components/moonbase/SearchableDropdown.svelte (3)

141-146: Dynamic Tailwind class may be purged in production builds.

The computed class 'top-[4.75rem]' constructed at runtime via the ternary on line 145 may not be detected by Tailwind's purge mechanism. To ensure it's included:

💡 Option 1: Use a safelist in tailwind.config
// tailwind.config.js
module.exports = {
  safelist: ['top-[4.75rem]', 'top-10'],
  // ...
}
💡 Option 2: Use inline style instead
- $: tagTop = hasCategoryRow ? 'top-[4.75rem]' : 'top-10';
+ $: tagTopPx = hasCategoryRow ? '4.75rem' : '2.5rem';

Then in the template:

<div class="... sticky z-10 ..." style="top: {tagTopPx}">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte` around lines
141 - 146, The runtime-computed Tailwind class assigned via the tagTop reactive
variable (defined with $: tagTop and depending on hasCategoryRow, categories and
tags in SearchableDropdown.svelte) may be purged by Tailwind; fix by replacing
the dynamic Tailwind class with a safe approach: either add the two variants
('top-[4.75rem]' and 'top-10') to your Tailwind safelist in tailwind.config.js,
or stop using dynamic classes and compute a numeric pixel value (e.g. tagTopPx)
and apply it via an inline style attribute (style="top: {tagTopPx}") on the
sticky element instead of toggling class names at runtime.

216-234: ARIA role="option" on <button> elements is semantically discouraged.

While functional, placing role="option" on interactive <button> elements conflicts with ARIA expectations where options should be non-interactive children of a listbox. The keyboard navigation implementation is good, but for better screen reader compatibility consider using <div> or <li> elements with tabindex instead.

This is a minor accessibility concern that doesn't block functionality but may affect screen reader announcements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte` around lines
216 - 234, In SearchableDropdown.svelte replace the interactive <button
role="option"> with a non-button option element (e.g., <div> or <li>) that keeps
role="option" and aria-selected={isSelected(val, val._idx)}, preserves the same
class string and slot usage, and moves the click behavior to an onclick handler
that sets open = false and calls onSelect(val, val._idx, event); also add
keyboard activation handling (keydown for Enter/Space) and tabindex="0" so the
element is focusable and activation via keyboard triggers the same onSelect
logic; ensure references to isSelected, onSelect, val._idx, val, and open remain
unchanged.

49-51: Risk of _idx property collision with source objects.

The spread operation { ...v, _idx: i } will overwrite any existing _idx property on source objects, potentially corrupting data passed to onSelect. Consider using a less common property name or keeping the index separate.

💡 Suggested fix using a namespaced property
 $: filtered = values
 	// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-	.map((v: any, i: number) => ({ ...v, _idx: i }))
+	.map((v: any, i: number) => ({ ...v, __searchableDropdown_idx: i }))
 	// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
 	.filter((v: any) => {

Then update all references to _idx__searchableDropdown_idx (or a shorter unique prefix like _sd_idx).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte` around lines
49 - 51, The mapping that creates filtered items currently injects a plain _idx
property which can clobber source objects; change the injected index property to
a namespaced name (e.g. _sd_idx or __searchableDropdown_idx) in the mapping that
builds filtered (in SearchableDropdown.svelte) and update every reference that
reads _idx (including where onSelect is called and any lookup by index) to use
the new namespaced property so the original objects are not mutated or
overwritten.
test/test_native/test_utilities.cpp (1)

369-372: Test the production index helper, not a copied formula.

These cases only exercise the local xyzUnModified() copy, so VirtualLayer::XYZUnModified() can drift and this suite will still stay green. Please extract the formula into a pure shared helper, or otherwise route the test through the production implementation, so the native test covers the shipped code path and its nrOfLights_t semantics too.

Based on learnings, "Add unit tests for simple, pure, side-effect-free functions and structs in the backend (e.g. math utilities, coordinate operations, string helpers)."

Also applies to: 374-422

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/test_native/test_utilities.cpp` around lines 369 - 372, The test
currently calls a local copy xyzUnModified(...) instead of the production
helper, so regressions in VirtualLayer::XYZUnModified() or its nrOfLights_t
semantics won't be caught; replace or redirect the test to use the production
implementation by either (a) moving the index formula into a shared pure helper
function (e.g. a new util function like IndexHelpers::xyzUnModified or
Numeric::XYZIndex) and calling that from both VirtualLayer::XYZUnModified() and
the test, or (b) change the test to call VirtualLayer::XYZUnModified() directly
(constructing a minimal VirtualLayer or exposing a static wrapper) so the test
exercises the shipped code path and its nrOfLights_t behavior instead of the
local copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte`:
- Line 1: The file SearchableDropdown.svelte has Prettier formatting violations;
run the project's Prettier formatter (e.g., run prettier --write on
SearchableDropdown.svelte or run the repo's format script) to reformat the file
according to the project's Prettier config, then stage the updated file so the
pipeline passes; ensure you use the repository's Prettier configuration when
formatting.

In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts`:
- Around line 22-83: createScene currently allocates new GL resources every call
(program, shaders, positionBuffer, colorBuffer) causing GPU leaks; make it
idempotent by checking existing globals (gl, program, positionBuffer,
colorBuffer) and either reuse them or delete them before creating new ones: call
gl.deleteBuffer(positionBuffer)/gl.deleteBuffer(colorBuffer) and
gl.deleteProgram(program)/gl.deleteShader(...) when replacing, or skip creation
if the same program/buffers are already initialized; ensure you update
uMVPLocation after reusing/creating program and re-bind buffers/attributes
(references: function createScene, globals gl, program, positionBuffer,
colorBuffer, uMVPLocation).
- Around line 169-179: The camera padding is too small and causes clipping for
some LED matrix aspect ratios; in the camera distance calculation (using
distanceForHeight and distanceForWidth) update the padding multiplier applied
when computing cameraDistance (currently "* 2.5") to use a factor of 5 so the
cameraDistance = Math.max(distanceForHeight, distanceForWidth) * 5, ensuring
both dimensions have adequate padding.

In `@platformio.ini`:
- Around line 227-233: The default FastLED dependency in lib_deps is pinned to
the 20260312 commit (34ba2f752814550a0d7f8545b7926298fbdc4790) which the comment
notes has slower WebSocket throughput; revert the active lib_deps entry to the
previously tested/stable commit (20260309,
9b195b4ff9d315d9754398d14962ad7cfb764e53) so the default build uses the verified
revision, and move the 20260312 pin back to a commented line until throughput is
validated; update the FASTLED_BUILD macro/annotation accordingly to reflect the
tested revision.

In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 74-84: Update the thread/task ownership comments to correctly
reflect where the periodic functions are invoked: change the comment for
loop20ms() to indicate it is called from effectTask() (not the SvelteKit task)
and change the comment for loop20msDrivers() to indicate it is called from
driverTask() (not the SvelteKit task); keep any core affinity notes if still
accurate and ensure the referenced symbols loop20ms(), loop20msDrivers(),
effectTask(), and driverTask() are named exactly as in the code so the header
documents the real threading model.

---

Outside diff comments:
In `@interface/src/routes/moonbase/monitor/Monitor.svelte`:
- Around line 66-95: When a new header arrives handleHeader currently recreates
scene via createScene(el) but never resets the layout gate `done`, so subsequent
handleChannels calls won't call requestLayout(); update handleHeader (near
createScene/el usage) to reset the layout request gate by setting `done = false`
(or invoking the appropriate reset function) immediately after creating the
scene so handleChannels can trigger requestLayout() again on reconnect/layout
changes.

In `@src/MoonLight/Layers/PhysicalLayer.cpp`:
- Around line 105-121: requestMapPhysical and requestMapVirtual are being
cleared with plain stores which can race with requestMappings(); change them to
atomic consume semantics so a new request arriving during mapLayout() isn't lost
— e.g. make requestMapPhysical/requestMapVirtual atomic<bool> (or protect with
the same mutex used by requestMappings()/driverTask/loopDrivers()) and replace
the plain "= false" with an atomic exchange(false) (or perform the flag
test-and-clear under the mutex) around the mapLayout() sequence (pass = 1/2 and
mapLayout()) so producers calling requestMappings() cannot overwrite a
concurrent request; locate symbols requestMapPhysical, requestMapVirtual,
requestMappings(), driverTask, loopDrivers(), mapLayout(), and pass to apply the
change.

---

Nitpick comments:
In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte`:
- Around line 141-146: The runtime-computed Tailwind class assigned via the
tagTop reactive variable (defined with $: tagTop and depending on
hasCategoryRow, categories and tags in SearchableDropdown.svelte) may be purged
by Tailwind; fix by replacing the dynamic Tailwind class with a safe approach:
either add the two variants ('top-[4.75rem]' and 'top-10') to your Tailwind
safelist in tailwind.config.js, or stop using dynamic classes and compute a
numeric pixel value (e.g. tagTopPx) and apply it via an inline style attribute
(style="top: {tagTopPx}") on the sticky element instead of toggling class names
at runtime.
- Around line 216-234: In SearchableDropdown.svelte replace the interactive
<button role="option"> with a non-button option element (e.g., <div> or <li>)
that keeps role="option" and aria-selected={isSelected(val, val._idx)},
preserves the same class string and slot usage, and moves the click behavior to
an onclick handler that sets open = false and calls onSelect(val, val._idx,
event); also add keyboard activation handling (keydown for Enter/Space) and
tabindex="0" so the element is focusable and activation via keyboard triggers
the same onSelect logic; ensure references to isSelected, onSelect, val._idx,
val, and open remain unchanged.
- Around line 49-51: The mapping that creates filtered items currently injects a
plain _idx property which can clobber source objects; change the injected index
property to a namespaced name (e.g. _sd_idx or __searchableDropdown_idx) in the
mapping that builds filtered (in SearchableDropdown.svelte) and update every
reference that reads _idx (including where onSelect is called and any lookup by
index) to use the new namespaced property so the original objects are not
mutated or overwritten.

In `@interface/src/test/moonbase_utilities.test.ts`:
- Around line 47-63: Add a unit test to verify that extractEmojis filters
duplicates: update the describe('extractEmojis') tests to include a case (e.g.,
"filters duplicate emojis") that calls extractEmojis with a string containing
repeated emoji characters (and a mix with non-emoji text) and asserts the
returned array only contains unique emoji entries (or matches the documented
uniqueness behavior); reference the existing extractEmojis function and the
describe('extractEmojis') block when adding this new it(...) test case.

In `@test/test_native/test_utilities.cpp`:
- Around line 369-372: The test currently calls a local copy xyzUnModified(...)
instead of the production helper, so regressions in
VirtualLayer::XYZUnModified() or its nrOfLights_t semantics won't be caught;
replace or redirect the test to use the production implementation by either (a)
moving the index formula into a shared pure helper function (e.g. a new util
function like IndexHelpers::xyzUnModified or Numeric::XYZIndex) and calling that
from both VirtualLayer::XYZUnModified() and the test, or (b) change the test to
call VirtualLayer::XYZUnModified() directly (constructing a minimal VirtualLayer
or exposing a static wrapper) so the test exercises the shipped code path and
its nrOfLights_t behavior instead of the local copy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7c58dae-22b6-4ab7-bf85-c469f9e9d322

📥 Commits

Reviewing files that changed from the base of the PR and between 10c4331 and 1065ecd.

📒 Files selected for processing (11)
  • interface/src/lib/components/moonbase/SearchableDropdown.svelte
  • interface/src/lib/stores/moonbase_utilities.ts
  • interface/src/routes/moonbase/monitor/Monitor.svelte
  • interface/src/routes/moonbase/monitor/monitor_webgl.ts
  • interface/src/test/moonbase_utilities.test.ts
  • lib/framework/WWWData.h
  • platformio.ini
  • src/MoonLight/Layers/PhysicalLayer.cpp
  • src/MoonLight/Layers/PhysicalLayer.h
  • src/MoonLight/Nodes/Effects/E_WLED.h
  • test/test_native/test_utilities.cpp

frontend
=======
- monitor_webgl: Avoid recreating WebGL resources on every header.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
interface/src/test/moonbase_utilities.test.ts (1)

2-2: Consider co-locating this test with moonbase_utilities.ts.

The import is correct, but keeping moonbase_utilities.test.ts beside the source file will avoid brittle ../ rewrites and match the repo’s frontend test convention. Based on learnings: Frontend test files should be co-located with the source file they test (e.g. moonbase_utilities.test.ts next to moonbase_utilities.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/test/moonbase_utilities.test.ts` at line 2, Move the test file
moonbase_utilities.test.ts to be co-located with the source
moonbase_utilities.ts and update its import paths accordingly so it imports
getTimeAgo, initCap, and extractEmojis directly from './moonbase_utilities';
ensure any relative '../' paths are replaced with './' and run tests to verify
imports resolve.
interface/src/routes/moonbase/monitor/monitor_webgl.ts (1)

38-38: Arrow function implicitly returns a value.

The static analysis tool flags this because the callback implicitly returns the result of deleteShader. Use a block body to satisfy the linter.

♻️ Suggested fix
-		if (shaders) shaders.forEach((s) => gl!.deleteShader(s));
+		if (shaders) shaders.forEach((s) => { gl!.deleteShader(s); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts` at line 38, The
linter flags the concise arrow callback because it implicitly returns the result
of gl!.deleteShader; update the forEach callback for shaders to use a block body
that performs the call without returning a value (e.g., replace
shaders.forEach((s) => gl!.deleteShader(s)) with a block callback that calls
gl!.deleteShader(s)); key symbols: shaders, gl, deleteShader.
src/MoonLight/Layers/PhysicalLayer.h (1)

29-34: Make the layer owner non-copyable.

PhysicalLayer now clearly owns buffer state, vectors, and semaphore handles behind the global layerP, but the type is still implicitly copyable. Deleting copy/assignment here prevents accidental shallow copies of ownership-bearing state.

♻️ Suggested lock-down
 class PhysicalLayer {
  public:
@@
   PhysicalLayer();
   ~PhysicalLayer();
+  PhysicalLayer(const PhysicalLayer&) = delete;
+  PhysicalLayer& operator=(const PhysicalLayer&) = delete;
+  PhysicalLayer(PhysicalLayer&&) = delete;
+  PhysicalLayer& operator=(PhysicalLayer&&) = delete;

Also applies to: 68-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/PhysicalLayer.h` around lines 29 - 34, PhysicalLayer
currently allows implicit copies which can shallow-copy ownership-bearing
members; make it non-copyable by deleting its copy constructor and copy
assignment operator (e.g., add `PhysicalLayer(const PhysicalLayer&) = delete;`
and `PhysicalLayer& operator=(const PhysicalLayer&) = delete;`) in the class
declaration near the other constructors/destructor so accidental copies of
layerP and its buffers/semaphores are prevented; also apply the same deletion
for move operations if you want to forbid moves, or explicitly default/move them
if moves should be supported.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/lib/components/moonbase/SearchableDropdown.svelte`:
- Around line 214-232: The button currently uses role="option" which conflicts
with its implicit button role; change the interactive element in
SearchableDropdown.svelte to a non-button (e.g., replace the <button> with a
<div> or <li>) and keep the visual classes, aria-selected (use
aria-selected={isSelected(val, val._sd_idx)}), tabindex="0" for keyboard focus,
and preserve the click handler logic that sets open = false and calls
onSelect(val, val._sd_idx, event); additionally add a keydown handler that
triggers the same onSelect behavior for Enter/Space to maintain keyboard
accessibility, keep the slot name="item" with {val} and index={val._sd_idx}
intact; alternatively, if you prefer the simpler change, remove role="option"
from the <button> and rely on existing keyboard navigation while keeping
isSelected, onSelect, open and the slot unchanged.
- Around line 139-144: The dynamic Tailwind arbitrary class assigned to tagTop
(computed from hasCategoryRow/hasTagRow derived from categories and tags) can be
purged at build time; change the implementation to use an inline style for the
top offset (e.g., set style="top: {hasCategoryRow ? '4.75rem' : '2.5rem'}" on
the element that currently uses tagTop) or add the literal "top-[4.75rem]" to
your Tailwind safelist so the class is preserved; update references to tagTop
(and the reactive $: tagTop definition) accordingly so the component uses the
chosen safe approach.

In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts`:
- Around line 112-127: The createProgram function must guard against
gl.createProgram() returning null: after calling gl.createProgram() in
createProgram, check if program is null and, if so, log a clear error (include
gl.getError() or a message), then throw an Error instead of using the null
value; only call gl.attachShader/gl.linkProgram/gl.deleteProgram when program is
non-null. Update createProgram's null-check and error handling to mirror the
pattern used in createShader so callers don’t receive a null WebGLProgram.

In `@interface/src/test/moonbase_utilities.test.ts`:
- Around line 60-62: The test currently only checks count; change it to assert
the exact ZWJ emoji returned by extractEmojis so regressions returning the wrong
glyph are caught. Replace the toHaveLength(1) check in the "handles ZWJ
sequences" test with an equality assertion (e.g., expect(extractEmojis('Family:
👨‍👩‍👧')).toEqual(['👨‍👩‍👧']) or assert the first element equals '👨‍👩‍👧')
targeting the extractEmojis call so the returned sequence is validated exactly.

---

Nitpick comments:
In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts`:
- Line 38: The linter flags the concise arrow callback because it implicitly
returns the result of gl!.deleteShader; update the forEach callback for shaders
to use a block body that performs the call without returning a value (e.g.,
replace shaders.forEach((s) => gl!.deleteShader(s)) with a block callback that
calls gl!.deleteShader(s)); key symbols: shaders, gl, deleteShader.

In `@interface/src/test/moonbase_utilities.test.ts`:
- Line 2: Move the test file moonbase_utilities.test.ts to be co-located with
the source moonbase_utilities.ts and update its import paths accordingly so it
imports getTimeAgo, initCap, and extractEmojis directly from
'./moonbase_utilities'; ensure any relative '../' paths are replaced with './'
and run tests to verify imports resolve.

In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 29-34: PhysicalLayer currently allows implicit copies which can
shallow-copy ownership-bearing members; make it non-copyable by deleting its
copy constructor and copy assignment operator (e.g., add `PhysicalLayer(const
PhysicalLayer&) = delete;` and `PhysicalLayer& operator=(const PhysicalLayer&) =
delete;`) in the class declaration near the other constructors/destructor so
accidental copies of layerP and its buffers/semaphores are prevented; also apply
the same deletion for move operations if you want to forbid moves, or explicitly
default/move them if moves should be supported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7a29264e-ae82-49b1-ba27-81438602086e

📥 Commits

Reviewing files that changed from the base of the PR and between 1065ecd and fbf20fa.

📒 Files selected for processing (6)
  • interface/src/lib/components/moonbase/SearchableDropdown.svelte
  • interface/src/routes/moonbase/monitor/Monitor.svelte
  • interface/src/routes/moonbase/monitor/monitor_webgl.ts
  • interface/src/test/moonbase_utilities.test.ts
  • lib/framework/WWWData.h
  • src/MoonLight/Layers/PhysicalLayer.h

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
interface/src/routes/moonbase/monitor/monitor_webgl.ts (1)

147-167: Consider removing parameter shadowing.

The parameters vertices and colors shadow the module-level exports with the same names. Since Monitor.svelte always passes the exported arrays anyway (per context snippets), using the module-level variables directly would reduce confusion.

♻️ Optional: Remove shadowing parameters
-export const updateScene = (vertices: number[], colors: number[]) => {
+export const updateScene = () => {
 	if (!gl) return;
 
 	// Set the MVP matrix

Then update the caller in Monitor.svelte:

-	updateScene(vertices, colors);
+	updateScene();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts` around lines 147 -
167, The function updateScene currently shadows module-level arrays by accepting
parameters named vertices and colors; remove these parameters from the signature
so updateScene uses the module-scope vertices and colors directly, update any
references inside updateScene (e.g., bindBuffer/bufferData and drawArrays) to
use the module variables, and update the caller in Monitor.svelte to call
updateScene() with no arguments; reference symbols: updateScene, vertices,
colors, positionBuffer, colorBuffer, uMVPLocation, getMVPMatrix, and the
Monitor.svelte call site.
src/MoonLight/Layers/PhysicalLayer.h (1)

92-102: The public mapLayout() API correctly handles all current use cases, but the protocol state design warrants future consideration.

All three mapLayout() call sites (two in PhysicalLayer.cpp and one in ModuleEffects.h) correctly prime pass before calling and handle monitorPass in tightly controlled sequences. The MonitorLayout path is intentionally an external caller that relies on direct field access per the documented pass-1-only design. However, exposing pass and monitorPass as public mutable fields does allow invalid combinations to be representable outside the class. Consider whether mapLayout(uint8_t pass, bool monitorPass = false) or a dedicated method like mapLayoutMonitor() would better prevent misuse by future maintainers, especially if more external callers are added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/MoonLight/Layers/PhysicalLayer.h` around lines 92 - 102, Public mutable
fields pass and monitorPass allow invalid external states; change mapLayout() to
accept the intended mode instead of relying on external mutation: replace the
public uint8_t pass and bool monitorPass usage by adding a signature
mapLayout(uint8_t pass, bool monitorPass = false) (or add a convenience
mapLayoutMonitor() that calls mapLayout(1,true)), make the fields
private/internal state if needed, and update all callers in PhysicalLayer.cpp
and ModuleEffects.h to call the new API with the correct arguments so external
code cannot set inconsistent combinations directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@interface/src/routes/moonbase/monitor/monitor_webgl.ts`:
- Around line 147-167: The function updateScene currently shadows module-level
arrays by accepting parameters named vertices and colors; remove these
parameters from the signature so updateScene uses the module-scope vertices and
colors directly, update any references inside updateScene (e.g.,
bindBuffer/bufferData and drawArrays) to use the module variables, and update
the caller in Monitor.svelte to call updateScene() with no arguments; reference
symbols: updateScene, vertices, colors, positionBuffer, colorBuffer,
uMVPLocation, getMVPMatrix, and the Monitor.svelte call site.

In `@src/MoonLight/Layers/PhysicalLayer.h`:
- Around line 92-102: Public mutable fields pass and monitorPass allow invalid
external states; change mapLayout() to accept the intended mode instead of
relying on external mutation: replace the public uint8_t pass and bool
monitorPass usage by adding a signature mapLayout(uint8_t pass, bool monitorPass
= false) (or add a convenience mapLayoutMonitor() that calls mapLayout(1,true)),
make the fields private/internal state if needed, and update all callers in
PhysicalLayer.cpp and ModuleEffects.h to call the new API with the correct
arguments so external code cannot set inconsistent combinations directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dff5dcb3-c2a5-4a38-97a4-d87ab094d9f1

📥 Commits

Reviewing files that changed from the base of the PR and between fbf20fa and 002f973.

📒 Files selected for processing (5)
  • interface/src/lib/components/moonbase/SearchableDropdown.svelte
  • interface/src/routes/moonbase/monitor/monitor_webgl.ts
  • interface/src/test/moonbase_utilities.test.ts
  • lib/framework/WWWData.h
  • src/MoonLight/Layers/PhysicalLayer.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • interface/src/test/moonbase_utilities.test.ts
  • interface/src/lib/components/moonbase/SearchableDropdown.svelte

@ewowi
Copy link
Collaborator Author

ewowi commented Mar 12, 2026

@coderabbitai, I am about to merge this PR, please do an in-depth review of all the commits made, make a summary, a recommendation to merge and a list of possible future actions.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Tip

For best results, initiate chat on the files or code changes.

@ewowi: Here is a comprehensive in-depth review of all changes in PR #142, along with a summary, merge recommendation, and future action items.


📋 PR #142 — Full Review Summary

Commit 1 — 85c68f332: Frontend Extraction + Unit Tests

Scope: Extracts updateRecursive and normalizePosition into testable modules; adds comprehensive test suites; fixes an import path.

Area Assessment
updateRecursivemodule.ts ✅ Excellent separation of concerns. 18 test cases in moonbase_module.test.ts cover primitives, arrays, nested objects, pruneMissing, and p/name identity change edge cases.
normalizePositionmonitor.ts ✅ Clean extraction. 8 test cases in moonbase_monitor.test.ts cover axis inversion, dimension-collapse, 2D-strip, and origin/far-corner cases.
WebGL code → monitor_webgl.ts ✅ Correct decoupling: pure math function isolated from stateful WebGL rendering pipeline. setMatrixDimensions now accepts depth as a third argument.
Import path fix in moonbase_utilities.test.ts ✅ Necessary correction (./moonbase_utilities../lib/stores/moonbase_utilities).

Commit 2 — 2e8ff854: Code Style Fixes

Scope: for (let key in …)for (const key in …) uniformly; minor formatting-only changes.

No issues. This is correct ESLint/TypeScript convention — loop variables not reassigned should be const.


Commit 3 — 10c433197: SearchableDropdown + Category System + LPS Metrics

Scope: Large multi-faceted commit covering UI components, utility extraction, C++ type system, and telemetry.

Frontend

Area Assessment
SearchableDropdown.svelte ✅ Solid reusable component with search, category tabs, tag cloud, and keyboard Escape support. Replaces duplicated dropdown logic in FieldRenderer.svelte.
extractEmojis + positionDropdownmoonbase_utilities.ts ✅ Correct extraction. extractEmojis tests added (5 cases: single emoji, multiple, none, ZWJ sequences, uniqueness).
FieldRenderer.svelte ✅ Palette and selectFile dropdowns now use SearchableDropdown; color swatches rendered as items; cleaner, no duplicated logic.
lpslps_all / lps_effects / lps_drivers ✅ Well-structured split of the performance telemetry into granular per-subsystem metrics across analytics.ts, models.ts, SystemStatus.svelte, SystemMetrics.svelte, AnalyticsService.h, ESP32SvelteKit.h/.cpp, SystemStatus.cpp, main.cpp. Consistent end-to-end change.

C++ Backend

Area Assessment
LightsHeader.h + PhysMap.h new pure headers ✅ Clean extraction of pure types (no ESP32/FreeRTOS deps) enabling native unit tests.
VirtualLayer refactor ✅ Major expansion of public API with full drawing primitives, blur helpers, and mapping lifecycle.
nrOfLights_t type (uint32_t with PSRAM, uint16_t without) ✅ Proper conditional type alias; consistent use in blur loop variables (uint16_tnrOfLights_t).
category() static method across all Node classes ✅ Large but mechanical change; clean and consistent across Effects (MoonLight, WLED, FastLED, MoonModules, MovingHeads), Layouts, Drivers, Modifiers.
addNodeValue<T>() template helper in NodeManager.h ✅ Eliminates repetitive addControlValue calls; uses T::category() and getNameAndTags<T>().
requestMappings() split (hasOnLayoutrequestMapPhysical, hasModifierrequestMapVirtual) ✅ Correct semantic split; physical vs. virtual mapping triggers are now independent.
Pass 1 → Pass 2 coupling fix in PhysicalLayer.cpp requestMapVirtual = true after pass 1 completes ensures pass 2 always runs.
Native test suite test_layers.cpp (+578 lines) ✅ Comprehensive doctest suite for pure headers (LightsHeader, PhysMap, nrOfLights_t, VirtualLayer coordinate math, isMapped).
palettes.h emoji removal from palette names ✅ Palettes now rely on category field set from emoji detection in ModuleLightsControl.h.

⚠️ Open Issues (not blocking merge)

# Severity File Issue
1 Low SearchableDropdown.svelte _idx spread collision: if a source values object already has _idx, it will be silently overridden. Consider a non-colliding name like __dropdownIdx.
2 Medium SearchableDropdown.svelte role="option" on <button> is non-conformant. option should be a child of a listbox; using <button> here may confuse screen readers. Consider <div role="option"> + keyboard navigation (Arrow keys, Enter).
3 Medium SearchableDropdown.svelte top-[4.75rem] is a dynamically composed class string and won't be picked up by Tailwind's static scanner, meaning it could be missing from the production CSS bundle. Fix: add 'top-[4.75rem]' to the safelist in tailwind.config.*, which requires modifying the Tailwind CSS configuration file, and bear in mind that adding too many classes can bloat the final CSS file.

✅ Merge Recommendation

Recommend merging. The PR is high-quality and delivers real value:

  1. Testability — Pure function extraction with comprehensive test coverage (18 + 8 + 5 test cases) directly improves long-term maintainability.
  2. Architectural improvementLightsHeader.h / PhysMap.h as platform-free headers, VirtualLayer modularisation, and monitor.ts / monitor_webgl.ts split are all solid structural improvements.
  3. UX improvementSearchableDropdown with category tabs, tag cloud, and search is a significant UX upgrade over duplicated inline dropdown logic.
  4. Metrics improvement — The lpslps_all/effects/drivers split gives operators real insight into performance bottlenecks.
  5. The three remaining open issues are medium/low severity and do not affect functional correctness — they are suitable for a follow-up PR.

🔭 Future Actions (Post-merge)

  1. ARIA accessibility for SearchableDropdown — Refactor <button role="option"> to <div role="option"> (or a proper listbox pattern), and add full keyboard navigation: ArrowUp/ArrowDown to move focus, Enter to select, Escape to close.

  2. Tailwind safelist — Add 'top-[4.75rem]' to the safelist in tailwind.config.* to prevent it from being purged in production builds.

  3. _idx naming — Rename _idx to __dropdownIdx (or similar) in SearchableDropdown.svelte to avoid silent property collision when spread onto source values objects.

  4. extractEmojis edge cases — Consider adding a test case for skin-tone modifier sequences (e.g., 👋🏽) to complement the existing ZWJ tests, since those are multi-codepoint sequences that behave differently from ZWJ.

  5. VirtualLayer drawing primitives tests — The new drawLine, drawCircle, drawText and blur functions in VirtualLayer.cpp are not yet covered by native tests. As these grow in complexity, native unit tests would be valuable.

  6. LiveScript .sc category unification — The inline JSON construction for .sc entries in ModuleEffects.h uses a hard-coded "LiveScript" category, while all other nodes use T::category(). Consider introducing a LiveScriptNode wrapper or at least a constant so the category string isn't duplicated.

  7. positionDropdown testingpositionDropdown touches the DOM so it's harder to unit-test in Vitest, but a JSDOM-based test covering the "selected item visible" and "no selection → below trigger" branches would improve confidence.


🧠 Learnings used
Learnt from: CR
Repo: MoonModules/MoonLight PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-03T20:55:04.582Z
Learning: Applies to {src/routes/moonbase/**/*.{ts,tsx},src/lib/components/moonbase/**/*.{ts,tsx},src/lib/stores/moonbase_*.{ts,tsx},src/lib/types/moonbase_*.{ts,tsx}} : Add unit tests for pure, side-effect-free TypeScript functions in MoonLight-specific frontend files

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 136
File: src/MoonBase/Modules/ModuleDevices.h:148-148
Timestamp: 2026-03-04T10:07:41.583Z
Learning: In MoonModules/MoonLight, the `// 🌙` (MoonBase addition) and `// 💫` (MoonLight addition) comment markers are ONLY required when modifying upstream SvelteKit files (i.e., files that originate from the upstream ESP32-sveltekit project). Do NOT require or suggest these markers for MoonBase-specific or MoonLight-specific files (such as `src/MoonBase/Modules/ModuleDevices.h` or other files in `src/MoonBase/` or `src/MoonLight/`). The markers indicate where upstream files have been patched.

@ewowi ewowi merged commit 7662292 into main Mar 12, 2026
38 checks passed
@ewowi ewowi deleted the repo-includes branch March 12, 2026 12:02
@ewowi ewowi changed the title Repository includes and unit tests Searchable drop-downs Mar 12, 2026
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