From 0f90c0e199af51acd216c70665bdf25c32b66d2e Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Mon, 4 May 2026 22:50:19 -0400 Subject: [PATCH 1/4] refactor(sidebar): collapse rail/sections to single-IA, log docs - Sidebar: kill the drifting railGroups + sections dual definition. Single source of truth (workItems / libraryItems / footerItems) rendered in both pinned and rail modes; pin/unpin is a width and label affordance, not an IA switch. Hairline divider replaces section labels. Guides moves to the footer alongside Account. Renames: Home -> Dashboard, History -> Sessions, Insights -> Analytics. - CURRENT-STATE.md: log PR #158 (session impeccable pass + tasklane keyboard flow) under "Recently shipped". - PRODUCT.md: design-context source of truth (users, brand, aesthetic); sibling to DESIGN-SYSTEM.md. - skills-lock.json: lock /impeccable + /documentation-writer skill versions so other sessions reproduce the same tooling state. - Drop stale .impeccable.md. Co-Authored-By: Claude Opus 4.7 --- CURRENT-STATE.md | 1 + .impeccable.md => PRODUCT.md | 0 frontend/src/components/layout/Sidebar.tsx | 135 ++++++++------------- skills-lock.json | 17 +++ 4 files changed, 69 insertions(+), 84 deletions(-) rename .impeccable.md => PRODUCT.md (100%) create mode 100644 skills-lock.json diff --git a/CURRENT-STATE.md b/CURRENT-STATE.md index 31116b9a..c145417a 100644 --- a/CURRENT-STATE.md +++ b/CURRENT-STATE.md @@ -12,6 +12,7 @@ ## Recently shipped (post-0.1.0.0) +- **2026-05-01 — PR #158** Session-screen UX impeccable pass + tasklane keyboard flow. Heuristic score 24/40 → 33/40 across five sub-passes (distill, quieter, layout, typeset, polish). Removed duplicate "Suggested checks" chip strip → TaskLane is the single source of truth; added inline `Next steps · N pending` cue on the latest action-bearing AI bubble; consolidated session header to Resolve + Escalate + ⋯ kebab; centered messages column to match composer; dropped all banned decorations (side stripes, gradient surfaces, backdrop blur, accent borderTop) for a single decoration channel per surface; unified 14 text sizes into a 5-step scale. TaskLane keyboard flow: Enter submits + auto-advances, Shift+Enter newline, Esc cancel, focus jumps to Send after the last task. Banner ↔ script-panel are now linked (collapse hides both, any outcome closes both). WhatWeKnow section is collapsible with `sessionStorage` memory + auto-collapse-at-5-facts. Side fix: ParameterizationPreview no longer over-highlights short parameter values (word-boundary check). Two backlog entries logged in `.ai/TODO.md`: ConcludeSessionModal multi-select and `bg-card-hover` Tailwind drift in CommandPalette. - **2026-05-01 — PR #156** Suggested-fix "Awaiting verification" outcome. Engineers can now park a fix in `applied_pending` (waiting on client power-cycle, AD replication, license sync, etc.) instead of forcing a synchronous worked/didn't/partial verdict. PendingBanner with worked / didn't / update reason / dismiss; nudge "Still checking" records pending with a reason; page-level Resolve auto-patches pending → success before the resolution flow opens; page-level Escalate intercepts pending. Migration `c0f3a4b7e91d` (`pending_reason` column + status CHECK constraint). - **2026-04-30 — PR #155** Escalation Mode wedge. Magic-moment handoff-context screen for senior pickup, live SSE escalation arrivals, post-claim time-to-first-action metric (`GET /analytics/flowpilot/escalations`), atomic role-gated claim with conflict resolution, queue self-exclusion, chat ownership extended to claimed sessions. The wedge for the first paying-customer push. diff --git a/.impeccable.md b/PRODUCT.md similarity index 100% rename from .impeccable.md rename to PRODUCT.md diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 6dc2b113..f5404ab4 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -2,10 +2,10 @@ import { useCallback, useEffect, useRef, useState, type PointerEvent as ReactPoi import { Link, useLocation } from 'react-router-dom' import type { LucideIcon } from 'lucide-react' import { - LayoutGrid, Clock, AlertTriangle, GitBranch, Code2, Wand2, - ListChecks, Download, BarChart3, + LayoutGrid, Clock, AlertTriangle, GitBranch, + ListChecks, BarChart3, Settings, Pin, PinOff, - History, FileText, Network, Ticket, + FileText, Ticket, BookOpen, } from 'lucide-react' import { cn } from '@/lib/utils' import { useUserPreferencesStore } from '@/store/userPreferencesStore' @@ -31,11 +31,6 @@ interface NavEntry { children?: NavSubItem[] } -interface NavSection { - title: string - items: NavEntry[] -} - /* ── Sidebar component ──────────────────────────────── */ export function Sidebar() { @@ -78,36 +73,40 @@ export function Sidebar() { /* ── Navigation data ──────────────────────────────── */ - /* ── Grouped nav: 5 top-level icons (Sentry-style) ── */ + /* Single source-of-truth IA. Same items, same order, in both rail + * and pinned modes. Pin/unpin is a width/label affordance, not an + * IA switch. A hairline divider separates the two groups; no labels. */ - const railGroups: NavEntry[] = [ + const workItems: NavEntry[] = [ { - href: '/', icon: LayoutGrid, label: 'Home', shortLabel: 'Home', + href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash', matchPaths: ['/'], }, - { - href: '/sessions', icon: History, label: 'History', shortLabel: 'History', - badge: stats?.active_count || undefined, - matchPaths: ['/sessions', '/escalations', '/pilot'], - children: [ - { href: '/sessions', label: 'Session History', count: stats?.active_count || undefined }, - { href: '/escalations', label: 'Escalations', count: stats?.escalation_count || undefined }, - ], - }, { href: '/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets', matchPaths: ['/tickets'], }, + { + href: '/sessions', icon: Clock, label: 'Sessions', shortLabel: 'Sessions', + badge: stats?.active_count || undefined, + matchPaths: ['/sessions'], + }, + { + href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal', + badge: stats?.escalation_count || undefined, + matchPaths: ['/escalations'], + }, + ] + + const libraryItems: NavEntry[] = [ { href: '/trees', icon: GitBranch, label: 'Flows', shortLabel: 'Flows', badge: stats?.tree_counts.total || undefined, - matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/review-queue', '/network-diagrams'], + matchPaths: ['/trees', '/flows', '/my-trees', '/step-library', '/network-diagrams'], children: [ - { href: '/trees', label: 'Flow Library', count: stats?.tree_counts.total || undefined }, { href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined }, - { href: '/network-diagrams', label: 'Network Maps' }, { href: '/step-library', label: 'Solutions Library' }, - { href: '/review-queue', label: 'Review Queue' }, + { href: '/network-diagrams', label: 'Network Maps' }, ], }, { @@ -115,60 +114,25 @@ export function Sidebar() { badge: pendingDraftCount || undefined, matchPaths: ['/scripts', '/script-builder'], children: [ - { href: '/scripts', label: 'Script Library', count: pendingDraftCount || undefined }, { href: '/script-builder', label: 'Script Builder' }, ], }, { - href: '/analytics', icon: BarChart3, label: 'Insights', shortLabel: 'Data', + href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review', + matchPaths: ['/review-queue'], + }, + { + href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats', matchPaths: ['/analytics', '/shares'], children: [ - { href: '/analytics', label: 'Analytics' }, { href: '/shares', label: 'Exports' }, ], }, ] - /* Pinned mode still uses the detailed section layout */ - const sections: NavSection[] = [ - { - title: 'RESOLVE', - items: [ - { href: '/', icon: LayoutGrid, label: 'Dashboard', shortLabel: 'Dash' }, - { href: '/sessions', icon: Clock, label: 'Session History', shortLabel: 'History', badge: stats?.active_count || undefined, matchPaths: ['/sessions'] }, - { href: '/tickets', icon: Ticket, label: 'Tickets', shortLabel: 'Tickets', matchPaths: ['/tickets'] }, - { href: '/escalations', icon: AlertTriangle, label: 'Escalations', shortLabel: 'Escal', badge: stats?.escalation_count || undefined }, - ], - }, - { - title: 'KNOWLEDGE', - items: [ - { - href: '/trees', icon: GitBranch, label: 'Flow Library', shortLabel: 'Flows', - badge: stats?.tree_counts.total || undefined, - matchPaths: ['/trees', '/flows', '/my-trees'], - children: [ - { href: '/trees', label: 'Flow Library' }, - { href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined }, - ], - }, - { href: '/network-diagrams', icon: Network, label: 'Network Maps', shortLabel: 'NetMap', matchPaths: ['/network-diagrams'] }, - { href: '/scripts', icon: Code2, label: 'Scripts', shortLabel: 'Scripts' }, - { href: '/script-builder', icon: Wand2, label: 'Script Builder', shortLabel: 'Builder' }, - { href: '/review-queue', icon: ListChecks, label: 'Review Queue', shortLabel: 'Review' }, - ], - }, - { - title: 'INSIGHTS', - items: [ - { href: '/analytics', icon: BarChart3, label: 'Analytics', shortLabel: 'Stats' }, - { href: '/shares', icon: Download, label: 'Exports', shortLabel: 'Export' }, - ], - }, - ] - const footerItems: NavEntry[] = [ - { href: '/account', icon: Settings, label: 'Account', shortLabel: 'Acct' }, + { href: '/guides', icon: BookOpen, label: 'Guides', shortLabel: 'Guides', matchPaths: ['/guides'] }, + { href: '/account', icon: Settings, label: 'Account', shortLabel: 'Acct', matchPaths: ['/account'] }, ] /* ── Active detection ─────────────────────────────── */ @@ -369,9 +333,9 @@ export function Sidebar() { /* ── Find active flyout group for drawer ── */ + const allRailItems = [...workItems, ...libraryItems, ...footerItems] const activeFlyoutGroup = flyoutIndex && !sidebarPinned - ? railGroups.find((_, i) => `rail-${i}` === flyoutIndex) || - footerItems.find((_, i) => `footer-${i}` === flyoutIndex) + ? allRailItems.find(item => item.href === flyoutIndex) || null : null /* ── Main render ──────────────────────────────────── */ @@ -386,23 +350,20 @@ export function Sidebar() { > {/* Pinned sidebar content */}
- {sections.map((section, si) => ( -
- {si > 0 && ( -
- {section.title} -
- )} - {section.items.map((item, ii) => renderPinnedItem(item, `${si}-${ii}`))} -
- ))} + {workItems.map(item => renderPinnedItem(item, item.href))} +
{/* Footer */}
- {footerItems.map((item, i) => renderPinnedItem(item, `footer-${i}`))} + {footerItems.map(item => renderPinnedItem(item, item.href))} +
- -
-
- - {isEditingName ? ( -
- setEditedName(e.target.value)} - className={cn( - 'flex-1 rounded-md border border-border bg-card px-3 py-2', - 'text-foreground placeholder:text-muted-foreground', - 'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20' - )} - autoFocus - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveName() - if (e.key === 'Escape') { - setEditedName(account?.name ?? '') - setIsEditingName(false) - } - }} - /> - - -
- ) : ( -
- {account?.name} - {isAccountOwner && ( - - )} -
- )} -
- -
-
- -
- - {account?.display_code} - - -
-

