Skip to content

feat(firearms): shot count tracking per firearm (#11) - #34

Merged
unclesp1d3r merged 15 commits into
mainfrom
11-shot-count-tracking-per-firearm
Jul 3, 2026
Merged

unclesp1d3r merged 15 commits into
mainfrom
11-shot-count-tracking-per-firearm

Conversation

@unclesp1d3r

@unclesp1d3r unclesp1d3r commented Jul 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements shot count tracking per firearm (#11): a range_session child table logging rounds fired per firearm, with a derived lifetime round total surfaced on the firearms list, on-demand session history, and log/edit/delete — all owner-scoped through the parent firearm. Standalone slice; ammo (#7) and service-interval (#10) integrations are designed-for but deferred.

Plan: docs/plans/2026-07-03-001-feat-shot-count-tracking-plan.md

What shipped (U1–U6)

  • U1 — range_session schema: first firearm child (FK ON DELETE CASCADE, rounds_fired >= 1 check, nullable ammo_id Add ammo inventory tracking with low-stock alerts and summary rollups #7 seam, no owner_id) + generated migration + factory.
  • U2 — pure validation: rounds_fired is a whole number ≥ 1; date is a valid ISO date.
  • U3 — visibility-scoped service: CRUD authorized through the parent firearm; lifetimeRoundTotals derives per-firearm totals via a SQL SUM (cast to number); visibleFirearmPermissions supplies the UI gating signal.
  • U4 — server actions: log / update / delete + an on-demand list read.
  • U5 — firearms list gains a Rounds column and a per-row Sessions panel that loads history on demand (loading / empty / error states); log/edit/delete controls are gated on the viewer's firearm permission (KTD7).
  • U6 — Playwright e2e: derived total tracks logged and deleted sessions end-to-end (AE1).

Key decisions

  • Derived, not stored. The lifetime total is a SQL sum over the log, never a mutable counter — edits and deletes stay consistent with no reconciliation (R4).
  • Child inherits auth from the firearm. Sessions carry no owner_id and no own grant family; visibility and write-auth resolve through the parent firearm (R7/R62). Session mutations — including delete — require edit on the firearm (KTD3), deliberately looser than firearm delete (owner-only).

Test plan

  • bun run typecheck — clean
  • bun run lint (biome) — clean
  • bun test — 219 pass / 0 fail (new range-session unit + integration tests cover AE1, AE2, the owner/edit/view/none authorization matrix, KTD7, cascade, the rounds check constraint, and the update existence-leak guard)
  • bun run build — succeeds
  • bun run test:e2e — 14 pass (incl. range-sessions AE1; no regressions in the firearms flows)
  • Migration generated and applied

Code review

Ran a multi-reviewer pass (correctness, security, testing, maintainability). Fixed before merge:

  • P1 (3-reviewer agreement) — updateRangeSession now authorizes before validating, so an invisible session can't be probed via invalid input (ValidationError vs NotFound); added a guard test (R70 existence-hiding).
  • P1 — the session form remounts on edit-target switch (key) so stale field state can't overwrite a different session.
  • P2 — useCallback the history load (drop the exhaustive-deps suppression); extract toPermission and a shared expectRejects; derive the session list once and replace a 4-way ternary with an early-return render function.

Tooling fix (out of plan)

The sqlfluff pre-commit hook had no dialect configured anywhere in the repo and errored on every SQL commit (and its auto-fix mangled generated SQL). Added .sqlfluff (dialect = postgres) and excluded the drizzle-generated src/db/migrations/ from the hook — hand-linting generated migrations fights the generator (e.g. PG01 NOT VALID/CONCURRENTLY rules drizzle never emits).

Review residuals — resolved (no deferrals)

Both items the review flagged as deferrable were addressed in this PR:

  • View-only UI gating now has e2e coverage. e2e/range-sessions-sharing.spec.ts is a two-user spec: an owner shares a firearm view-only, and a second browser context (the viewer's session) reads the lifetime total + history but sees no Log/Edit/Delete controls (AE2 at the UI layer, KTD7). Exports storageStateFor to support multi-user specs.
  • Redundant visibility queries removed. lifetimeRoundTotals now accepts an optional pre-resolved visible set; the firearms page derives it once from visibleFirearmPermissions and feeds it in, so the feature adds a single owned∪granted scan (serving both gating and the totals) rather than three.

Closes #11.

…estcontainers environment

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Replace the spawned `next start` (and the start-app.ts argv shim that forced
env past it) with Next's in-process `nextStart({ port, hostname })`. With no
child process, nothing re-loads a local .env or mise-cached env to clobber the
launcher's per-run DATABASE_URL/BETTER_AUTH_URL/BETTER_AUTH_SECRET, so sessions
minted against the test container validate correctly. Drops the secret-on-argv
and the internal-bin import; keeps the production build, ephemeral random ports,
in-process minting, and Ryuk cleanup. Mirrors the community next-dev in-process
e2e pattern. All 13 specs pass locally; CI (no .env) unaffected.

Also documents the root cause + fix in docs/solutions/test-failures/.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Completes the shim removal: replace the spawned `next start` with Next's
in-process `nextStart({ port, hostname })` in the launcher, so no child process
re-loads a local .env / mise-cached env to clobber this run's DATABASE_URL /
BETTER_AUTH_URL / BETTER_AUTH_SECRET. Update the docs/solutions learning to the
in-process solution. All 13 e2e specs pass locally; CI unaffected.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
First firearm child table (#11): one row per firearm per range trip, FK
ON DELETE CASCADE, rounds_fired >= 1 check, nullable ammo_id seam for #7.
No owner_id — visibility inherits from the parent firearm (R62).

Fix the sqlfluff pre-commit hook: add .sqlfluff (dialect=postgres) and
exclude the drizzle-generated src/db/migrations/ from sqlfluff, which
otherwise errored (no dialect) and mangled generated SQL on every commit.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
… date)

Returns all failure codes together (KTD4). rounds_fired must be a whole
number >= 1; date must be a non-empty parseable ISO date.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
… totals

CRUD authorized through the parent firearm (KTD2); delete needs only edit
(KTD3). lifetimeRoundTotals derives per-firearm totals via SQL sum, cast to
number (KTD1). Add visibleFirearmPermissions for UI control gating (KTD7).

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
ActionResult-wrapped, mirroring firearm actions. Mutations revalidate
/firearms; listRangeSessionsAction is read-only for the on-demand history.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Firearms list gains a Rounds column (derived total) and a per-row Sessions
panel. History loads on demand with loading/empty/error states; log/edit/
delete controls are gated on the viewer's firearm permission (KTD7).

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…llow-ups

Code review (3-reviewer agreement, P1):
- updateRangeSession now authorizes before validating, so an invisible
  session can't be probed via invalid input (ValidationError vs NotFound);
  add a guard test for the invalid-input + no-visibility case (R70).
- Remount the session form on edit-target switch (key) so stale field state
  can't overwrite a different session (P1 correctness).

Simplify pass:
- useCallback the history load (drop the exhaustive-deps suppression);
  extract toPermission and a shared expectRejects; derive the session list
  once and replace the 4-way ternary with an early-return render function.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Copilot AI review requested due to automatic review settings July 3, 2026 22:12
@unclesp1d3r unclesp1d3r linked an issue Jul 3, 2026 that may be closed by this pull request
2 tasks
@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: be6e7d50-5f42-40f8-9ded-f5398357155b

📥 Commits

Reviewing files that changed from the base of the PR and between 8285f1f and 9191688.

📒 Files selected for processing (1)
  • .pre-commit-config.yaml

Walkthrough

Adds firearm range-session tracking with persisted sessions, derived lifetime round totals, permission-gated history controls, Playwright coverage, and updated test-server startup behavior.

Changes

Range session shot count tracking

Layer / File(s) Summary
Lint and dependency updates
.pre-commit-config.yaml, .sqlfluff, mise.toml, package.json
Updates SQLFluff configuration, excludes generated migration SQL from pre-commit lint/fix hooks, and bumps tool and package versions.
Schema and migration
src/db/inventory-schema.ts, src/db/migrations/0006_wakeful_exodus.sql, src/db/migrations/meta/*
Adds the range_session table, migration SQL, snapshot metadata, and journal entry.
Validation rules
src/domain/range-sessions/validate.ts, src/domain/validation-messages.ts, src/domain/range-sessions/__tests__/validate.test.ts
Adds pure input validation for session date and rounds, user-facing messages, and tests for all validation codes.
Permission normalization
src/auth/visibility.ts
Normalizes stored grant strings to view/edit before exposing firearm permissions.
Range session domain service
src/domain/range-sessions/service.ts, src/test-support/factories.ts, src/test-support/assertions.ts, src/domain/range-sessions/__tests__/service.test.ts
Implements CRUD, visibility-scoped session listing, and lifetime round aggregation with integration coverage and DB/test helpers.
Server actions
app/(app)/firearms/session-actions.ts
Adds authenticated create/update/delete/list server actions for range sessions and revalidates the firearms route after mutations.
Firearms UI
app/(app)/firearms/page.tsx, app/(app)/firearms/firearms-view.tsx, app/(app)/firearms/range-session-form.tsx, app/(app)/firearms/range-session-history.tsx
Fetches per-firearm totals and permissions, shows lifetime rounds in the list, and adds session logging/history panels.
E2E coverage and test harness
e2e/fixtures/auth.ts, e2e/fixtures/user-pool.ts, e2e/range-sessions.spec.ts, e2e/range-sessions-sharing.spec.ts, e2e/start-test-server.ts, e2e/start-app.ts, docs/solutions/test-failures/*
Adds range-session and sharing e2e coverage, seeds additional users, starts Next in-process in the test server, removes the subprocess shim, and documents the env-clobbering failure mode.
Feature plan document
docs/plans/2026-07-03-001-feat-shot-count-tracking-plan.md
Adds the shot-count-tracking plan with requirements, implementation units, verification, and done criteria.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant RangeSessionForm
  participant SessionActions
  participant RangeSessionService
  participant Database

  User->>RangeSessionForm: submit session data
  RangeSessionForm->>SessionActions: logRangeSessionAction(input)
  SessionActions->>RangeSessionService: createRangeSession(actorId, input)
  RangeSessionService->>Database: insert range_session row
  Database-->>RangeSessionService: created row
  RangeSessionService-->>SessionActions: RangeSession
  SessionActions-->>RangeSessionForm: ActionResult({ id })
Loading
sequenceDiagram
  participant FirearmsPage
  participant FirearmsView
  participant RangeSessionHistory
  participant SessionActions

  FirearmsPage->>FirearmsView: FirearmListItem[] with roundTotal and permission
  FirearmsView->>RangeSessionHistory: open history panel
  RangeSessionHistory->>SessionActions: listRangeSessionsAction(firearmId)
  SessionActions-->>RangeSessionHistory: sessions
  RangeSessionHistory-->>FirearmsView: onChange -> router.refresh()
Loading

Possibly related PRs

  • unclesp1d3r/mag_stacker#13: Changes the same firearm visibility and permission-resolution path that this PR uses to gate session controls.
  • unclesp1d3r/mag_stacker#25: Extends the same Playwright fixture and test-server harness used here for multi-user range-session coverage.

Suggested labels: enhancement, backend, frontend, shared, testing, priority:medium

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The core feature lands, but #11 also calls for ammo decrement and service-interval integration, which this PR explicitly defers. Implement the ammoId-triggered ammo decrement and service-interval integration, or split those deferrals into separate linked work.
Out of Scope Changes check ⚠️ Warning The PR adds unrelated planning and troubleshooting docs plus dependency/version bumps that are not required by #11. Move non-feature docs and tool/dependency updates to a separate PR unless they are required for this feature.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed It follows Conventional Commits and accurately describes the feature scope.
Description check ✅ Passed The description clearly matches the shot-count tracking changes and supporting tooling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch 11-shot-count-tracking-per-firearm

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

Copilot AI 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.

Pull request overview

Implements firearm-scoped range session logging to derive and display lifetime round totals (issue #11), including owner/grant-based authorization inherited from the parent firearm, plus end-to-end coverage and a small e2e harness/tooling reliability fix.

Changes:

  • Add range_session child table + migration, factories, validation, and visibility-scoped domain service (CRUD + derived totals).
  • Wire server actions and firearms UI to show lifetime totals and load session history on demand with permission-based gating.
  • Add integration + Playwright e2e coverage, and harden local e2e reliability by serving Next in-process; fix sqlfluff hook dialect/config.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/test-support/factories.ts Adds makeRangeSession DB factory for integration tests.
src/test-support/assertions.ts Adds expectRejects helper for Drizzle thenables in Bun tests.
src/domain/validation-messages.ts Adds user-facing messages for range-session validation codes.
src/domain/range-sessions/validate.ts Introduces pure range-session input validation.
src/domain/range-sessions/service.ts Adds authorized CRUD + derived lifetime totals for sessions.
src/domain/range-sessions/tests/validate.test.ts Unit tests for range-session validation behavior.
src/domain/range-sessions/tests/service.test.ts Integration tests for auth + totals + cascade behaviors.
src/db/migrations/meta/0006_snapshot.json Updates Drizzle snapshot metadata for new table.
src/db/migrations/meta/_journal.json Records new migration in Drizzle journal.
src/db/migrations/0006_wakeful_exodus.sql Generated migration creating range_session table + index/FK/check.
src/db/inventory-schema.ts Defines rangeSession table schema, constraints, and index.
src/auth/visibility.ts Adds visibleFirearmPermissions for per-firearm UI gating signal.
e2e/start-test-server.ts Serves Next in-process via nextStart to prevent env clobbering locally.
e2e/start-app.ts Removes no-longer-needed spawned-app shim.
e2e/range-sessions.spec.ts Adds e2e coverage for log/sum/delete derived total behavior.
e2e/fixtures/user-pool.ts Adds range-sessions seeded user key for the new spec.
docs/solutions/test-failures/e2e-dotenv-mise-clobbers-launcher-env.md Documents the local e2e env-clobber failure mode and fix.
docs/plans/2026-07-03-001-feat-shot-count-tracking-plan.md Captures the implementation plan and technical decisions for #11.
app/(app)/firearms/session-actions.ts Adds server actions for session CRUD + history reads.
app/(app)/firearms/range-session-history.tsx Adds on-demand session history panel with edit/delete gating.
app/(app)/firearms/range-session-form.tsx Adds client form for logging/editing sessions with validation feedback.
app/(app)/firearms/page.tsx Joins derived totals + permissions into firearms list items.
app/(app)/firearms/firearms-view.tsx Adds “Rounds” column and “Sessions” panel entrypoint.
.sqlfluff Configures sqlfluff dialect to Postgres.
.pre-commit-config.yaml Excludes generated migrations from sqlfluff hooks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/domain/range-sessions/service.ts
Comment thread src/domain/range-sessions/validate.ts
Comment thread src/domain/range-sessions/__tests__/validate.test.ts

@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: 3

🤖 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 @.pre-commit-config.yaml:
- Around line 59-66: The SQLFluff hooks in .pre-commit-config.yaml are excluding
src/db/migrations/ entirely, which leaves migration SQL unchecked. Update the
sqlfluff-lint and sqlfluff-fix entries to keep migrations covered by narrowing
the suppression to only the generator-specific rule(s), or add a CI migration
validation step for the drizzle-kit output. Use the existing sqlfluff-lint and
sqlfluff-fix hook definitions as the place to fix this gating gap.

In `@app/`(app)/firearms/firearms-view.tsx:
- Line 59: The sessions panel is holding a stale snapshot of the selected
firearm because `sessionsFor` stores the whole `FirearmListItem` and is never
re-derived after `router.refresh()`. Update `firearms-view.tsx` to store only
the selected firearm id in the sessions state, then derive the current item from
the live `firearms` list on each render before passing it to
`RangeSessionHistory`. Also update the `setSessionsFor(...)` call sites to use
the id-based setter and clear it with null so renamed firearms or revoked grants
immediately reflect in the open panel.

In `@e2e/start-test-server.ts`:
- Around line 29-37: The test harness relies on the internal
next/dist/cli/next-start entry point from next in start-test-server, which is
unsafe while package.json still allows a floating ^16.2.9 range. Update the
dependency definition for next to an exact pinned version if nextStart is kept,
and keep the import in start-test-server aligned with that fixed version so
future patch/minor upgrades do not break this harness. Reference the nextStart
import and the next dependency declaration when making the change.
🪄 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: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 74269660-d362-456a-b487-ed6e9c8f2d58

📥 Commits

Reviewing files that changed from the base of the PR and between 7e15a54 and f235ac2.

📒 Files selected for processing (25)
  • .pre-commit-config.yaml
  • .sqlfluff
  • app/(app)/firearms/firearms-view.tsx
  • app/(app)/firearms/page.tsx
  • app/(app)/firearms/range-session-form.tsx
  • app/(app)/firearms/range-session-history.tsx
  • app/(app)/firearms/session-actions.ts
  • docs/plans/2026-07-03-001-feat-shot-count-tracking-plan.md
  • docs/solutions/test-failures/e2e-dotenv-mise-clobbers-launcher-env.md
  • e2e/fixtures/user-pool.ts
  • e2e/range-sessions.spec.ts
  • e2e/start-app.ts
  • e2e/start-test-server.ts
  • src/auth/visibility.ts
  • src/db/inventory-schema.ts
  • src/db/migrations/0006_wakeful_exodus.sql
  • src/db/migrations/meta/0006_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/domain/range-sessions/__tests__/service.test.ts
  • src/domain/range-sessions/__tests__/validate.test.ts
  • src/domain/range-sessions/service.ts
  • src/domain/range-sessions/validate.ts
  • src/domain/validation-messages.ts
  • src/test-support/assertions.ts
  • src/test-support/factories.ts
💤 Files with no reviewable changes (1)
  • e2e/start-app.ts

Comment thread .pre-commit-config.yaml Outdated
Comment thread app/(app)/firearms/firearms-view.tsx Outdated
Comment thread e2e/start-test-server.ts
…iduals

- Derive the round-total visible set from visibleFirearmPermissions instead
  of a second getVisibleIds pass: lifetimeRoundTotals takes an optional
  pre-resolved set; the firearms page feeds it the permission map's keys, so
  the feature adds one owned∪granted scan (gating + totals), not three.
- Add a two-user Playwright spec for AE2 at the UI layer: an owner shares a
  firearm view-only, and a second browser context (the viewer's session)
  reads the lifetime total and history but sees no Log/Edit/Delete controls
  (KTD7). Exports storageStateFor for multi-user specs.

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
@unclesp1d3r unclesp1d3r self-assigned this Jul 3, 2026
…ust to version 1.55.1

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
…o version 26.1.0

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
@coderabbitai coderabbitai Bot added the dependencies Pull requests that update a dependency file label Jul 3, 2026
- lifetimeRoundTotals keeps sum(rounds_fired) as bigint and parses at the
  edge instead of casting ::int (removes int4 overflow risk). [copilot]
- validateRangeSession rejects impossible calendar days that Date.parse
  normalizes (e.g. 2026-02-31); add day-overflow tests. [copilot]
- firearms-view derives the sessions panel's firearm from the live list by
  id, so a rename/revoked-grant/delete reflects immediately instead of a
  stale snapshot. [coderabbit]

Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
Signed-off-by: UncleSp1d3r <unclesp1d3r@evilbitlabs.io>
@unclesp1d3r
unclesp1d3r enabled auto-merge (squash) July 3, 2026 23:38
@unclesp1d3r
unclesp1d3r merged commit 97edd44 into main Jul 3, 2026
7 checks passed
@unclesp1d3r
unclesp1d3r deleted the 11-shot-count-tracking-per-firearm branch July 3, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shot count tracking per firearm

2 participants