chore: merge executor worktree (04-03)

This commit is contained in:
lorentz 2026-05-03 18:04:35 -04:00
commit 422ea2bd0e
2 changed files with 136 additions and 4 deletions

View file

@ -0,0 +1,110 @@
---
phase: 04
plan: 03
subsystem: mobile-tickets-detail
tags: [mobile, tickets, header, navigation, accessibility]
dependency_graph:
requires: []
provides: [ticket-detail-header-D18]
affects: [app/mobile/tickets/[id]/page.tsx]
tech_stack:
added: []
patterns: [three-slot-header, external-link-with-noopener]
key_files:
modified:
- app/mobile/tickets/[id]/page.tsx
decisions:
- Desktop URL chosen as /analyzer/ticket/{id} (in-app, stays in auth boundary; UI-SPEC permits either Autotask direct URL or analyzer URL)
- rel="noopener noreferrer" added per T-04-11 threat mitigation (reverse tabnabbing defense-in-depth, even on same-origin)
- -mx-4 px-4 technique extends header band to parent div's edges so border-b runs full-width under the new header bar
metrics:
duration_minutes: 5
completed_date: "2026-05-03"
tasks_completed: 1
files_changed: 1
---
# Phase 4 Plan 3: Detail Page In-Page Header Reskin Summary
**One-liner:** Three-slot in-page header on `/mobile/tickets/[id]` — back chevron + breadcrumb + external-link icon — replacing the legacy `Back` button per D-18.
---
## What Changed
### Exact Lines Modified in `app/mobile/tickets/[id]/page.tsx`
**Import line (line 7):** Added `ExternalLink` to the named lucide-react import, alphabetically between `Eye` and `Mail`. All existing imports retained.
**Header bar (lines ~240-264, replacing ~240-242):** The legacy single-line back button:
```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>
```
Was replaced with the D-18 three-slot header bar (25 lines). The replacement lives inside the existing `<div className="px-4 pt-4 pb-3 border-b">` parent — that wrapper was NOT changed. The new inner `div` uses `-mx-4 px-4` to extend to the parent's edges so its `border-b` runs full-width as a visual separator between the new header and the body below.
---
## Desktop URL Choice
`/analyzer/ticket/{id}` was used as the desktop link target.
**Rationale:** The analyzer ticket view (`/analyzer/ticket/{id}`) is the canonical in-app desktop view of a ticket in this codebase. It stays within the auth boundary (same domain, same session cookie), so the user lands on a fully functional desktop page rather than being redirected to an external system. The UI-SPEC permits either the Autotask direct URL or the analyzer URL — the analyzer URL is preferred per the plan's spec note.
---
## D-19 Body Untouched — Confirmation
The following sections were preserved verbatim (not modified):
| Section | Preserved |
|---------|-----------|
| Badges row (`flex items-start gap-2 mb-2` — priority + ticket# + status pills) | Yes |
| `<h1 className="text-base font-bold leading-snug">` ticket title | Yes |
| Metadata icons row (Briefcase / User / Clock) | Yes |
| 3-card stats grid (Notes / Time entries / Hours logged) | Yes |
| Description Collapsible block (`AlignLeft` icon, raw/formatted toggle) | Yes |
| Timeline section (filter toggles, `TimelineCard` list) | Yes |
| `TimelineCard` component definition | Yes |
| `loading` and `error` states | Yes |
| Helper functions: `fmtDate`, `fmtHours`, `renderContent`, `relTime` | Yes (note: `relTime` is not in this file — lives in the list page; all helpers in this file preserved) |
| Constant maps: `STATUS_LABEL`, `PRIORITY_LABEL`, `PRIORITY_COLOR` | Yes |
---
## Parallel Execution
This plan (04-03) ran in parallel with 04-01 and 04-02 in wave 1. There is no shared file conflict — 04-03 only touches `app/mobile/tickets/[id]/page.tsx`, while 04-01 touches `app/api/mobile/tickets/route.ts` and 04-02 touches `app/mobile/tickets/page.tsx`. No merge conflicts expected.
---
## Deviations from Plan
None — plan executed exactly as written.
---
## Known Stubs
None. The header bar is fully wired: `router.back()` for navigation, `ticket.ticket_number` for breadcrumb text, `ticket.id` for the external link href. No placeholder values.
---
## Threat Flags
No new threat surface beyond what was declared in the plan's threat model (T-04-11, T-04-12). T-04-11 mitigation (`rel="noopener noreferrer"`) is implemented.
---
## Self-Check
Checking created file and commit:
- `app/mobile/tickets/[id]/page.tsx` — FOUND
- Commit `b1ff866` — FOUND
- `04-03-SUMMARY.md` — FOUND
## Self-Check: PASSED

View file

@ -4,7 +4,7 @@ import { useEffect, useState, use } from 'react';
import { useRouter } from 'next/navigation';
import {
ArrowLeft, RefreshCw, Clock, FileText, Timer, CheckCircle2,
ChevronDown, ChevronRight, User, Briefcase, AlertCircle, EyeOff, Eye, Mail, AlignLeft, Code2,
ChevronDown, ChevronRight, User, Briefcase, AlertCircle, EyeOff, Eye, ExternalLink, Mail, AlignLeft, Code2,
} from 'lucide-react';
const PRIORITY_LABEL: Record<number, string> = {
@ -237,9 +237,31 @@ export default function TicketTimeline({ params }: { params: Promise<{ id: strin
<div>
{/* Ticket header */}
<div className="px-4 pt-4 pb-3 border-b">
<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>
<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>
<div className="flex items-start gap-2 mb-2">
<span className={`shrink-0 text-xs font-medium px-2 py-0.5 rounded-full border ${priClass}`}>
{PRIORITY_LABEL[ticket.priority] ?? `P${ticket.priority}`}