wulf-pulse/.planning/phases/04-tickets-restyle/04-03-PLAN.md
lorentz 9658640c04 fix(04-01): restore phase 2/3 work lost by worktree soft-reset
The soft reset to 77073ba inadvertently staged deletions of all phase 2
and 3 artifacts. This commit restores them from their source commits so
subsequent task commits build on the complete prior-phase foundation:
- components/mobile/{BottomNav,HeaderBar,KpiCardMobile,MoreDrawer,NeedsAttentionStrip,WorkerStatusRow}
- app/mobile/layout.tsx, dashboard/page.tsx, analyzer/page.tsx
- app/api/mobile/dashboard/route.ts
- All .planning/** files from phases 01-04
- CLAUDE.md, app/layout.tsx, app/styles/brand.css, public/manifest.json
2026-05-03 18:01:14 -04:00

13 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
04 03 execute 1
app/mobile/tickets/[id]/page.tsx
true
TICK-07
truths artifacts key_links
Detail page in-page header shows a back chevron + 'Tickets' label that calls router.back()
Detail page in-page header shows the breadcrumb 'Tickets / #{ticket_number}' centered
Detail page in-page header shows an external-link icon that opens the desktop ticket URL in a new tab
The shell HeaderBar (Wulf mark + Bell + avatar) from app/mobile/layout.tsx still renders above the in-page header
The detail body (priority/status badges, stats grid, description, timeline) is unchanged from the legacy implementation
path provides contains
app/mobile/tickets/[id]/page.tsx Mobile ticket detail page with reskinned in-page header per D-18 ArrowLeft
from to via pattern
app/mobile/tickets/[id]/page.tsx lucide-react ArrowLeft + ExternalLink icons named import ExternalLink
from to via pattern
app/mobile/tickets/[id]/page.tsx /api/mobile/tickets/{id}/timeline endpoint fetch — unchanged from legacy fetch(`/api/mobile/tickets/
Reskin only the in-page header bar at the top of `app/mobile/tickets/[id]/page.tsx` per D-18: replace the current "← Back" button with a three-slot header (back chevron + label, breadcrumb, external link). The detail body — priority badge row, h1 title, stats grid, description block, timeline — stays untouched per D-19.

Purpose: closes TICK-07. The shell HeaderBar already renders above this page from app/mobile/layout.tsx, so the in-page header docks under it consistently with the new shell language.

Output:

  • Modified app/mobile/tickets/[id]/page.tsx with the new three-slot header bar and ExternalLink icon import. Body untouched.

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

@.planning/REQUIREMENTS.md @.planning/phases/04-tickets-restyle/04-CONTEXT.md @.planning/phases/04-tickets-restyle/04-UI-SPEC.md @.planning/phases/02-mobile-shell/02-CONTEXT.md @CLAUDE.md @app/mobile/tickets/[id]/page.tsx Task 1: Reskin the in-page header of /mobile/tickets/[id] with back chevron, breadcrumb, and external-link icon app/mobile/tickets/[id]/page.tsx - app/mobile/tickets/[id]/page.tsx (full 357-line current file — only lines ~239-242 change in the header; everything else is preserved) - .planning/phases/04-tickets-restyle/04-CONTEXT.md decisions D-18 (header structure) and D-19 (body unchanged) - .planning/phases/04-tickets-restyle/04-UI-SPEC.md "Detail Page In-Page Header" section Open `app/mobile/tickets/[id]/page.tsx` and make TWO surgical edits.
**Edit 1: Add `ExternalLink` to the lucide-react imports (line 5-8 region).**

Current import block:
```typescript
import {
  ArrowLeft, RefreshCw, Clock, FileText, Timer, CheckCircle2,
  ChevronDown, ChevronRight, User, Briefcase, AlertCircle, EyeOff, Eye, Mail, AlignLeft, Code2,
} from 'lucide-react';
```

Add `ExternalLink` to the named imports (alphabetical position: after `Eye`, before `Mail`). Final form:
```typescript
import {
  ArrowLeft, RefreshCw, Clock, FileText, Timer, CheckCircle2,
  ChevronDown, ChevronRight, User, Briefcase, AlertCircle, EyeOff, Eye, ExternalLink, Mail, AlignLeft, Code2,
} from 'lucide-react';
```

Do NOT remove any existing import — `ArrowLeft`, `RefreshCw`, etc. all remain in use.

**Edit 2: Replace the legacy back button (currently at lines ~240-242) with the three-slot header bar per D-18 / UI-SPEC §"Detail Page In-Page Header".**

Current code (the section to replace — inside the `{/* Ticket header */}` div, only the FIRST element of that block):
```tsx
<button onClick={() => router.back()} className="flex items-center gap-1 text-sm text-muted-foreground mb-3 hover:text-foreground">
  <ArrowLeft className="w-4 h-4" /> Back
