docs(04): capture phase context
This commit is contained in:
parent
05a3d44045
commit
28b5845ee8
2 changed files with 269 additions and 0 deletions
142
.planning/phases/04-tickets-restyle/04-CONTEXT.md
Normal file
142
.planning/phases/04-tickets-restyle/04-CONTEXT.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Phase 4: Tickets Restyle - Context
|
||||
|
||||
**Gathered:** 2026-05-03 (auto mode)
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Reskin the mobile Tickets surfaces (`/mobile/tickets` list + `/mobile/tickets/[id]` detail header) to match the new shell. Replace the current sticky search/filter bar with a Collapsible URL-synced filter strip. Replace page-based pagination (`?page=N`, 30/page) with cursor-based infinite scroll (~25/page, IntersectionObserver) plus a "Load more" fallback. Add priority left-edge stripes to list rows. Detail page body stays largely as-is — only its header is reskinned.
|
||||
|
||||
In scope: list page UI + filter strip + URL deep-linking + cursor pagination API change + detail header reskin.
|
||||
|
||||
Out of scope: filter set expansion (only the four spec'd filters), detail body refactor, comment/attach UI changes, search UX overhaul beyond what existing search input provides.
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Filter strip
|
||||
- **D-01:** Use shadcn `Collapsible` component for the filter strip; default state is **collapsed** (only the search input + "Filters" toggle button visible). Reason: TICK-01 spec says default collapsed; matches phone-first density goal.
|
||||
- **D-02:** Expanded panel exposes exactly four controls: status (multi-select chips: Open/In Progress/Waiting), priority (chips: Critical/High/Medium/Low), queue (Select dropdown sourced from existing queues), and an "Assigned to me" toggle. No additional filter capabilities this phase.
|
||||
- **D-03:** Keep the existing search input visible at all times (above the Collapsible toggle), debounced 400ms — match current behavior.
|
||||
- **D-04:** "Clear all" button appears in the expanded panel when ≥1 filter is active.
|
||||
|
||||
### URL sync
|
||||
- **D-05:** Use `useSearchParams()` + `router.replace()` (NOT `router.push()`) to update query string on filter changes. Reason: replace prevents back-button pollution from filter tweaks; matches Next.js App Router convention.
|
||||
- **D-06:** URL param keys: `q` (search), `status` (comma-separated ints), `priority` (comma-separated ints), `queue` (int), `mine` (`1`/absent). On reload, page hydrates filter state from these params — deep link works.
|
||||
- **D-07:** Cursor (`cursor`) is intentionally **not** persisted to URL — fresh visits always start at the top of the list.
|
||||
|
||||
### Pagination — cursor model
|
||||
- **D-08:** Replace `?page=N&limit=30` with `?cursor=<opaque>&limit=25`. Reason: TICK-05 mandates cursor-based ~25/page.
|
||||
- **D-09:** Cursor is a base64-encoded JSON of `{ last_activity_date: ISO string, id: number }`. Reason: `last_activity_date DESC, id DESC` matches the manager's triage mental model (recently-touched bubbles up); tie-breaker on id makes it stable.
|
||||
- **D-10:** API returns `{ tickets: [...], nextCursor: string | null, hasMore: boolean }`. When `nextCursor` is null, list is exhausted.
|
||||
- **D-11:** Page size is 25 (per TICK-05). The `limit` query param is accepted but capped server-side at 25 to prevent abuse.
|
||||
|
||||
### Infinite scroll trigger
|
||||
- **D-12:** Use the browser-native `IntersectionObserver` API (no library). A sentinel `<div ref={sentinelRef} />` lives at the end of the list; when it intersects the viewport with `rootMargin: '200px'`, fetch next page.
|
||||
- **D-13:** Guard against duplicate fetches: the sentinel callback is a no-op if `loadingMore || !hasMore`.
|
||||
- **D-14:** Always render a focusable "Load more" button below the sentinel as the accessibility fallback (TICK-06). Clicking it triggers the same fetch path. Hide only when `!hasMore`.
|
||||
|
||||
### List row presentation
|
||||
- **D-15:** Each row has a 4px-wide left-edge color stripe via `border-l-4` + a priority color class:
|
||||
- 1 (Critical) → `border-red-500`
|
||||
- 2 (High) → `border-orange-400`
|
||||
- 3 (Medium) → `border-amber-400`
|
||||
- 4 (Low) → `border-slate-300`
|
||||
Reason: TICK-03 spec.
|
||||
- **D-16:** Row body shows: ticket number (mono small), title (1-line truncate), company (muted small), age (relative time, muted), assignee (initials avatar or text). Single-tap navigates to `/mobile/tickets/[id]` (TICK-04).
|
||||
- **D-17:** Remove the priority dot from the existing row (replaced by the stripe). Keep `relTime()` helper as-is.
|
||||
|
||||
### Detail page header
|
||||
- **D-18:** Detail page (`/mobile/tickets/[id]`) keeps its current body. Only the in-page header bar at the top is reskinned: replace the current title bar with a row containing a back chevron (`ArrowLeft` icon → `router.back()`), the breadcrumb "Tickets / #{ticket_number}", and an external-link icon that opens the desktop ticket URL. The shell's HeaderBar (Wulf + Bell + avatar) already renders above it from `app/mobile/layout.tsx` — no change there.
|
||||
- **D-19:** No structural changes to the detail body, comments, or attachments — out of scope for this phase.
|
||||
|
||||
### Empty state
|
||||
- **D-20:** When the active filter set returns zero results, render a centered message: "No tickets match your filters" with a "Clear filters" button. When there are no tickets at all (no filters, empty result), render "No tickets to triage right now" with a refresh affordance.
|
||||
|
||||
### Loading & error states
|
||||
- **D-21:** Initial load → skeleton rows (5 placeholder cards). Subsequent infinite scroll → small inline spinner above the Load more button. Error → toast + Load more button shows "Retry".
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact spacing/typography within rows (match existing density)
|
||||
- Whether to memoize row components (only if perf measurement warrants)
|
||||
- Cursor encoding helper location (lib/services or inline in route)
|
||||
- Exact skeleton visual
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Phase spec
|
||||
- `docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §6.2 (Tickets) — spec for filter strip, priority stripes, cursor pagination, detail header reskin
|
||||
- `.planning/REQUIREMENTS.md` (TICK-01 through TICK-07) — locked acceptance criteria
|
||||
|
||||
### Project conventions
|
||||
- `CLAUDE.md` — Pulse stack rules (no SWR/react-query, no ORM, fetch-from-clients pattern), `/mobile/*` route boundary, `port 3100`
|
||||
- `DESIGN.md` — token usage, navigation IA, current cleanup backlog
|
||||
- `ARCHITECTURE.md` — runtime, data flow, workers (no impact this phase)
|
||||
|
||||
### Prior phase context
|
||||
- `.planning/phases/02-mobile-shell/02-CONTEXT.md` — MobileShell + HeaderBar decisions (the detail header docks under this)
|
||||
- `.planning/phases/03-dashboard-restyle/03-01-SUMMARY.md` — pattern for new `/api/mobile/*` shape with TypeScript interface exports (mirror this for tickets endpoint)
|
||||
|
||||
### Existing code (entry points)
|
||||
- `app/mobile/tickets/page.tsx` — current list page (164 lines, page-based)
|
||||
- `app/mobile/tickets/[id]/page.tsx` — current detail page (357 lines, body kept as-is)
|
||||
- `app/api/mobile/tickets/route.ts` — current API route (90 lines, page-based, with `kiosk_settings` company scoping)
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `Collapsible` from shadcn (`components/ui/collapsible.tsx`) — exists in shadcn primitives
|
||||
- `relTime()` helper inline in `app/mobile/tickets/page.tsx:29-37` — keep
|
||||
- `kiosk_settings` company scoping helper `getMobileCompanyFilter()` in `app/api/mobile/tickets/route.ts:3-26` — keep, do not regress
|
||||
- `lucide-react` icons (Search, X, RefreshCw, ChevronRight, Clock, ArrowLeft) — already in deps
|
||||
- shadcn primitives: `Button`, `Input`, `Select`, `Toggle` (or `Switch`), `Skeleton` — all available in `components/ui/`
|
||||
- `IntersectionObserver` — browser-native, no dep needed
|
||||
|
||||
### Established Patterns
|
||||
- Mobile pages are `'use client'` with `useState` + `useEffect` + `fetch('/api/mobile/...')` — NO SWR, NO react-query
|
||||
- API routes call `postgresClient.query()` with parameterized SQL; manual snake_case → camelCase transform
|
||||
- `requireAuth()` from `lib/auth-utils.ts` is the auth gate for all `/api/*` routes
|
||||
- TypeScript interfaces for API response shapes are exported from the route file and imported via `import type` in the page (pattern from Phase 3)
|
||||
|
||||
### Integration Points
|
||||
- `app/mobile/layout.tsx` (Phase 2) renders the new shell — Tickets pages dock inside it; no layout changes needed
|
||||
- BottomNav active-tab detection uses `pathname.startsWith('/mobile/tickets')` — already correct
|
||||
- `kiosk_settings` table for company scoping — existing, already wired into the current ticket route
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Mirror Phase 3's pattern: API route exports `MobileTicketListResponse` and the row interface; page imports the response type via `import type`.
|
||||
- Cursor encoder/decoder should be small (~10 lines) and live inline in the route file unless a second consumer appears.
|
||||
- Status filter should default to "Open + In Progress + Waiting" (i.e. `status != 5`) matching the existing route's hardcoded filter, so the URL with no status param yields the same default the manager expects.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- Bulk actions (mass close/assign) — out of scope; manager use case here is read-and-tap-into-detail
|
||||
- Saved filter presets — not in TICK-* scope; consider for a future phase
|
||||
- Detail page body refactor (comments, time entries) — explicitly out of scope (TICK-07 says "body kept largely as-is")
|
||||
- Server-sent push of new tickets — no streaming this iteration
|
||||
- Search highlight in row — nice-to-have, not in TICK-* scope
|
||||
- Tablet/desktop responsive breakpoints — `/mobile` is phone-only by milestone constraint
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 04-tickets-restyle*
|
||||
*Context gathered: 2026-05-03*
|
||||
127
.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md
Normal file
127
.planning/phases/04-tickets-restyle/04-DISCUSSION-LOG.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Phase 4: Tickets Restyle - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-05-03
|
||||
**Phase:** 04-tickets-restyle
|
||||
**Mode:** auto (--auto --chain)
|
||||
**Areas discussed:** Filter strip, URL sync, Pagination model, Infinite scroll trigger, List row presentation, Detail page header, Empty/loading states
|
||||
|
||||
---
|
||||
|
||||
## Filter strip
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Always-expanded inline filters | All controls visible by default — more taps reachable | |
|
||||
| `Collapsible` from shadcn, default collapsed | TICK-01 spec; phone-first density | ✓ |
|
||||
| Drawer-based filter sheet | Filters open in a Sheet — feels heavier, more taps | |
|
||||
|
||||
**Auto choice rationale:** TICK-01 explicitly says "default collapsed". Spec-locked.
|
||||
|
||||
---
|
||||
|
||||
## Filter set
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Status + priority + queue + assigned-to-me | Spec set | ✓ |
|
||||
| Add company filter | More flexibility; out of TICK-* scope | |
|
||||
| Add date range | Could be useful; out of TICK-* scope | |
|
||||
|
||||
**Auto choice rationale:** TICK-01 enumerates the four filters. Anything more is scope creep.
|
||||
|
||||
---
|
||||
|
||||
## URL sync mechanism
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| `router.push()` on every filter change | Each filter tweak adds a back-stack entry | |
|
||||
| `router.replace()` on every filter change | Filter tweaks don't pollute back stack | ✓ |
|
||||
| Manual `history.replaceState()` | Lower-level; loses Next.js rerouting integration | |
|
||||
|
||||
**Auto choice rationale:** `router.replace()` is the App Router idiom for filter UIs. Back button should return to whatever surface the user came from, not 8 filter mutations ago.
|
||||
|
||||
---
|
||||
|
||||
## Pagination model
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Keep page-based `?page=N&limit=30` | Simpler, current behavior | |
|
||||
| Cursor `?cursor=&limit=25` with last_activity_date+id | Stable under writes; matches triage workflow | ✓ |
|
||||
| Cursor on created_date+id | Stable but doesn't reflect "recent activity" priority | |
|
||||
|
||||
**Auto choice rationale:** TICK-05 mandates cursor-based ~25/page. Choosing `last_activity_date DESC, id DESC` because managers triage by "what just changed" not "what was created".
|
||||
|
||||
---
|
||||
|
||||
## Infinite scroll trigger
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| `react-intersection-observer` library | Hook abstraction, extra dep | |
|
||||
| Browser-native `IntersectionObserver` | No dep, ~20 lines | ✓ |
|
||||
| Scroll-event listener with throttle | Less precise, more re-renders | |
|
||||
|
||||
**Auto choice rationale:** CLAUDE.md prohibits adding new state libraries; native IntersectionObserver is sufficient.
|
||||
|
||||
---
|
||||
|
||||
## "Load more" fallback
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Skip — sentinel handles it | Fails accessibility | |
|
||||
| Always-rendered focusable button below sentinel | TICK-06 mandates accessibility | ✓ |
|
||||
| Show only when keyboard navigation detected | Brittle, not robust | |
|
||||
|
||||
**Auto choice rationale:** TICK-06 requires the fallback button. Always-rendered keeps it focusable and visible to screen readers.
|
||||
|
||||
---
|
||||
|
||||
## List row priority indicator
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Existing dot (bg-color-N) | Smaller, less prominent | |
|
||||
| Left-edge stripe via `border-l-4` | TICK-03 spec | ✓ |
|
||||
| Background tint of whole row | Too heavy on phone | |
|
||||
|
||||
**Auto choice rationale:** TICK-03 explicitly specifies left-edge stripe.
|
||||
|
||||
---
|
||||
|
||||
## Detail page header
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Refactor full detail page | Out of scope per TICK-07 | |
|
||||
| Reskin in-page header only (back chevron + breadcrumb + ext link) | TICK-07 spec | ✓ |
|
||||
|
||||
**Auto choice rationale:** TICK-07 explicitly says "header reskinned... body kept largely as-is".
|
||||
|
||||
---
|
||||
|
||||
## Empty/loading states
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Spinner only on load | Janky, no skeleton | |
|
||||
| Skeleton rows initial + inline spinner subsequent + empty-state messages | Polished, fits design tokens | ✓ |
|
||||
|
||||
**Auto choice rationale:** Standard mobile pattern; existing `Skeleton` shadcn primitive available.
|
||||
|
||||
---
|
||||
|
||||
## Auto-resolved scope creep checks
|
||||
|
||||
- **Bulk actions** — flagged and deferred (read-tap-detail is the manager workflow)
|
||||
- **Saved presets** — deferred to a future phase
|
||||
- **Detail body refactor** — explicitly excluded by TICK-07
|
||||
|
||||
## External research
|
||||
|
||||
None. Decisions were derivable from spec + existing code patterns.
|
||||
Loading…
Add table
Add a link
Reference in a new issue