Enable User Strikes dashboard + fix UX issues blocking v1 (#600)
* 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>