Skip to content

feat(ui): SkillPlans Timeline + merged Profiles page; six shared primitives - #26

Merged
guarzo merged 28 commits into
mainfrom
worktree-ui-redesign
May 5, 2026
Merged

feat(ui): SkillPlans Timeline + merged Profiles page; six shared primitives#26
guarzo merged 28 commits into
mainfrom
worktree-ui-redesign

Conversation

@guarzo

@guarzo guarzo commented May 5, 2026

Copy link
Copy Markdown
Owner

Summary

Three-phase frontend redesign per docs/superpowers/plans/2026-05-04-canifly-ui-redesign.md.

  • Phase 1 — Six shared UI primitives in renderer/src/components/ui/: EveTypeIcon, FilterBar, PageShell, Connector, ProgressRail, PairSelect. Each has a colocated test.
  • Phase 2 — Skill Plans redesign: new PlanTimeline view (default), usePlanReadiness hook unifying readiness math, MatrixShell extraction, MissingSkillsPopover drill-down, and the PlanList.handleCopy bug fix (was passing the Skills array as newPlanName).
  • Phase 3 — Merged /profiles page replacing /mapping and /sync. Smart default mode (mapping when unmatched chars exist, else sync). New MappingView + SyncView bodies; useMappingDerivations + useSyncAction hooks. SwatchBridge SVG overlay encodes mtime pairings as informational hairlines between paired rows. /mapping and /sync redirect to /profiles?view=... to preserve bookmarks.

26 commits, 49 files (+2498/-1603). 30 test files / 117 tests passing locally; production build succeeds.

Aesthetic constraints honored

No glow, no glass, no decorative animation. The Connector SVG primitive is the one signature visual addition and is used informationally only (Mapping bridge, Sync flow indicator, Skill Plans timeline rail). Inter + JetBrains Mono stay; existing OKLCH token palette unchanged.

