Mirrored from GitHub github.com/roostorg/coop

Enable User Strikes dashboard + fix UX issues blocking v1 (#600) master

* Expose the existing User Strikes dashboard in the sidebar The User Strikes dashboard was already fully built — four tabs covering per-policy strike counts, per-action strike toggles, org-level thresholds + TTL, and analytics, with real GraphQL mutations across ~1400 lines — but was unreachable from navigation. The route at `/user_strikes` existed but was commented "uncomment this when final UI is finished," and the sidebar entry was also commented out. This is what issue #156 ("Finish / enable user strike and score system") is actually asking for: the strikes configuration UX exists, it just isn't wired into the navigation. Two changes: - Uncomment the User Strikes nav entry in Dashboard.tsx, with `urlPath: 'user_strikes'` (the commented-out version had `userStrikes` which didn't match the route). - Add 'User Strikes' to the closed MenuItemName union in Sidebar.tsx (otherwise the title literal fails to typecheck). Note: the deprecated User Quality Score signal (`USER_SCORE`) remains disabled in the rule builder with its existing tooltip. Removing it entirely (plus the dead `actions.penalty` / `policies.penalty` schema) will be a follow-up issue — too much scope for a 1.5-week v1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix User Strikes route + Strike Window save flow Two bugs that surfaced once the User Strikes nav link was uncommented: 1. Route path: registered at 'user_strikes' but the sidebar generates URLs by prepending the parent urlPath ('rules' for Automated Enforcement → /dashboard/rules/user_strikes), so the page 404'd. Renamed the route to 'rules/user_strikes' to match the sibling sub-items (rules/proactive, rules/banks, rules/report). 2. Strike Window save flow had three compounding bugs that made it look like saves were silently failing: - Display: `value={ttlFormState + ' days'}` on a `type="number"` input is invalid — browser rejected the string and rendered the placeholder "90" instead of the saved value. Moved the " days" suffix to a sibling <span>; the input now just holds the number. - Refetch: `useGQLUpdateUserStrikeTtlMutation` had no `onCompleted` handler (unlike the sibling thresholds mutation), so the cached `userStrikeTTL` stayed stale after a successful write. - State sync: `useState(orgTTL)` only takes the initial value; even with refetch, the form's local state wouldn't pick up the new value. Added a `key={\`strikeTTLForm-${orgTTL}\`}` on the parent so the form remounts when the refetched value changes (mirrors the pattern used on the threshold form). - Bonus: `setTTL()` is now awaited so the editing-mode flip happens after the mutation completes, not in parallel. Width was also bumped from 3.5em to 6em (cutting off 3-digit values plus the number-input spinner controls). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix Analytics tab UX issues on User Strikes dashboard Three problems surfaced once the page was reachable with real data: 1. Chart legend and x-axis label collided at the bottom of the BarChart — both default to bottom positioning. Moved the legend to `verticalAlign="top"` so it sits above the chart instead. 2. The "Strikes Applied" x-axis label was getting clipped below the tick numbers. Bumped the chart's bottom margin from 15 → 40 to make room for both. 3. The dashed red `ReferenceLine`s marking org strike thresholds had no explanation — users couldn't tell what they meant. Added them to the legend via a custom `payload` (ReferenceLine doesn't participate in the legend on its own). 4. Tooltip floated around with the cursor inside a single bar, which felt buggy. Pinned its Y position to mid-chart (220) so it only tracks horizontally, and replaced the invisible default cursor highlight with a subtle column fill so the user can see which bar is selected. 5. The "Recent Actions Taken By Your Strike System" table was using the Table component's default `w-fit` (shrink-to-content) container class, so a sparse table rendered very narrow. Passed `containerClassName="w-full"` so it fills its page-width parent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix lint-staged shell quoting so eslint runs on staged files only `JSON.stringify(filename)` produced double-quoted args that collapsed against the outer `bash -c "..."` wrapper, so the shell parsed the command as `cd <pkg> && .../eslint --fix` with no file args. eslint then linted the entire package directory and surfaced every pre-existing warning/error in the repo, blocking any commit whose staged files were clean but whose tree wasn't. Switch to single-quoted args (with the standard `'\''` escape for any embedded single quote) so the file paths survive the outer double-quoted `bash -c` intact. Confirmed locally: with this change, `npx lint-staged` on the User Strikes review fixes runs eslint against exactly the two staged files instead of all 126 files in `client/src/`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review feedback on User Strikes dashboard Four fixes from PR #600 review threads: - StrikeAnalyticsTab: lift the BarChart height to a `chartHeight` const and derive the tooltip y from `chartHeight / 2 + 20`. The +20 still nudges the tooltip below the title; changing the chart height no longer silently desyncs the tooltip from the plot area. - StrikeAnalyticsTab: collapse the table sort comparator to `a.time > b.time ? -1 : a.time < b.time ? 1 : 0`. The old form never returned `0`, so equal timestamps were treated as strictly ordered and could reorder nondeterministically across renders. - StrikeAnalyticsTab: switch `actionsById[values.actionId] || 'Unknown'` to `??`. An action name that's an empty string (falsy) shouldn't get replaced with `'Unknown'` — only `null`/`undefined` should fall through. This also matches the repo's lint preference of `??` over `||` for fallbacks. - ThresholdsAndSettingsTab: type `StrikeTTLForm.setTTL` as `(ttl: number) => Promise<void>`. The Save handler does `await setTTL(ttlFormState); toggleEditing();`, so awaiting a void-typed prop both fails `await-thenable` lint and lets the toggle fire before the mutation round-trips on a slow link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add empty state for Policy Scores tab --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Caleb McQuaid <calebmcquaid@gmail.com>