---
phase: 20-fab-full-screen-catalog-search
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/server/services/tag.service.ts
  - src/server/routes/tags.ts
  - src/server/index.ts
  - src/client/stores/uiStore.ts
  - src/client/hooks/useTags.ts
  - src/client/hooks/useGlobalItems.ts
  - tests/services/tag.service.test.ts
  - tests/routes/tags.test.ts
autonomous: true
requirements:
  - CATFLOW-01
  - CATFLOW-02

must_haves:
  truths:
    - "GET /api/tags returns all tags from the database"
    - "GET /api/global-items endpoint is reachable (route registered)"
    - "UIStore exposes fabMenu and catalogSearch state slices"
    - "useGlobalItems hook supports optional tags parameter"
    - "useTags hook fetches and caches tag data"
  artifacts:
    - path: "src/server/services/tag.service.ts"
      provides: "getAllTags function"
      exports: ["getAllTags"]
    - path: "src/server/routes/tags.ts"
      provides: "GET /api/tags endpoint"
      exports: ["tagRoutes"]
    - path: "src/client/stores/uiStore.ts"
      provides: "FAB menu + catalog search state"
      contains: "fabMenuOpen"
    - path: "src/client/hooks/useTags.ts"
      provides: "Tag fetching hook"
      exports: ["useTags"]
    - path: "src/client/hooks/useGlobalItems.ts"
      provides: "Updated hook with tag support"
      contains: "tags"
  key_links:
    - from: "src/server/routes/tags.ts"
      to: "src/server/services/tag.service.ts"
      via: "getAllTags import"
      pattern: "getAllTags"
    - from: "src/server/index.ts"
      to: "src/server/routes/tags.ts"
      via: "app.route registration"
      pattern: "app\\.route.*tags"
    - from: "src/server/index.ts"
      to: "src/server/routes/global-items.ts"
      via: "app.route registration"
      pattern: "app\\.route.*global-items"
---

<objective>
Create the server-side tags endpoint, register the global-items route, extend UIStore with FAB/catalog-search state, and update client hooks for tag-aware searching.

Purpose: Provides the data layer (tags API), state management (UIStore), and hooks that Plan 02's UI components will consume.
Output: Working GET /api/tags endpoint, registered GET /api/global-items, extended UIStore, useTags hook, updated useGlobalItems hook with tag support.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
@.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md

@src/server/index.ts
@src/server/routes/global-items.ts
@src/server/services/global-item.service.ts
@src/db/schema.ts
@src/client/stores/uiStore.ts
@src/client/hooks/useGlobalItems.ts
@src/client/lib/api.ts
@tests/routes/global-items.test.ts
@tests/helpers/db.ts

<interfaces>
<!-- Key types and contracts the executor needs -->

From src/db/schema.ts:
```typescript
export const tags = pgTable("tags", {
  id: serial("id").primaryKey(),
  name: text("name").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
```

From src/client/lib/api.ts:
```typescript
export async function apiGet<T>(path: string): Promise<T>;
```

From src/client/stores/uiStore.ts (existing pattern):
```typescript
export const useUIStore = create<UIState>((set) => ({ ... }));
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Tag service, route, route registrations, and tests</name>
  <files>
    src/server/services/tag.service.ts,
    src/server/routes/tags.ts,
    src/server/index.ts,
    tests/services/tag.service.test.ts,
    tests/routes/tags.test.ts
  </files>
  <read_first>
    src/db/schema.ts,
    src/server/index.ts,
    src/server/routes/global-items.ts,
    src/server/services/global-item.service.ts,
    tests/routes/global-items.test.ts,
    tests/helpers/db.ts
  </read_first>
  <behavior>
    - Test: getAllTags returns all tags from the database as { id, name }[] (no createdAt)
    - Test: getAllTags returns empty array when no tags exist
    - Test: GET /api/tags returns 200 with array of tag objects
    - Test: GET /api/global-items still works after route registration (existing tests pass)
  </behavior>
  <action>
    1. Create `src/server/services/tag.service.ts`:
       - Export `getAllTags(db)` function that selects `{ id: tags.id, name: tags.name }` from the `tags` table, ordered alphabetically by name (per D-17, alphabetical is a reasonable default).
       - Import `tags` from `../../db/schema.ts`.
       - Use the same `type Db = typeof prodDb` pattern as other services.

    2. Create `src/server/routes/tags.ts`:
       - Single GET "/" handler that calls `getAllTags(db)` and returns `c.json(allTags)`.
       - Export as `tagRoutes`.
       - Follow the exact pattern from `src/server/routes/global-items.ts` (Hono + Env type).

    3. Update `src/server/index.ts`:
       - Add import: `import { globalItemRoutes } from "./routes/global-items.ts";`
       - Add import: `import { tagRoutes } from "./routes/tags.ts";`
       - Register both routes AFTER existing route registrations (before MCP block):
         `app.route("/api/global-items", globalItemRoutes);`
         `app.route("/api/tags", tagRoutes);`
       - CRITICAL: The global-items route file EXISTS but is NOT registered (research pitfall #1). Must add it here.
       - Add public route skip in auth middleware for tags: `if (c.req.path.startsWith("/api/tags") && c.req.method === "GET") return next();`
       - Add public route skip for global-items: `if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET") return next();`

    4. Create `tests/services/tag.service.test.ts`:
       - Use `createTestDb()` from `tests/helpers/db.ts`.
       - Test: returns empty array when no tags.
       - Test: returns all tags as `{ id, name }` after inserting test data.
       - Seed tags using `db.insert(tags).values(...)`.

    5. Create `tests/routes/tags.test.ts`:
       - Follow pattern from `tests/routes/global-items.test.ts`.
       - Use the app with test db injection.
       - Test: GET /api/tags returns 200 with JSON array.
       - Test: response contains expected tags after seeding.
  </action>
  <verify>
    <automated>bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - File `src/server/services/tag.service.ts` exists and exports `getAllTags`
    - File `src/server/routes/tags.ts` exists and exports `tagRoutes`
    - `src/server/index.ts` contains `app.route("/api/tags", tagRoutes)`
    - `src/server/index.ts` contains `app.route("/api/global-items", globalItemRoutes)`
    - `src/server/index.ts` contains auth skip for `/api/tags` and `/api/global-items` GET requests
    - All tag tests pass: `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts`
    - Existing global-items tests still pass: `bun test tests/routes/global-items.test.ts`
  </acceptance_criteria>
  <done>GET /api/tags returns all tags. GET /api/global-items is registered and reachable. All tests green.</done>