Notable behavior

  • Smart default for /profiles: query string > localStorage > smart default. A persisted 'mapping' preference is overridden if all characters are now matched (so users don't get stuck in mapping mode after setup).
  • SkillPlans default view is now Timeline; Matrix and By plan remain available via the segmented control.
  • PlanList copy now derives a unique target name (Hurricane (copy), Hurricane (copy 2), ...) instead of crashing the API call with a Skills payload.
  • Sync's closed <Select> now displays the file name (was bare userId) via the new PairSelect renderValue logic.

Code-review fixes applied pre-PR

A final-pass reviewer flagged three issues, all fixed in commit d26cdd9:

  1. useSyncAction was throwing silently — added toast.error in the catch (matches useAsyncOperation).
  2. MappingView local association state was diverging from the prop after refreshData — added a sync useEffect.
  3. SwatchBridge rects went stale after filter changes — added filter to the recompute effect's deps.

Known follow-ups (deferred, low impact)

  • Profiles.isDefaultDir initial state is false regardless of whether currentSettingsDir matches the Tranquility default. Reset-to-default button shows when it shouldn't on first load. Cosmetic; clicking is safe.
  • PlanList.jsx still has an inline plan-icon <img>; could be replaced with EveTypeIcon for consistency. Behaviorally identical.

Test plan

  • cd renderer && npm test passes (currently 30/30 files, 117/117 tests)
  • cd renderer && npm run build succeeds
  • Manual: navigate to /skill-plans — Timeline renders by default; segmented control switches to Matrix and By plan; filter input takes / focus and Esc clears
  • Manual: click copy on a By-plan row — new plan named <name> (copy) appears
  • Manual: click missing-count number — popover lists aggregated missing skills
  • Manual: navigate to /profiles — defaults to Mapping if there are unmatched chars, Sync otherwise
  • Manual: visit /mapping and /sync directly — both redirect to /profiles with the right mode
  • Manual: in Sync mode, character/user file selects show file names in the closed state
  • Manual: in Mapping mode (≥1024px wide), drag a character onto a user file — SwatchBridge hairlines appear between paired rows whose mtime swatches match

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified Profiles page combining Mapping and Sync modes with mode switching, keyboard shortcuts, and persistent view/filter.
    • Timeline view for skill plans plus a reusable matrix/timeline UI.
    • New UI building blocks: search/filter bar, visual connectors/bridges, progress rails, pair selectors, type icons, missing-skills popovers, and improved sync actions (choose/backup/reset).
  • Improvements

    • Navigation surfaces Profiles and cleaner, more consistent layouts and accessibility across mapping, syncing, and skill-plan workflows.

guarzo added 26 commits May 4, 2026 22:03
Adds Timeline as the third segmented option (Timeline | Matrix | By plan),
sets 'timeline' as the default view, and replaces inline page chrome with
PageShell + FilterBar. Preserves existing handleCopy deduplication logic.
… Select

Replaces the two inline MUI Select blocks and StatusDot/arrow glue in
SyncProfileRow with the PairSelect primitive (Task 1.6), so the closed
right-hand select displays the user's name instead of the bare userId.
Updates the Sync page column header from 7 columns to match the new
4-column row layout.
Wire Profiles page into Routes.jsx as /profiles; add bookmark-preserving
Navigate redirects for /mapping and /sync. Consolidate nav entries in
HeaderNav from separate Mapping + Sync items into a single Profiles item.
Forward refs through MapAccountCard (to header row) and MapCharacterCard (to
outer row), then compute mtime-matched pairs in MappingView via ResizeObserver
+ window events and render SwatchBridge as an overlay between the columns.
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: edcc3a4b-0ca8-473b-b155-1f44462821f3

📥 Commits

Reviewing files that changed from the base of the PR and between 00cc25e and d11faa4.

📒 Files selected for processing (1)
  • renderer/eslint.config.js

📝 Walkthrough

Walkthrough

Consolidates Mapping and Sync into a single Profiles page, redirects /mapping and /sync to /profiles?view=..., and adds MappingView and SyncView. Introduces multiple UI primitives (MatrixShell, ProgressRail, PairSelect, FilterBar, PageShell, Connector, EveTypeIcon), hooks (useMappingDerivations, usePlanReadiness, useSyncAction), tests, and nav/routing updates.

Changes

Profiles consolidation, mapping & sync

Layer / File(s) Summary
Routing / Wiring
renderer/src/Routes.jsx
Adds Navigate and replaces /mapping and /sync with redirects to /profiles?view=...; wires /profiles route to render Profiles with app-data props.
Page API / Props
renderer/src/pages/Profiles.jsx
New Profiles page: view selection (mapping/sync), URL searchParam ↔ state sync, localStorage persistence, keyboard shortcuts, electron sync actions wired via useSyncAction, and mode-specific header/actions.
Data derivation
renderer/src/hooks/useMappingDerivations.js
Adds computeMappingDerivations + hook returning accounts, availableCharacters, mtimeToColor from subDirs and associations.
Mapping core
renderer/src/components/profiles/MappingView.jsx
New MappingView: drag/drop association, unassociate, DOM-measured SVG bridge pairs, filtering, sort, confirm dialogs, local associations sync and refresh.
Sync core
renderer/src/components/profiles/SyncView.jsx, renderer/src/hooks/useSyncAction.js
New SyncView: per-profile selections, persistence, sync and sync-all flows with confirmation; useSyncAction centralizes async run/loading/toast/error handling.
Profile UI primitives
renderer/src/components/profiles/MapAccountCard.jsx, renderer/src/components/profiles/MapCharacterCard.jsx, renderer/src/components/profiles/SwatchBridge.jsx, renderer/src/components/profiles/SyncProfileRow.jsx, renderer/src/components/ui/PairSelect.jsx
Account/Character cards converted to forwardRef; SwatchBridge renders Bézier SVG paths; SyncProfileRow now uses PairSelect for paired selects/status.
Header / Navigation
renderer/src/components/common/HeaderNav.jsx
Removes separate Mapping/Sync nav items; adds single Profiles nav entry and updates icon imports.
Tests & removals
renderer/src/components/profiles/*.test.jsx, renderer/src/pages/Profiles.test.jsx, renderer/src/pages/Mapping.jsx (removed), renderer/src/pages/Sync.jsx (removed)
Adds tests for MappingView, SwatchBridge, SyncView, Profiles; removes old Mapping.jsx/Sync.jsx and their tests.

Skill Plans modernization & UI primitives

Layer / File(s) Summary
Readiness logic
renderer/src/hooks/usePlanReadiness.js
Adds computePlanReadiness and usePlanReadiness to standardize per-character plan readiness (state, eta, missing skills).
Matrix/table primitive
renderer/src/components/skillplan/MatrixShell.jsx
New MatrixShell: sticky header and frozen left column layout for matrix-style grids.
Progress & connectors
renderer/src/components/ui/ProgressRail.jsx, renderer/src/components/ui/Connector.jsx, renderer/src/components/ui/EveTypeIcon.jsx
Adds ProgressRail segmented bar, Connector SVG curve, and EveTypeIcon image/placeholder component.
UI helpers
renderer/src/components/ui/FilterBar.jsx, renderer/src/components/ui/PageShell.jsx
Adds FilterBar (search input with shortcut/clear/count) and PageShell layout wrapper.
Skillplan features
renderer/src/components/skillplan/PlanMatrix.jsx, renderer/src/components/skillplan/PlanTimeline.jsx, renderer/src/components/skillplan/PlanList.jsx, renderer/src/components/skillplan/MissingSkillsPopover.jsx
PlanMatrix refactored to use readiness and MatrixShell; PlanTimeline added using ProgressRail; PlanList aggregates missing skills and uses MissingSkillsPopover.
Tests
renderer/src/hooks/usePlanReadiness.test.js, renderer/src/components/skillplan/*.test.jsx, renderer/src/components/ui/*.test.jsx
Adds tests covering readiness computation, MatrixShell, ProgressRail, FilterBar, PairSelect, EveTypeIcon, MissingSkillsPopover, and related behavior.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Profiles as Profiles.jsx
    participant MappingView
    participant AccountsAPI
    participant LocalStorage

    User->>Profiles: Open /profiles?view=mapping or click Profiles→Mapping
    Profiles->>LocalStorage: read persisted view/filter/mapSort
    Profiles->>MappingView: render with subDirs, associations, filter, view, sort
    MappingView->>MappingView: computeMappingDerivations(subDirs, associations)
    User->>MappingView: drag character onto account
    MappingView->>User: show confirm dialog
    User->>MappingView: confirm
    MappingView->>AccountsAPI: associateCharacter(userId, charId)
    AccountsAPI-->>MappingView: success
    MappingView->>Profiles: refreshData / update associations
Loading
sequenceDiagram
    participant User
    participant Profiles as Profiles.jsx
    participant SyncView
    participant SyncAPI
    participant Toast

    User->>Profiles: Open /profiles?view=sync or click Profiles→Sync
    Profiles->>SyncView: render with settingsData, associations, userSelections
    SyncView->>SyncView: initialize per-profile selections
    User->>SyncView: select character/user, click Sync
    SyncView->>SyncView: show confirm dialog
    User->>SyncView: confirm
    SyncView->>SyncAPI: syncSubdirectory(profile, userId, charId)
    SyncAPI-->>SyncView: success
    SyncView->>Toast: show success message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • guarzo/canifly#2: Overlapping UI refactors around mapping/sync/profile components and pages.
  • guarzo/canifly#5: Related HeaderNav and NAV_LINKS changes affecting navigation entries used here.
  • guarzo/canifly#15: React/forwardRef and eslint-plugin-react version/compat updates that align with forwardRef usage in components.

Poem

A rabbit hops through routes anew,
Bundles Mapping, Sync into one view,
Bridges curve in SVG light,
Rails and shells keep timelines bright,
Hooks hum softly—profiles take flight. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding Timeline view for SkillPlans, merging Profiles page, and introducing six shared UI primitives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-ui-redesign
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch worktree-ui-redesign

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@renderer/src/components/profiles/MappingView.jsx`:
- Line 109: The effect in MappingView.jsx has an unnecessary dependency on
filter causing extra recomputations; remove filter from the dependency array of
the useEffect that currently lists [accounts, availableCharacters, mtimeToColor,
filter] so it becomes [accounts, availableCharacters, mtimeToColor]; ensure this
effect only uses values internal to it (accounts, availableCharacters,
mtimeToColor) and that filter-driven reactivity remains implemented where
filteredAccounts/filteredCharacters are computed elsewhere so behavior is
unchanged.
- Around line 132-137: handleDrop and handleUnassociate currently assume
associateCharacter/unassociateCharacter succeed and show no feedback on failure;
wrap the calls to associateCharacter and unassociateCharacter in try/catch,
check result?.success and on false or caught errors call toast.error with
result.message or the caught error message, and ensure state updates
(setAssociations) and refreshData only happen on success; reference the existing
functions handleDrop/handleUnassociate, associateCharacter,
unassociateCharacter, setAssociations and refreshData when making these changes.

In `@renderer/src/components/profiles/SwatchBridge.jsx`:
- Around line 4-6: The empty-state branch in the SwatchBridge component returns
a plain <svg> which omits the absolute overlay styling and pointer-events-none
used by the non-empty rendering; update the early return (the if (!pairs ||
pairs.length === 0) branch) to render the same overlay svg as the non-empty case
by including the absolute overlay classes (e.g., "absolute inset-0
pointer-events-none") merged with the existing className and using the same
width/height props so layout and interactivity remain consistent while pairs are
loading.

In `@renderer/src/components/profiles/SyncProfileRow.jsx`:
- Around line 23-27: The charOptions useMemo assumes each item has a name and
will throw or render empty when name is missing; update the sort and map to
fallback to charId when name is falsy by comparing (a.name || a.charId) and
(b.name || b.charId) in the localeCompare call and setting primary to (c.name ||
c.charId) while keeping secondary as c.charId so labels and sorting remain
stable even if name is absent; modify the charOptions computation that
references subDir.availableCharFiles, sort, and map accordingly.

In `@renderer/src/components/profiles/SyncView.jsx`:
- Around line 35-44: The state-updater in handleSelectionChange currently calls
persistSelections(next) inside setSelections(prev => ...), which breaks purity
and can cause duplicate writes; change handleSelectionChange to compute the
updated "next" selections outside the updater, call setSelections with either
setSelections(prev => ({...prev, ...})) but do NOT invoke persistSelections
inside that updater — instead call persistSelections(next) after setSelections
returns (or in a .then callback if using the functional update result), and
ensure persistSelections errors are caught/handled; reference
handleSelectionChange, setSelections, and persistSelections when making this
change.

In `@renderer/src/components/skillplan/MatrixShell.jsx`:
- Around line 19-24: The frozen top-left cell container (the div rendering
{frozenHeader}) must be exposed as a column header for screen readers: add
role="columnheader" (and aria-colindex if you track column indices) to the div
that renders frozenHeader in MatrixShell.jsx; likewise, mark the frozen row
label elements (the elements rendering the left-side row labels around lines
where frozen row labels are rendered) with role="rowheader" (and aria-rowindex
as appropriate) so assistive tech can associate headers with table cells.

In `@renderer/src/components/ui/EveTypeIcon.jsx`:
- Line 11: EveTypeIcon.jsx currently builds Tailwind classes at runtime via the
sizeClass = `h-${size} w-${size}` expression which Tailwind cannot detect;
instead, create a static mapping object (e.g., sizeToClass) inside the
EveTypeIcon component mapping allowed size prop values (constrain the size prop
to those values) to full class strings like "h-4 w-4", "h-6 w-6", etc., replace
sizeClass with a lookup (sizeToClass[size] || defaultClass), and update the prop
type/validation for size to accept only the mapped keys so Tailwind can pick up
the classes at build time.

In `@renderer/src/components/ui/FilterBar.jsx`:
- Around line 45-47: In FilterBar.jsx change the conditional that currently
hides the counter for falsy values to a nullish check so zero still renders:
replace the truthy check around {count ? (<span className="text-meta text-ink-3
tabular">{count}</span>) : null} with a null/undefined guard (e.g., check count
!= null or count !== undefined) so the <span> renders for 0 but remains hidden
when count is null/undefined; update the JSX in the FilterBar component where
the count rendering occurs.

In `@renderer/src/hooks/useMappingDerivations.js`:
- Around line 20-24: The roundToMinute function and surrounding mapping logic
must be hardened against malformed payloads: in roundToMinute(mtime) validate
that mtime produces a valid Date (e.g., new Date(mtime) is not "Invalid Date")
and return a safe fallback (null or empty string) instead of calling toISOString
when invalid; additionally, wherever the code assumes array fields exist (the
mapping logic that iterates fields around the usages at/near lines 33 and 49),
guard with Array.isArray checks before iterating or map/forEach and skip or
default missing/invalid entries so a single malformed file entry cannot throw
(update the functions/blocks referencing roundToMinute and the array fields to
perform these validations and handle fallbacks).

In `@renderer/src/hooks/usePlanReadiness.js`:
- Line 17: The ETA calculation is incorrectly turning a valid 0 into null by
using the || operator; update the assignment that sets eta (the line that calls
calculateDaysFromToday in usePlanReadiness.js) to use the nullish coalescing
operator so that 0 is preserved (i.e., replace the `|| null` behavior with `??
null`), then run the hook's unit/UI tests to confirm "finishes today" is
represented as 0 rather than null.

In `@renderer/src/hooks/useSyncAction.js`:
- Around line 11-31: The isLoading boolean can be flipped false by one
overlapping run() call while others are still in flight; change the loading
tracking to a counter (e.g., activeCount via useRef or useState) and increment
it at the start of run() and decrement in the finally block, then derive/set
isLoading as activeCount > 0 (update setIsLoading or replace it with derived
boolean) so controls remain disabled until all concurrent run() invocations have
completed; update references to isLoading/setIsLoading and the run function to
use this counter-based logic.

In `@renderer/src/pages/Profiles.jsx`:
- Around line 111-117: The effect is using a stale closure over params; change
the setParams call inside the useEffect so it uses the callback form to read
current search params instead of the closed-over params. In the useEffect that
currently calls writeLS(LS.view, view) and creates new URLSearchParams(params)
then setParams(...), replace that with a call to setParams(prev => { create a
new URLSearchParams from prev, set('view', view) and return it }, { replace:
true }) so the handler uses the latest params; keep writeLS and the view
dependency unchanged and remove the eslint-disable if you want the hook rules to
be satisfied.

In `@renderer/src/Routes.jsx`:
- Around line 69-75: Normalize the lastBackupDir prop before passing it to
Profiles so it is always a string; replace the current prop usage
(lastBackupDir) with a normalized value (e.g., typeof lastBackupDir === 'string'
? lastBackupDir : '' or String(lastBackupDir) with an Array.isArray check) so
Profiles receives '' instead of an empty array and
window.electronAPI.chooseDirectory(...) is never called with a truthy array.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 70c528f4-ef0d-4dee-8fff-7fb13ef3b867

📥 Commits

Reviewing files that changed from the base of the PR and between 30309e6 and d26cdd9.

📒 Files selected for processing (48)
  • renderer/src/Routes.jsx
  • renderer/src/components/common/HeaderNav.jsx
  • renderer/src/components/profiles/MapAccountCard.jsx
  • renderer/src/components/profiles/MapAccountCard.test.jsx
  • renderer/src/components/profiles/MapCharacterCard.jsx
  • renderer/src/components/profiles/MapCharacterCard.test.jsx
  • renderer/src/components/profiles/MappingView.jsx
  • renderer/src/components/profiles/MappingView.test.jsx
  • renderer/src/components/profiles/MtimeSwatch.jsx
  • renderer/src/components/profiles/SwatchBridge.jsx
  • renderer/src/components/profiles/SwatchBridge.test.jsx
  • renderer/src/components/profiles/SyncProfileRow.jsx
  • renderer/src/components/profiles/SyncView.jsx
  • renderer/src/components/profiles/SyncView.test.jsx
  • renderer/src/components/skillplan/MatrixShell.jsx
  • renderer/src/components/skillplan/MatrixShell.test.jsx
  • renderer/src/components/skillplan/MissingSkillsPopover.jsx
  • renderer/src/components/skillplan/MissingSkillsPopover.test.jsx
  • renderer/src/components/skillplan/PlanList.jsx
  • renderer/src/components/skillplan/PlanList.test.jsx
  • renderer/src/components/skillplan/PlanMatrix.jsx
  • renderer/src/components/skillplan/PlanTimeline.jsx
  • renderer/src/components/skillplan/PlanTimeline.test.jsx
  • renderer/src/components/ui/Connector.jsx
  • renderer/src/components/ui/Connector.test.jsx
  • renderer/src/components/ui/EveTypeIcon.jsx
  • renderer/src/components/ui/EveTypeIcon.test.jsx
  • renderer/src/components/ui/FilterBar.jsx
  • renderer/src/components/ui/FilterBar.test.jsx
  • renderer/src/components/ui/PageShell.jsx
  • renderer/src/components/ui/PageShell.test.jsx
  • renderer/src/components/ui/PairSelect.jsx
  • renderer/src/components/ui/PairSelect.test.jsx
  • renderer/src/components/ui/ProgressRail.jsx
  • renderer/src/components/ui/ProgressRail.test.jsx
  • renderer/src/hooks/useMappingDerivations.js
  • renderer/src/hooks/useMappingDerivations.test.js
  • renderer/src/hooks/usePlanReadiness.js
  • renderer/src/hooks/usePlanReadiness.test.js
  • renderer/src/hooks/useSyncAction.js
  • renderer/src/hooks/useSyncAction.test.js
  • renderer/src/pages/Mapping.jsx
  • renderer/src/pages/Mapping.test.jsx
  • renderer/src/pages/Profiles.jsx
  • renderer/src/pages/Profiles.test.jsx
  • renderer/src/pages/SkillPlans.jsx
  • renderer/src/pages/Sync.jsx
  • renderer/src/pages/Sync.test.jsx
💤 Files with no reviewable changes (4)
  • renderer/src/pages/Mapping.test.jsx
  • renderer/src/pages/Mapping.jsx
  • renderer/src/pages/Sync.jsx
  • renderer/src/pages/Sync.test.jsx

Comment thread renderer/src/components/profiles/MappingView.jsx Outdated
Comment thread renderer/src/components/profiles/MappingView.jsx Outdated
Comment thread renderer/src/components/profiles/SwatchBridge.jsx
Comment thread renderer/src/components/profiles/SyncProfileRow.jsx
Comment thread renderer/src/components/profiles/SyncView.jsx
Comment thread renderer/src/hooks/useMappingDerivations.js
if (c.PendingPlans?.[planName]) {
const queue = c.SkillQueue;
const training = Array.isArray(queue) && queue.length > 0 && Boolean(character.MCT);
const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) || null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve a valid ETA of 0 days.

Line 17 uses || null, which converts a valid 0 return into null. That hides “finishes today” state.

Suggested fix
-        const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) || null;
+        const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) ?? null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) || null;
const eta = calculateDaysFromToday(c.PendingFinishDates?.[planName]) ?? null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@renderer/src/hooks/usePlanReadiness.js` at line 17, The ETA calculation is
incorrectly turning a valid 0 into null by using the || operator; update the
assignment that sets eta (the line that calls calculateDaysFromToday in
usePlanReadiness.js) to use the nullish coalescing operator so that 0 is
preserved (i.e., replace the `|| null` behavior with `?? null`), then run the
hook's unit/UI tests to confirm "finishes today" is represented as 0 rather than
null.

Comment on lines +11 to +31
const [isLoading, setIsLoading] = useState(false);

const run = useCallback(async (operation, options = {}) => {
const { successMessage, errorContext = 'sync action' } = options;
try {
setIsLoading(true);
const result = await operation();
if (result?.success) {
if (successMessage) toast.success(successMessage);
else if (result.message) toast.success(result.message);
}
return result;
} catch (err) {
logger.error(`${errorContext} failed`, err);
const userMessage = err?.message || `${errorContext} failed`;
toast.error(userMessage);
throw err;
} finally {
setIsLoading(false);
}
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isLoading is incorrect for overlapping run() calls

If run() is triggered twice before the first call finishes, Line 29 can set loading to false while one operation is still in flight. That can re-enable controls too early.

Suggested fix
 export function useSyncAction() {
-    const [isLoading, setIsLoading] = useState(false);
+    const [pendingCount, setPendingCount] = useState(0);
+    const isLoading = pendingCount > 0;
 
     const run = useCallback(async (operation, options = {}) => {
         const { successMessage, errorContext = 'sync action' } = options;
         try {
-            setIsLoading(true);
+            setPendingCount((n) => n + 1);
             const result = await operation();
             if (result?.success) {
                 if (successMessage) toast.success(successMessage);
                 else if (result.message) toast.success(result.message);
             }
             return result;
         } catch (err) {
             logger.error(`${errorContext} failed`, err);
             const userMessage = err?.message || `${errorContext} failed`;
             toast.error(userMessage);
             throw err;
         } finally {
-            setIsLoading(false);
+            setPendingCount((n) => Math.max(0, n - 1));
         }
     }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isLoading, setIsLoading] = useState(false);
const run = useCallback(async (operation, options = {}) => {
const { successMessage, errorContext = 'sync action' } = options;
try {
setIsLoading(true);
const result = await operation();
if (result?.success) {
if (successMessage) toast.success(successMessage);
else if (result.message) toast.success(result.message);
}
return result;
} catch (err) {
logger.error(`${errorContext} failed`, err);
const userMessage = err?.message || `${errorContext} failed`;
toast.error(userMessage);
throw err;
} finally {
setIsLoading(false);
}
}, []);
const [pendingCount, setPendingCount] = useState(0);
const isLoading = pendingCount > 0;
const run = useCallback(async (operation, options = {}) => {
const { successMessage, errorContext = 'sync action' } = options;
try {
setPendingCount((n) => n + 1);
const result = await operation();
if (result?.success) {
if (successMessage) toast.success(successMessage);
else if (result.message) toast.success(result.message);
}
return result;
} catch (err) {
logger.error(`${errorContext} failed`, err);
const userMessage = err?.message || `${errorContext} failed`;
toast.error(userMessage);
throw err;
} finally {
setPendingCount((n) => Math.max(0, n - 1));
}
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@renderer/src/hooks/useSyncAction.js` around lines 11 - 31, The isLoading
boolean can be flipped false by one overlapping run() call while others are
still in flight; change the loading tracking to a counter (e.g., activeCount via
useRef or useState) and increment it at the start of run() and decrement in the
finally block, then derive/set isLoading as activeCount > 0 (update setIsLoading
or replace it with derived boolean) so controls remain disabled until all
concurrent run() invocations have completed; update references to
isLoading/setIsLoading and the run function to use this counter-based logic.

Comment thread renderer/src/pages/Profiles.jsx Outdated
Comment thread renderer/src/Routes.jsx
- Routes: lastBackupDir always normalized to a string (was [] when
  config.LastBackupDir was missing — broke chooseDirectory contract).
- Profiles: setParams uses functional form; drops stale closure on params.
- MappingView: associate/unassociate wrapped in try/catch with toast on
  failure (was silent on rejection); bridge effect drops redundant
  filter dep (ResizeObserver covers re-render).
- SwatchBridge: empty-state branch carries the same absolute overlay
  styling as the populated branch, so a 0-pair render doesn't push
  layout when containerSize is non-zero.
- SyncProfileRow: charOptions falls back to charId when name is missing
  (sort + display) so a malformed file entry no longer throws.
- SyncView: persistSelections moved out of the setSelections updater
  (was impure under StrictMode); errors now surface via toast.
- MatrixShell: frozen header gets role=columnheader, frozen row cell
  gets role=rowheader so screen readers can associate headers with
  cells in the dense matrix.
- EveTypeIcon: size prop maps to a static class table (was dynamic
  template — Tailwind v4 JIT couldn't see it, so the size prop had no
  visual effect in production builds). Constrained to oneOf 4/5/6/8.
- FilterBar: count rendering switched to nullish guard so a literal
  zero count would still display.
- useMappingDerivations: roundToMinute returns null for invalid Date
  inputs; iteration guards with Array.isArray and skips malformed
  entries so a single bad payload row no longer throws.

Skipped (logged in PR comments):
- usePlanReadiness `?? null` change — calculateDaysFromToday returns
  strings, never numeric 0, so the distinction is moot here.
- useSyncAction concurrent-counter — useSyncAction is one instance per
  hook usage and buttons disable while loading; no current call site
  triggers concurrent run() invocations to defend against.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@renderer/src/components/skillplan/MatrixShell.jsx`:
- Around line 69-80: MatrixShell.propTypes (and similar assignments across ~48
function components) are inert under React 19; replace runtime propTypes with a
codebase migration: either convert MatrixShell to TypeScript by adding a Props
interface for MatrixShell (defining ariaLabel, frozenHeader, columnHeaders,
rows, frozenWidth, colWidth) and update the MatrixShell function signature to
use that type, or add a development-only runtime validator (e.g., a
validateProps utility called inside MatrixShell that performs the same checks as
the old propTypes) and remove MatrixShell.propTypes to avoid misleading usage;
additionally update renderer/eslint.config.js to report React version 19.2.5 so
lint rules are accurate.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 71cd843f-ca20-4412-aaef-aafb93f436c9

📥 Commits

Reviewing files that changed from the base of the PR and between d26cdd9 and 00cc25e.

📒 Files selected for processing (10)
  • renderer/src/Routes.jsx
  • renderer/src/components/profiles/MappingView.jsx
  • renderer/src/components/profiles/SwatchBridge.jsx
  • renderer/src/components/profiles/SyncProfileRow.jsx
  • renderer/src/components/profiles/SyncView.jsx
  • renderer/src/components/skillplan/MatrixShell.jsx
  • renderer/src/components/ui/EveTypeIcon.jsx
  • renderer/src/components/ui/FilterBar.jsx
  • renderer/src/hooks/useMappingDerivations.js
  • renderer/src/pages/Profiles.jsx

Comment thread renderer/src/components/skillplan/MatrixShell.jsx
@guarzo
guarzo merged commit fe34d50 into main May 5, 2026
2 checks passed
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