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
13 KiB
13 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04 | 03 | execute | 1 |
|
true |
|
|
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.tsxwith the new three-slot header bar andExternalLinkicon 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> |
<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.tsxcontinues to render above this in-page header — no double-rendering. </success_criteria>