</task>

<task type="auto">
  <name>Task 2: UIStore extension + useTags hook + useGlobalItems tag support</name>
  <files>
    src/client/stores/uiStore.ts,
    src/client/hooks/useTags.ts,
    src/client/hooks/useGlobalItems.ts
  </files>
  <read_first>
    src/client/stores/uiStore.ts,
    src/client/hooks/useGlobalItems.ts,
    src/client/lib/api.ts,
    .planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
  </read_first>
  <action>
    1. Extend `src/client/stores/uiStore.ts` per D-21, D-22, D-23:
       - Add to UIState interface:
         ```
         fabMenuOpen: boolean;
         openFabMenu: () => void;
         closeFabMenu: () => void;
         catalogSearchOpen: boolean;
         catalogSearchMode: "collection" | "thread" | null;
         openCatalogSearch: (mode: "collection" | "thread") => void;
         closeCatalogSearch: () => void;
         ```
       - Add to store implementation:
         ```
         fabMenuOpen: false,
         openFabMenu: () => set({ fabMenuOpen: true }),
         closeFabMenu: () => set({ fabMenuOpen: false }),
         catalogSearchOpen: false,
         catalogSearchMode: null,
         openCatalogSearch: (mode) => set({ catalogSearchOpen: true, catalogSearchMode: mode, fabMenuOpen: false }),
         closeCatalogSearch: () => set({ catalogSearchOpen: false, catalogSearchMode: null }),
         ```
       - Note: `openCatalogSearch` also closes the FAB menu (natural flow: tap FAB -> tap option -> menu closes, overlay opens).

    2. Create `src/client/hooks/useTags.ts`:
       - Export `useTags()` hook using `useQuery` from `@tanstack/react-query`.
       - Query key: `["tags"]`.
       - Query fn: `apiGet<Tag[]>("/api/tags")` where `Tag = { id: number; name: string }`.
       - Set `staleTime: 5 * 60 * 1000` (tags change rarely, cache 5 min).
       - Export the `Tag` interface as well.

    3. Update `src/client/hooks/useGlobalItems.ts`:
       - Modify `useGlobalItems` signature to accept optional `tags?: string[]` parameter.
       - Build query string using `URLSearchParams`:
         ```
         const params = new URLSearchParams();
         if (query) params.set("q", query);
         if (tags && tags.length > 0) params.set("tags", tags.join(","));
         const qs = params.toString();
         ```
       - Update query key to include tags: `["global-items", query ?? "", tags ?? []]`.
       - Update query fn URL: `` `/api/global-items${qs ? `?${qs}` : ""}` ``.
       - Keep all other exports (`useGlobalItem`, `useLinkItem`, `useUnlinkItem`) unchanged.
  </action>
  <verify>
    <automated>bun run lint && bun run build</automated>
  </verify>
  <acceptance_criteria>
    - `src/client/stores/uiStore.ts` contains `fabMenuOpen` state field
    - `src/client/stores/uiStore.ts` contains `catalogSearchOpen` state field
    - `src/client/stores/uiStore.ts` contains `catalogSearchMode` state field
    - `src/client/stores/uiStore.ts` contains `openCatalogSearch` action
    - `src/client/stores/uiStore.ts` contains `closeCatalogSearch` action
    - `src/client/hooks/useTags.ts` exists and exports `useTags`
    - `src/client/hooks/useGlobalItems.ts` accepts `tags?: string[]` parameter
    - `bun run lint` passes with no errors
    - `bun run build` succeeds
  </acceptance_criteria>
  <done>UIStore has FAB menu + catalog search state. useTags hook fetches tags. useGlobalItems supports tag filtering. Lint and build pass.</done>
</task>

</tasks>

<verification>
- `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts` -- all green
- `bun run lint` -- no errors
- `bun run build` -- succeeds
- `bun test` -- full suite green (no regressions)
</verification>

<success_criteria>
1. GET /api/tags returns all tags as JSON array
2. GET /api/global-items is registered and reachable (was missing from index.ts)
3. UIStore has fabMenuOpen, catalogSearchOpen, catalogSearchMode state
4. useTags hook works with staleTime caching
5. useGlobalItems accepts optional tags parameter for filtering
6. All existing tests pass (no regressions)
</success_criteria>

<output>
After completion, create `.planning/phases/20-fab-full-screen-catalog-search/20-01-SUMMARY.md`
</output>