</button>
```

Replace with the new three-slot header. **Important context:** the parent `<div className="px-4 pt-4 pb-3 border-b">` already provides horizontal padding and the bottom border. The new header must NOT double-bracket the border. So the new header bar replaces ONLY the back button — keep the parent div as-is, just substitute its first child:

```tsx
<div className="flex items-center justify-between -mx-4 px-4 py-3 border-b mb-3">
  <button
    type="button"
    onClick={() => router.back()}
    aria-label="Back to Tickets"
    className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
  >
    <ArrowLeft className="w-4 h-4" aria-hidden="true" />
    <span>Tickets</span>
  </button>

  <p className="text-sm font-semibold truncate mx-2 flex-1 text-center">
    Tickets / #{ticket.ticket_number}
  </p>

  <a
    href={`/analyzer/ticket/${ticket.id}`}
    target="_blank"
    rel="noopener noreferrer"
    aria-label="Open ticket on desktop"
    className="text-muted-foreground hover:text-foreground transition-colors shrink-0"
  >
    <ExternalLink className="w-4 h-4" aria-hidden="true" />
  </a>
</div>
```

Notes on the layout:
- `-mx-4 px-4` extends the header band to the parent div's edges and reapplies internal padding so the `border-b` runs full-width visually under the new header.
- The legacy parent div still has its own `border-b` from when it bracketed the entire header (badges + title + stats + description). That outer `border-b` stays — it now sits below the description / stats area, which is the correct visual structure (the new header has its own divider; the outer border still divides the header section from the timeline).
- Breadcrumb uses `text-sm font-semibold truncate` — UI-SPEC §"Typography" `Detail breadcrumb` row.
- Desktop URL: `/analyzer/ticket/{id}` — this is the desktop analyzer ticket view (the canonical desktop ticket URL in this codebase). UI-SPEC permits "Autotask direct URL" as alternative; the analyzer URL is the in-app desktop equivalent and stays inside the auth boundary.
- `aria-hidden="true"` on icons because the surrounding text / aria-label provides the accessible name.

**What NOT to change (D-19 — body untouched):**
- The badges row (`<div className="flex items-start gap-2 mb-2">` with priority + ticket number + status pills) — preserve verbatim.
- The `<h1 className="text-base font-bold leading-snug">{ticket.title}</h1>` — preserve verbatim.
- The metadata icons row (Briefcase / User / Clock with company / assignee / created date) — preserve verbatim.
- The 3-card stats grid (Notes / Time entries / Hours logged) — preserve verbatim.
- The Description Collapsible block + Timeline section — preserve verbatim.
- The `TimelineCard` component definition — preserve verbatim.
- The `loading` and `error` states — preserve verbatim.
- All helper functions (`fmtDate`, `fmtHours`, `renderContent`, `relTime`) — preserve verbatim.
- The `STATUS_LABEL`, `PRIORITY_LABEL`, `PRIORITY_COLOR` constant maps — preserve verbatim.

Do NOT touch `app/mobile/tickets/[id]/timeline/route.ts` or any other file. The plan's `files_modified` is exactly one file.

Anti-patterns (do NOT do):
- Do NOT remove the parent `<div className="px-4 pt-4 pb-3 border-b">` wrapper — the body still expects it.
- Do NOT remove or alter the existing badges, title, stats, description, or timeline — body is out of scope (D-19).
- Do NOT introduce a `<HeaderBar>` element here — that's the shell's job and already renders from `app/mobile/layout.tsx`.
- Do NOT swap the desktop URL to an external Autotask link unless the analyzer URL is unreachable — the UI-SPEC accepts either; analyzer URL is preferred (in-app navigation).
npx tsc --noEmit --pretty 2>&1 | grep -E "app/mobile/tickets/\[id\]/page\.tsx" || echo "OK: detail page typechecks" - `grep -q "ExternalLink" app/mobile/tickets/[id]/page.tsx` (icon imported and used) - `grep -q 'aria-label="Back to Tickets"' app/mobile/tickets/[id]/page.tsx` (D-18 / UI-SPEC accessibility) - `grep -q 'aria-label="Open ticket on desktop"' app/mobile/tickets/[id]/page.tsx` (D-18 external link a11y) - `grep -q "Tickets / #" app/mobile/tickets/[id]/page.tsx` (D-18 breadcrumb literal) - `grep -q "router\.back()" app/mobile/tickets/[id]/page.tsx` (back gesture preserved) - `grep -q "/analyzer/ticket/" app/mobile/tickets/[id]/page.tsx` (desktop URL in href) - `grep -q 'target="_blank"' app/mobile/tickets/[id]/page.tsx` (opens in new tab) - `grep -q 'rel="noopener noreferrer"' app/mobile/tickets/[id]/page.tsx` (security on target=_blank) - `grep -q "TimelineCard" app/mobile/tickets/[id]/page.tsx` (body component preserved — D-19) - `grep -q "function fmtDate" app/mobile/tickets/[id]/page.tsx` (body helper preserved — D-19) - `grep -q "function renderContent" app/mobile/tickets/[id]/page.tsx` (body helper preserved — D-19) - `grep -q "Notes" app/mobile/tickets/[id]/page.tsx && grep -q "Time entries" app/mobile/tickets/[id]/page.tsx && grep -q "Hours logged" app/mobile/tickets/[id]/page.tsx` (stats grid preserved — D-19) - `! grep -qE ">\\s*Back\\s*" app/mobile/tickets/[id]/page.tsx` (legacy "Back" text removed in favor of "Tickets") - `npx tsc --noEmit --pretty 2>&1` reports no errors for `app/mobile/tickets/[id]/page.tsx` The detail page imports `ExternalLink`, renders the three-slot in-page header (back chevron + "Tickets" label, breadcrumb "Tickets / #{ticket_number}", external-link icon to `/analyzer/ticket/{id}`), and leaves the badges row, title, stats grid, description, and timeline byte-identical to the legacy implementation. TypeScript compiles cleanly.

<threat_model>

Trust Boundaries

Boundary Description
in-page link → external desktop URL The new ExternalLink anchor opens /analyzer/ticket/{id} in a new tab

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-04-11 Tampering external link target="_blank" reverse tabnabbing mitigate rel="noopener noreferrer" on the anchor — prevents the opened page from accessing window.opener, even though /analyzer/ticket/{id} is same-origin. Defense in depth.
T-04-12 Information Disclosure desktop URL leaks ticket id in browser tab accept Same id is already in the current page URL; no new exposure.
</threat_model>
1. `npx tsc --noEmit --pretty` passes — no errors for `app/mobile/tickets/[id]/page.tsx`. 2. All Task 1 acceptance-criteria greps return success. 3. Manual smoke (developer terminal): visit `/mobile/tickets/` and confirm: - The shell HeaderBar (Wulf wordmark + Bell + avatar) renders at the very top from `app/mobile/layout.tsx`. - Below it, the new in-page header shows back chevron + "Tickets" on the left, breadcrumb in the center, ExternalLink icon on the right. - Below that, the unchanged badges row → title → stats grid → description → timeline. - Tapping "Tickets" calls `router.back()` and returns to the list. - Tapping the ExternalLink icon opens `/analyzer/ticket/` in a new tab.

<success_criteria>

  • TICK-07: Detail page header reskinned to match the new shell language (back chevron + breadcrumb + external link); body unchanged.
  • D-18 implemented exactly: three slots (back, breadcrumb, external).
  • D-19 honored: badges, title, stats grid, description, timeline byte-identical.
  • TypeScript clean.
  • The shell HeaderBar from app/mobile/layout.tsx continues to render above this in-page header — no double-rendering. </success_criteria>
After completion, create `.planning/phases/04-tickets-restyle/04-03-SUMMARY.md` documenting: - The exact lines changed in `app/mobile/tickets/[id]/page.tsx` (import line + header bar replacement). - The desktop URL chosen (`/analyzer/ticket/{id}`) and the rationale (in-app navigation, stays in auth boundary). - Confirmation that the body (D-19 scope) was not touched — list which sections remained verbatim. - That this plan ran in parallel with 04-02 (no shared file conflict).