Files
GearBox/.planning/phases/37-admin-global-item-management/37-REVIEW.md

4.1 KiB

phase, status, depth, files_reviewed, findings, reviewed_at
phase status depth files_reviewed findings reviewed_at
37 issues_found standard 9
critical warning info total
0 3 2 5
2026-04-19

Code Review: Phase 37 — Admin Global Item Management

Summary

9 files reviewed at standard depth. No critical issues. 3 warnings (real bugs / UX gaps worth fixing), 2 info notes.


Findings

WR-01 — Tags not populated from loaded item on edit page

Severity: warning File: src/client/routes/admin/items.$itemId.tsx (line 130)

The useEffect that populates form state from the loaded item hardcodes tags: [] instead of pulling tags from item. Since AdminGlobalItemDetail extends Omit<AdminGlobalItem, "tags">, tags are not present on the detail endpoint response. However, this means a user saving the form immediately will send tags: [] to the PUT endpoint, which calls syncGlobalItemTags with an empty array — silently clearing all existing tags.

Fix options:

  1. Include tags in the GET /api/admin/items/:id response (extend getGlobalItemWithOwnerCount to also fetch tags).
  2. Populate tags on the list page item click by passing them through navigation state, or fetch them separately on the edit page.

The safest fix is option 1: add a tag fetch to getGlobalItemWithOwnerCount and include them in AdminGlobalItemDetail.


WR-02 — Save always sends tags: form.tags (empty array clears tags)

Severity: warning File: src/client/routes/admin/items.$itemId.tsx (line 166)

Related to WR-01. The handleSave function always includes tags: form.tags in the PUT payload, even when the user hasn't interacted with the tag field. Since form.tags starts as [], saving without touching tags will call updateGlobalItemById with tags: [], which triggers syncGlobalItemTags(tx, id, []) — deleting all existing tags silently.

Fix: Only include tags in the payload when the user has explicitly modified the tag field. Track a tagsModified boolean flag, or use undefined as the default form tags value and only set it to [] when the user clears the last tag.


WR-03 — limit query param negative values not rejected

Severity: warning File: src/server/routes/admin-items.ts (line 48)

The limit guard is:

limit: isNaN(limit) || limit > 100 ? 50 : limit,

This passes through negative limit values (e.g., ?limit=-1) to listGlobalItemsForAdmin, which passes them directly to Drizzle's .limit(). SQLite treats LIMIT -1 as no limit — returning all rows. This is an admin-only endpoint so exploitation risk is low, but it could cause unintended large payloads.

Fix: Add limit <= 0 to the fallback condition:

limit: isNaN(limit) || limit <= 0 || limit > 100 ? 50 : limit,

INFO-01 — type Env = { Variables: { db?: any; userId?: number } } duplicated

Severity: info Files: src/server/routes/admin-items.ts (line 12), src/server/routes/admin.ts (line 4)

Both new files define Env locally using any for db. This is consistent with the existing pattern in other route files (e.g., global-items.ts), but it suppresses type safety on the db variable. Not a new issue — pre-existing pattern.


INFO-02 — AdminGlobalItemDetail redundantly declares ownerCount

Severity: info File: src/client/hooks/useAdminGlobalItems.ts (line 40)

export interface AdminGlobalItemDetail extends Omit<AdminGlobalItem, "tags"> {
  ownerCount: number;
}

AdminGlobalItem already includes ownerCount: number, so Omit<AdminGlobalItem, "tags"> already carries ownerCount. The explicit re-declaration is harmless but redundant. Minor cleanup opportunity.


Self-Check

  • No SQL injection vectors (Drizzle parameterized queries throughout)
  • Auth protection: all admin routes inherit requireAuth + requireAdmin from parent router
  • Transaction safety: delete and update operations use db.transaction
  • FK integrity: delete nullifies items.globalItemId before deleting
  • Input validation: PUT body validated via zValidator with Zod schema
  • No secrets or credentials in code
  • Build passes, tests pass