- Share this code with teammates so they can join your account during registration. -

-
- -
- -
- {ownerMember ? ownerMember.name : user?.account_role === 'owner' ? user.email : 'Owner unavailable'} -
-

- {ownerMember?.email ?? 'The owner manages billing, branding, integrations, and membership changes.'} -

-
-
- -
-
- -

{formatShortDate(account?.created_at)}

-
-
- -

{formatShortDate(account?.updated_at)}

-
-
-
-
- -
-
-
-
- -

Billing & Usage

-
-

- Monitor plan status, renewal timing, and current account limits. -

-
-
- -
+ ) : ( + <> +

{account?.name}

+ {isAccountOwner && ( + + )} + + )} +
+
+ + + {planLabel(plan)} plan + + {sub && ( + <> + · - - {plan.charAt(0).toUpperCase() + plan.slice(1)} Plan + {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} - {sub && ( - - {sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')} - - )} -
+ + )} + {ownerName && ( + <> + · + Owned by {ownerName} + + )} + {memberCount !== undefined && ( + <> + · + + {memberCount} {memberCount === 1 ? 'member' : 'members'} + + + )} + <> + · + Created {formatShortDate(account?.created_at)} + +
+ -
-
-

Renewal date

-

- {sub?.current_period_end ? new Date(sub.current_period_end).toLocaleDateString() : 'Not scheduled'} -

-
-
-

Branding access

-

- {limits?.custom_branding ? 'Included in your plan' : 'Upgrade required'} -

-
-
+ {/* ── Plan & Usage ───────────────────────────────────────────────── */} +
+
+ Plan & usage + + {sub?.current_period_end + ? `Renews ${new Date(sub.current_period_end).toLocaleDateString()}` + : 'No renewal scheduled'} + +
- {limits && usage && ( -
- - - -
- )} - - {plan === 'free' && ( -
- - -
- )} - {plan === 'pro' && ( -
- -
- )} + {limits && usage ? ( +
+ + +
+ ) : ( +

Plan limits unavailable.

+ )} -
-
- -

Access & Security

-
+ {plan !== 'team' && ( +
+ {plan === 'free' && } + +
+ )} +
-
-
-

Authentication

-

- {plan === 'team' ? 'Password auth available, SSO can be enabled.' : 'Password-based authentication is active.'} -

-

- Use profile settings to update your personal details and sign-in information. -

-
+ {/* ── People ─────────────────────────────────────────────────────── */} + {isAccountOwner ? ( +
+ People -
-

Single Sign-On

-

- {plan === 'team' ? 'Enterprise-ready setup available.' : 'Available on higher-tier account setups.'} -

-

- Contact support to configure SAML or OIDC for your organization. -

-
-
+
+ setInviteEmail(e.target.value)} + required + className={cn(inputClass, 'flex-1 min-w-[14rem]')} + /> + + +
- {isAccountOwner && ( -
-
-
-

Need enterprise security controls?

-

- We can help enable SSO and align account security for larger teams. -

+ {(members.length > 0 || pendingInvites.length > 0) && ( +
    + {members.map((member) => ( +
  • +
    +
    + + {member.name} + + {!member.is_active && ( + + Inactive + + )} +
    +
    + {member.email} + {member.last_login && ( + · Last seen {formatShortDate(member.last_login)} + )} +
    - - Contact Us - -
-
- )} -
- - - -
- -
-
-

Settings Areas

-

- Common account management actions, organized the way most SaaS teams expect to find them. + )} +

+ ) : ( +
+ People +

+ Membership and invites are managed by the account owner + {ownerName && ({ownerName})}. Contact your admin to make changes.

- +
+ )} -
- + Settings + +
+ } - title="Profile Settings" - description="Update your name, email, and personal details." - /> - - {isAccountOwner && ( - } - title="Branding" - description="Customize logo, accent color, and company name." - badge={limits?.custom_branding ? 'Included' : 'Plan gated'} - /> - )} - - {isAccountOwner && ( - } - title="Integrations" - description="Connect PSA and other external systems for your team." - /> - )} - - {isAccountOwner && ( - } - title="Chat Retention" - description="Control conversation retention and assistant data lifecycle." - /> - )} - - {isAccountOwner && ( - } - title="Team Categories" - description="Manage shared flow categories for your workspace." - /> - )} - - {isAccountOwner && ( - } - title="Target Lists" - description="Maintain saved server and device lists for the team." - /> - )} - - } - title="Support & Feedback" - description="Report bugs, request features, or share product feedback." + icon={} + title="Profile" + description="Your name, email, and personal preferences" />
+ + {isAccountOwner && ( +
+ } + title="Branding" + description="Logo, accent color, and company name" + status={ + limits?.custom_branding + ? { label: 'Included', tone: 'positive' } + : { label: 'Plan gated', tone: 'warning' } + } + /> + } + title="Integrations" + description="Connect PSA tools and external systems" + /> + } + title="Chat retention" + description="Conversation retention and assistant data lifecycle" + /> + } + title="Team categories" + description="Shared flow categories for your workspace" + /> + } + title="Target lists" + description="Saved server and device lists for the team" + /> +
+ )} + +
+ + + Need help? Send feedback or report a bug + +
+ + + {/* ── Account actions (transfer / delete / leave) ────────────────── */} +
+ {isAccountOwner ? ( + <> +
+
+

Transfer ownership

+

+ Move ownership of this account to another member. +

+
+ +
+
+
+

Delete account

+

+ Permanently delete the account and all data. Cannot be undone. +

+
+ +
+ + ) : ( +
+
+

Leave account

+

+ Leave this account and create a personal one. +

+
+ +
+ )}
{showTransferModal && ( diff --git a/frontend/src/pages/account/ProfileSettingsPage.tsx b/frontend/src/pages/account/ProfileSettingsPage.tsx index 933e37c5..70a8f877 100644 --- a/frontend/src/pages/account/ProfileSettingsPage.tsx +++ b/frontend/src/pages/account/ProfileSettingsPage.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom' import { User as UserIcon, Loader2, AlertCircle, Check } from 'lucide-react' import { authApi } from '@/api/auth' import { useAuthStore } from '@/store/authStore' +import { useUserPreferencesStore } from '@/store/userPreferencesStore' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import type { UserUpdate } from '@/types' @@ -16,6 +17,7 @@ const inputClass = cn( export function ProfileSettingsPage() { const user = useAuthStore((s) => s.user) const fetchUser = useAuthStore((s) => s.fetchUser) + const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore() const [name, setName] = useState(user?.name ?? '') const [email, setEmail] = useState(user?.email ?? '') @@ -120,6 +122,27 @@ export function ProfileSettingsPage() {
+ {/* Default export format — saved on change, not via Save Changes */} +
+ +

Pre-selected when exporting sessions.

+ +
+ {error && (
-- 2.49.1 From 07a3f01184bb484382967161441f78d91607750a Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Tue, 5 May 2026 01:02:44 -0400 Subject: [PATCH 3/4] =?UTF-8?q?fix(qa):=20ISSUE-001=20=E2=80=94=20fall=20b?= =?UTF-8?q?ack=20to=20members.length=20when=20usage.user=5Fcount=20is=20mi?= =?UTF-8?q?ssing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /subscription endpoint returns usage as {tree_count, session_count_this_month} without user_count, so the Seats UsageRow rendered as " / ∞" (blank current value). The TS type declared user_count: number, hiding this API/type drift; the old card-stack design hid it visually because each stat had its own border. The new flat layout surfaced the gap. Owners get a fallback to members.length (already fetched). Non-owners can't fetch members and don't need seat-count info, so the row hides entirely for them. Verified live: owner now sees Seats 2 / ∞. Co-Authored-By: Claude Opus 4.7 --- frontend/src/pages/AccountSettingsPage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/AccountSettingsPage.tsx b/frontend/src/pages/AccountSettingsPage.tsx index b04c502e..439b5bd3 100644 --- a/frontend/src/pages/AccountSettingsPage.tsx +++ b/frontend/src/pages/AccountSettingsPage.tsx @@ -406,7 +406,12 @@ export function AccountSettingsPage() { current={usage.session_count_this_month} max={limits.max_sessions_per_month} /> - + {(() => { + const seatCount = usage.user_count ?? (isAccountOwner ? members.length : null) + return seatCount !== null ? ( + + ) : null + })()}
) : (

Plan limits unavailable.

-- 2.49.1 From b544a7a462307db79f892c9c48b5562e97c164b9 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Wed, 6 May 2026 18:54:53 -0400 Subject: [PATCH 4/4] test(e2e): update account page heading assertion to match redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8612042 dropped the static "Account Management" heading in favor of the account name (rendered as a dynamic h1). Switch the smoke test to the "Settings" SectionLabel — a stable h2 that survives the redesign. Co-Authored-By: Claude Opus 4.7 --- frontend/e2e/navigation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/e2e/navigation.spec.ts b/frontend/e2e/navigation.spec.ts index c2858ee8..a68f177d 100644 --- a/frontend/e2e/navigation.spec.ts +++ b/frontend/e2e/navigation.spec.ts @@ -30,7 +30,7 @@ test.describe('authenticated navigation smoke tests', () => { await page.goto('/account') await expect( - page.getByRole('heading', { name: 'Account Management' }), + page.getByRole('heading', { name: 'Settings' }), ).toBeVisible() }) }) -- 2.49.1