209 lines
18 KiB
Markdown
209 lines
18 KiB
Markdown
# Project Research Summary
|
|
|
|
**Project:** GearBox v1.2 -- Collection Power-Ups
|
|
**Domain:** Gear management (bikepacking, sim racing, etc.) -- feature enhancement milestone
|
|
**Researched:** 2026-03-16
|
|
**Confidence:** HIGH
|
|
|
|
## Executive Summary
|
|
|
|
GearBox v1.2 adds six features to the existing gear management app: item search/filter, weight classification (base/worn/consumable), weight distribution charts, candidate status tracking, weight unit selection, and a planning category filter upgrade. Research confirms that four of six features require zero new dependencies -- they are pure application logic built on the existing stack (Drizzle ORM, React Query, Zod, Tailwind). The sole new dependency is `react-minimal-pie-chart` (~2kB gzipped) for donut chart visualization. The codebase is well-positioned for these additions: the settings table already supports key-value preferences, the `setup_items` join table is the correct place for weight classification, and the client-side data model is small enough for in-memory filtering.
|
|
|
|
The recommended approach is to build weight unit selection first because it refactors the `formatWeight` function that every subsequent feature depends on for display. Search/filter and candidate status tracking are independent and low-risk. Weight classification is the most architecturally significant change -- it adds a column to the `setup_items` join table and changes the sync API shape from `{ itemIds: number[] }` to `{ items: Array<{ itemId, weightClass }> }`. Weight distribution charts come last because they depend on both the unit formatter and the classification data. The two schema changes (columns on `setup_items` and `thread_candidates`) should be batched into a single Drizzle migration.
|
|
|
|
The primary risks are: (1) weight unit conversion rounding drift from bidirectional conversion in edit forms, (2) accidentally placing weight classification on the `items` table instead of the `setup_items` join table, and (3) chart data diverging from displayed totals due to separate computation paths. All three are preventable with clear architectural rules established in the first phase: store grams canonically, convert only at the display boundary, and use a single source of truth for weight computations.
|
|
|
|
## Key Findings
|
|
|
|
### Recommended Stack
|
|
|
|
The existing stack (React 19, Hono, Drizzle ORM, SQLite, Bun) handles all v1.2 features without modification. One small library addition is needed.
|
|
|
|
**Core technologies (all existing, no changes):**
|
|
- **Drizzle ORM `like()`, `eq()`, `and()`**: Available for server-side filtering if needed in the future, but client-side filtering is preferred at this scale
|
|
- **Zod `z.enum()`**: Validates weight classification (`"base" | "worn" | "consumable"`) and candidate status (`"researching" | "ordered" | "arrived"`) with compile-time type safety
|
|
- **React Query `useSetting()`**: Reactive settings caching ensures unit preference changes propagate to all weight displays without page refresh
|
|
- **Existing `settings` table**: Key-value store supports weight unit preference with no schema change
|
|
|
|
**New dependency:**
|
|
- **react-minimal-pie-chart ^9.1.2**: Donut/pie charts at ~2kB gzipped. React 19 compatible (explicit in peerDeps). Zero external dependencies. TypeScript native. Chosen over Recharts (~97kB, React 19 rendering issues reported) and Chart.js (~60kB, canvas-based, harder to style with Tailwind).
|
|
|
|
**What NOT to add:**
|
|
- Recharts, Chart.js, or visx (massive overkill for one chart type)
|
|
- Fuse.js or FTS5 (overkill for name search on sub-1000 item collections)
|
|
- XState (candidate status is a simple enum, not a complex state machine)
|
|
- i18n library for unit conversion (four constants and a formatter function)
|
|
|
|
### Expected Features
|
|
|
|
**Must have (table stakes):**
|
|
- **Search items by name** -- every competitor with an inventory has search; LighterPack notably lacks it and users complain
|
|
- **Filter items by category** -- partially exists in planning view, missing from collection view
|
|
- **Weight unit selection (g/oz/lb/kg)** -- universal across all competitors; gear specs come in mixed units
|
|
- **Weight classification (base/worn/consumable)** -- pioneered by LighterPack, now industry standard; "base weight" is the core metric of the ultralight community
|
|
|
|
**Should have (differentiators):**
|
|
- **Weight distribution donut chart** -- LighterPack's pie chart is cited as its best feature; GearBox can uniquely combine category and classification breakdown
|
|
- **Candidate status tracking (researching/ordered/arrived)** -- entirely unique to GearBox's planning thread concept; no competitor has purchase lifecycle tracking
|
|
- **Per-setup classification** -- architecturally superior to competitors; the same item can be classified differently across setups
|
|
|
|
**Defer (v2+):**
|
|
- Per-item weight input in multiple units (parsing complexity)
|
|
- Interactive chart drill-down (click to zoom into categories)
|
|
- Weight goals/targets (opinionated norms conflict with hobby-agnostic design)
|
|
- Custom weight classification labels beyond base/worn/consumable
|
|
- Server-side full-text search (premature for single-user scale)
|
|
- Status change timestamps on candidates (useful but not essential now)
|
|
|
|
### Architecture Approach
|
|
|
|
All v1.2 features integrate into the existing three-layer architecture (client/server/database) with minimal structural changes. The client layer gains 5 new files (SearchBar, WeightChart, UnitSelector components; useFormatWeight hook; migration SQL) and modifies 15 existing files. The server layer changes are limited to the setup service (weight classification PATCH endpoint, updated sync function) and thread service (candidate status field passthrough). No new route registrations are needed in `src/server/index.ts`. The API layer (`lib/api.ts`) and UI state store (`uiStore.ts`) require no changes.
|
|
|
|
**Major components:**
|
|
1. **`useFormatWeight` hook** -- single source of truth for unit-aware weight formatting; wraps `useSetting("weightUnit")` and `formatWeight(grams, unit)` so all weight displays stay consistent
|
|
2. **`WeightChart` component** -- reusable donut chart wrapper; used in collection page (weight by category) and setup detail page (weight by classification)
|
|
3. **`SearchBar` component** -- reusable search input with clear button; collection page filters via `useMemo` over the cached `useItems()` data
|
|
4. **Updated `syncSetupItems`** -- breaking API change from `{ itemIds: number[] }` to `{ items: Array<{ itemId, weightClass }> }`; single call site (ItemPicker.tsx) makes this safe
|
|
5. **`PATCH /api/setups/:id/items/:itemId`** -- new endpoint for updating weight classification without triggering full sync (which would destroy classification data)
|
|
|
|
### Critical Pitfalls
|
|
|
|
1. **Weight unit conversion rounding drift** -- bidirectional conversion in edit forms causes grams to drift over multiple edit cycles. Always load stored grams from the API, convert for display, and convert user input back to grams once on save. Never re-convert from a previously displayed value.
|
|
|
|
2. **Weight classification at the wrong level** -- placing `classification` on the `items` table instead of `setup_items` prevents per-setup classification. A rain jacket is "worn" in summer but "base weight" in winter. This is the single most important schema decision in v1.2 and is costly to reverse.
|
|
|
|
3. **Chart data diverging from displayed totals** -- the codebase already has two computation paths (SQL aggregates in `totals.service.ts` vs. JavaScript reduce in `$setupId.tsx`). Adding charts creates a third. Use a shared utility for weight summation and convert units only at the final display step.
|
|
|
|
4. **Server-side search for client-side data** -- adding search API parameters creates React Query cache fragmentation and unnecessary latency. Keep filtering client-side with `useMemo` over the cached items array.
|
|
|
|
5. **Test helper desync with schema** -- the manual `createTestDb()` in `tests/helpers/db.ts` duplicates schema in raw SQL. Every column addition must be mirrored there or tests pass against the wrong schema.
|
|
|
|
## Implications for Roadmap
|
|
|
|
Based on combined research, a 5-phase structure is recommended:
|
|
|
|
### Phase 1: Weight Unit Selection
|
|
|
|
**Rationale:** Foundational infrastructure. The `formatWeight` refactor touches every component that displays weight (~8 call sites). All subsequent features depend on this formatter working correctly with unit awareness. Building this first means classification totals, chart labels, and setup breakdowns automatically display in the user's preferred unit.
|
|
|
|
**Delivers:** Global weight unit preference (g/oz/lb/kg) stored in settings, `useFormatWeight` hook, updated `formatWeight` function, UnitSelector component in TotalsBar, correct unit display across all existing weight surfaces (ItemCard, CandidateCard, CategoryHeader, TotalsBar, setup detail), correct unit handling in ItemForm and CandidateForm weight inputs.
|
|
|
|
**Addresses:** Weight unit selection (table stakes from FEATURES.md)
|
|
|
|
**Avoids:** Rounding drift (Pitfall 1), inconsistent unit application (Pitfall 7), flash of unconverted weights on load
|
|
|
|
**Schema changes:** None (uses existing settings table key-value store)
|
|
|
|
### Phase 2: Search, Filter, and Planning Category Filter
|
|
|
|
**Rationale:** Pure client-side addition with no schema changes, no API changes, and no dependencies on other v1.2 features. Immediately useful as collections grow. The planning category filter upgrade fits naturally here since both involve filter UX and the icon-aware dropdown is a shared component.
|
|
|
|
**Delivers:** Search input in collection view, icon-aware category filter dropdown (reused in gear and planning tabs), filtered item display with count ("showing 12 of 47 items"), URL search param persistence, empty state for no results, result count display.
|
|
|
|
**Addresses:** Search items by name (table stakes), filter by category (table stakes), planning category filter upgrade (differentiator)
|
|
|
|
**Avoids:** Server-side search anti-pattern (Pitfall 3), search state lost on tab switch (UX pitfall), category groups disappearing incorrectly during filtering
|
|
|
|
**Schema changes:** None
|
|
|
|
### Phase 3: Candidate Status Tracking
|
|
|
|
**Rationale:** Simple schema change on `thread_candidates` with minimal integration surface. Independent of other features. Low complexity but requires awareness of the existing thread resolution flow. Schema change should be batched with Phase 4 into one Drizzle migration.
|
|
|
|
**Delivers:** Status column on candidates (researching/ordered/arrived), status badge on CandidateCard with click-to-cycle, status field in CandidateForm, Zod enum validation, status transition validation in service layer (researching -> ordered -> arrived, no backward transitions).
|
|
|
|
**Addresses:** Candidate status tracking (differentiator -- unique to GearBox)
|
|
|
|
**Avoids:** Status without transition validation (Pitfall 4), test helper desync (Pitfall 6), not handling candidate status during thread resolution
|
|
|
|
**Schema changes:** Add `status TEXT NOT NULL DEFAULT 'researching'` to `thread_candidates`
|
|
|
|
### Phase 4: Weight Classification
|
|
|
|
**Rationale:** Most architecturally significant change in v1.2. Changes the sync API shape (breaking change, single call site). Requires Phase 1 to be complete so classification totals display in the correct unit. Schema migration should be batched with Phase 3.
|
|
|
|
**Delivers:** `weightClass` column on `setup_items`, updated sync endpoint accepting `{ items: Array<{ itemId, weightClass }> }`, new `PATCH /api/setups/:id/items/:itemId` endpoint, three-segment classification toggle per item in setup detail view, base/worn/consumable weight subtotals.
|
|
|
|
**Addresses:** Weight classification base/worn/consumable (table stakes), per-setup classification (differentiator)
|
|
|
|
**Avoids:** Classification on items table (Pitfall 2), test helper desync (Pitfall 6), losing classification data on sync
|
|
|
|
**Schema changes:** Add `weight_class TEXT NOT NULL DEFAULT 'base'` to `setup_items`
|
|
|
|
### Phase 5: Weight Distribution Charts
|
|
|
|
**Rationale:** Depends on Phase 1 (unit-aware labels) and Phase 4 (classification data for setup breakdown). Only phase requiring a new npm dependency. Highest UI complexity but lowest architectural risk -- read-only visualization of existing data.
|
|
|
|
**Delivers:** `react-minimal-pie-chart` integration, `WeightChart` component, collection-level donut chart (weight by category from `useTotals()`), setup-level donut chart (weight by classification), chart legend with consistent colors, hover tooltips with formatted weights.
|
|
|
|
**Addresses:** Weight distribution visualization (differentiator)
|
|
|
|
**Avoids:** Chart/totals divergence (Pitfall 5), chart crashing on null-weight items, unnecessary chart re-renders on unrelated state changes
|
|
|
|
**Schema changes:** None (npm dependency: `bun add react-minimal-pie-chart`)
|
|
|
|
### Phase Ordering Rationale
|
|
|
|
- **Phase 1 first** because `formatWeight` is called by every weight-displaying component. Refactoring it after other features are built means touching the same files twice.
|
|
- **Phase 2 is independent** and could be built in any order, but sequencing it second allows the team to ship a quick win while Phase 3/4 schema changes are designed.
|
|
- **Batch Phase 3 + Phase 4 schema migrations** into one `bun run db:generate` run. Both add columns to existing tables; a single migration simplifies deployment.
|
|
- **Phase 4 after Phase 1** because classification totals need the unit-aware formatter.
|
|
- **Phase 5 last** because it is pure visualization depending on data from Phases 1 and 4, and introduces the only external dependency.
|
|
|
|
### Research Flags
|
|
|
|
Phases likely needing deeper research during planning:
|
|
- **Phase 4 (Weight Classification):** The sync API shape change is breaking. The existing delete-all/re-insert pattern destroys classification data. Needs careful design of the PATCH endpoint and how ItemPicker interacts with classification preservation during item add/remove. Worth a `/gsd:research-phase`.
|
|
- **Phase 5 (Weight Distribution Charts):** react-minimal-pie-chart API specifics (label rendering, responsive sizing, animation control) should be validated with a quick prototype. Consider a short research spike.
|
|
|
|
Phases with standard patterns (skip research-phase):
|
|
- **Phase 1 (Weight Unit Selection):** Well-documented pattern. Extend `formatWeight`, add a `useSetting` wrapper, propagate through components. No unknowns.
|
|
- **Phase 2 (Search/Filter):** Textbook client-side filtering with `useMemo`. No API changes. Standard React pattern.
|
|
- **Phase 3 (Candidate Status):** Simple column addition with Zod enum validation. Existing `useUpdateCandidate` mutation already handles partial updates.
|
|
|
|
## Confidence Assessment
|
|
|
|
| Area | Confidence | Notes |
|
|
|------|------------|-------|
|
|
| Stack | HIGH | Only one new dependency (react-minimal-pie-chart). React 19 compatibility verified via package.json peerDeps. All other features use existing stack with no changes. |
|
|
| Features | HIGH | Feature set derived from analysis of 8+ competing tools (LighterPack, Hikt, PackLight, Packstack, HikeLite, Packrat, OutPack, BPL Calculator). Clear consensus on table stakes vs. differentiators. |
|
|
| Architecture | HIGH | Based on direct codebase analysis with integration points mapped to specific files. The 5 new / 15 modified file inventory is concrete and verified against the existing codebase. |
|
|
| Pitfalls | HIGH | Derived from codebase-specific patterns (test helper duplication, dual computation paths) combined with domain risks (unit conversion rounding, classification scope). Not generic warnings. |
|
|
|
|
**Overall confidence:** HIGH
|
|
|
|
### Gaps to Address
|
|
|
|
- **`lb` display format:** FEATURES.md suggests "2 lb 3 oz" (pounds + remainder ounces) while STACK.md suggests simpler decimal format. The traditional "lb + oz" format is more useful to American users but adds formatting complexity. Decide during Phase 1 implementation.
|
|
- **Status change timestamps:** PITFALLS.md recommends storing `statusChangedAt` alongside `status` for staleness detection ("ordered 30 days ago -- still waiting?"). Low effort to add during the schema migration. Decide during Phase 3 planning.
|
|
- **Sync API backward compatibility:** The sync endpoint shape changes from `{ itemIds: number[] }` to `{ items: [...] }`. Single call site (ItemPicker.tsx), but verify no external consumers exist before shipping.
|
|
- **react-minimal-pie-chart responsive behavior:** SVG-based and should handle responsive sizing, but exact approach (CSS width vs. explicit size prop) should be validated in Phase 5. Not a risk, just a detail to confirm.
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
- [Drizzle ORM Filter Operators](https://orm.drizzle.team/docs/operators) -- like, eq, and operators for search/filter
|
|
- [Drizzle ORM Conditional Filters Guide](https://orm.drizzle.team/docs/guides/conditional-filters-in-query) -- dynamic filter composition
|
|
- [react-minimal-pie-chart GitHub](https://github.com/toomuchdesign/react-minimal-pie-chart) -- v9.1.2, React 19 peerDeps verified in package.json
|
|
- [LighterPack](https://lighterpack.com/) -- base/worn/consumable classification standard, pie chart visualization pattern
|
|
- [99Boulders LighterPack Tutorial](https://www.99boulders.com/lighterpack-tutorial) -- classification definitions and feature walkthrough
|
|
- [BackpackPeek Pack Weight Calculator Guide](https://backpackpeek.com/blog/pack-weight-calculator-base-weight-guide) -- weight classification methodology
|
|
- Direct codebase analysis of GearBox v1.1 -- schema.ts, services, hooks, routes, test helpers
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- [Hikt](https://hikt.app/) -- searchable gear closet, base vs worn weight display
|
|
- [PackLight (iOS)](https://apps.apple.com/us/app/packlight-for-backpacking/id1054845207) -- search, categories, bar graph visualization
|
|
- [Packstack](https://www.packstack.io/) -- base/worn/consumable weight separation
|
|
- [Packrat](https://www.packrat.app/) -- flexible weight unit input and display conversion
|
|
- [Recharts React 19 issue #6857](https://github.com/recharts/recharts/issues/6857) -- rendering issues with React 19.2.3
|
|
- [TanStack Query filtering discussions](https://github.com/TanStack/query/discussions/1113) -- client-side vs server-side filtering patterns
|
|
- [LogRocket Best React Chart Libraries 2025](https://blog.logrocket.com/best-react-chart-libraries-2025/) -- chart library comparison
|
|
|
|
### Tertiary (LOW confidence)
|
|
- [SQLite LIKE case sensitivity](https://github.com/drizzle-team/drizzle-orm-docs/issues/239) -- LIKE is case-insensitive in SQLite (relevant only if search moves server-side)
|
|
- [Drizzle ORM SQLite migration pitfalls #1313](https://github.com/drizzle-team/drizzle-orm/issues/1313) -- data loss bug with push + add column (monitor during migration)
|
|
|
|
---
|
|
*Research completed: 2026-03-16*
|
|
*Ready for roadmap: yes*
|