From 8bd395a0c7bfcba584dd003948ec07992fcf8be5 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Mon, 6 Apr 2026 16:53:48 +0000
Subject: [PATCH 1/3] fix: resolve task lane stale state, partial submit, and
closure bugs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Import and call clearTaskState before updating questions/actions in
handleSend and handleTaskSubmit so new AI tasks always replace stale
sessionStorage cache instead of being overridden by it
- Include pending (not yet completed) tasks in the AI message on partial
submit so the AI knows which tasks were left unanswered
- Fix stale closure in TaskLane saveTaskLane useEffect — use refs for
questions/actions so the debounced backend save always uses current values
- Add responses field to pending_task_lane TypeScript type, removing the
unsafe double-cast in selectChat
- Instruct the AI to re-surface incomplete tasks unless ≥75% confident
the information is no longer needed
Co-Authored-By: Claude Sonnet 4.6
---
backend/app/services/assistant_chat_service.py | 5 +++++
frontend/src/components/assistant/TaskLane.tsx | 14 +++++++++++---
frontend/src/pages/AssistantChatPage.tsx | 11 ++++++++---
frontend/src/types/ai-session.ts | 2 +-
4 files changed, 25 insertions(+), 7 deletions(-)
diff --git a/backend/app/services/assistant_chat_service.py b/backend/app/services/assistant_chat_service.py
index 49c44ffe..6e17b8e3 100644
--- a/backend/app/services/assistant_chat_service.py
+++ b/backend/app/services/assistant_chat_service.py
@@ -77,6 +77,9 @@ scope narrows it to this endpoint.
- JSON array of objects with `text` (required) and `context` (optional, 1 sentence)
- 1-3 questions per response
- Do NOT ask questions inline in your prose. ALL questions go in the marker.
+- If the engineer's message contains tasks marked `_(not yet completed)_`, re-include \
+those as questions/actions in your next response UNLESS you are ≥75% confident the \
+information is no longer needed to resolve the issue. Default to keeping them.
**[ACTIONS] marker format:**
- JSON array of objects with `label` (required), `command` (optional), `description` (required)
@@ -155,6 +158,8 @@ To create a fork, append this marker AFTER your [QUESTIONS]/[ACTIONS] markers:
Every single response MUST contain [QUESTIONS] and/or [ACTIONS] markers with valid JSON. \
No exceptions. Not even when forking. A response without at least one of these markers \
will crash the UI. If you are unsure, include both. The markers are REQUIRED output, not optional.
+If any tasks in the engineer's message are marked `_(not yet completed)_`, re-include them \
+in your markers unless you are ≥75% confident that information is no longer relevant.
"""
diff --git a/frontend/src/components/assistant/TaskLane.tsx b/frontend/src/components/assistant/TaskLane.tsx
index 590f34b3..21bc91db 100644
--- a/frontend/src/components/assistant/TaskLane.tsx
+++ b/frontend/src/components/assistant/TaskLane.tsx
@@ -130,6 +130,14 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
}
}, [handleMouseMove, handleMouseUp])
+ // Refs so the debounced save always uses the latest questions/actions/tasks
+ const questionsRef = useRef(questions)
+ const actionsRef = useRef(actions)
+ const tasksRef = useRef(tasks)
+ useEffect(() => { questionsRef.current = questions }, [questions])
+ useEffect(() => { actionsRef.current = actions }, [actions])
+ useEffect(() => { tasksRef.current = tasks }, [tasks])
+
// Save task state to sessionStorage on every change + debounce to backend
const saveTimerRef = useRef | null>(null)
useEffect(() => {
@@ -139,9 +147,9 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
saveTimerRef.current = setTimeout(() => {
aiSessionsApi.saveTaskLane(sessionId, {
- questions: questions.map(q => ({ text: q.text, context: q.context })),
- actions: actions.map(a => ({ label: a.label, command: a.command, description: a.description })),
- responses: tasks as unknown as Array>,
+ questions: questionsRef.current.map(q => ({ text: q.text, context: q.context })),
+ actions: actionsRef.current.map(a => ({ label: a.label, command: a.command, description: a.description })),
+ responses: tasksRef.current as unknown as Array>,
}).catch(() => { /* silent — best-effort save */ })
}, 2000)
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index 9b6169cb..c1ad8308 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -12,7 +12,7 @@ import { analytics } from '@/lib/analytics'
import { toast } from '@/lib/toast'
import { ChatSidebar, ChatSidebarCollapsedBar } from '@/components/assistant/ChatSidebar'
import { ChatMessage } from '@/components/assistant/ChatMessage'
-import { TaskLane } from '@/components/assistant/TaskLane'
+import { TaskLane, clearTaskState } from '@/components/assistant/TaskLane'
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
import { StatusUpdateModal } from '@/components/flowpilot/StatusUpdateModal'
import type { ChatListItem, ConclusionOutcome } from '@/types/assistant-chat'
@@ -242,7 +242,7 @@ export default function AssistantChatPage() {
if (q.length > 0 || a.length > 0) {
// Pre-load user's saved responses into sessionStorage BEFORE setting props
// so TaskLane can restore them on mount/prop-change
- const responses = (detail.pending_task_lane as Record).responses as unknown[] | undefined
+ const responses = detail.pending_task_lane.responses
if (responses && responses.length > 0) {
try {
sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
@@ -340,6 +340,7 @@ export default function AssistantChatPage() {
const hasQuestions = response.questions && response.questions.length > 0
const hasActions = response.actions && response.actions.length > 0
if (hasQuestions || hasActions) {
+ if (activeChatId) clearTaskState(activeChatId)
setActiveQuestions(response.questions || [])
setActiveActions(response.actions || [])
setShowTaskLane(true)
@@ -358,7 +359,8 @@ export default function AssistantChatPage() {
const handleTaskSubmit = async (responses: Array<{ type: string; state: string; value: string; text?: string; label?: string }>) => {
if (!activeChatId || loading) return
- // Format task responses into a structured message for the AI
+ // Format task responses into a structured message for the AI.
+ // Pending tasks are included so the AI knows they weren't completed yet.
const parts: string[] = []
for (const r of responses) {
const name = r.type === 'question' ? `Q: ${r.text}` : r.label || 'Check'
@@ -366,6 +368,8 @@ export default function AssistantChatPage() {
parts.push(`**${name}:**\n\`\`\`\n${r.value.trim()}\n\`\`\``)
} else if (r.state === 'skipped') {
parts.push(`**${name}:** _(skipped)_`)
+ } else {
+ parts.push(`**${name}:** _(not yet completed)_`)
}
}
const userMessage = parts.join('\n\n')
@@ -385,6 +389,7 @@ export default function AssistantChatPage() {
// Update task lane based on AI response
const hasQuestions = response.questions && response.questions.length > 0
const hasActions = response.actions && response.actions.length > 0
+ if (activeChatId) clearTaskState(activeChatId)
if (hasQuestions || hasActions) {
setActiveQuestions(response.questions || [])
setActiveActions(response.actions || [])
diff --git a/frontend/src/types/ai-session.ts b/frontend/src/types/ai-session.ts
index dd2ae7b4..c8f90886 100644
--- a/frontend/src/types/ai-session.ts
+++ b/frontend/src/types/ai-session.ts
@@ -196,7 +196,7 @@ export interface AISessionDetail extends AISessionSummary {
ticket_data: Record | null
steps: AISessionStepResponse[]
conversation_messages: Array<{ role: string; content: string }>
- pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[] } | null
+ pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[]; responses?: Array> } | null
is_branching: boolean
active_branch_id: string | null
}
From 990f04489f5b32bc22ee1de940386d6fe4d0d844 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Mon, 6 Apr 2026 20:17:39 +0000
Subject: [PATCH 2/3] fix: prevent TaskLane showing stale data when starting
new chat
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three race conditions in AssistantChatPage:
1. handleNewChat cleared showTaskLane/activeQuestions/activeActions
AFTER the createChatSession await — old lane was visible during
the network call. Moved clears before the await.
2. handleResumeNew never cleared old TaskLane state at all. Added
upfront clears before the first await.
3. handleSend and handleTaskSubmit had no stale-session guard. If
the user switched chats while sendChatMessage was in flight, the
response would set showTaskLane on the wrong session. Added
sentForChatId snapshot + currentChatRef guard (same pattern
already used in selectChat).
Co-Authored-By: Claude Sonnet 4.6
---
frontend/src/pages/AssistantChatPage.tsx | 34 +++++++++++++++---------
1 file changed, 22 insertions(+), 12 deletions(-)
diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx
index c1ad8308..1f675727 100644
--- a/frontend/src/pages/AssistantChatPage.tsx
+++ b/frontend/src/pages/AssistantChatPage.tsx
@@ -259,6 +259,11 @@ export default function AssistantChatPage() {
}, [])
const handleNewChat = async () => {
+ // Clear stale state immediately — don't wait for API to return
+ setShowTaskLane(false)
+ setActiveQuestions([])
+ setActiveActions([])
+ setMessages([])
try {
const session = await aiSessionsApi.createChatSession({
intake_type: 'free_text',
@@ -275,11 +280,6 @@ export default function AssistantChatPage() {
currentChatRef.current = session.session_id
setChats(prev => [chatItem, ...prev])
setActiveChatId(session.session_id)
- setMessages([])
- // Clear TaskLane from previous session
- setShowTaskLane(false)
- setActiveQuestions([])
- setActiveActions([])
} catch {
toast.error('Failed to create chat')
}
@@ -315,11 +315,14 @@ export default function AssistantChatPage() {
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
setLoading(true)
+ const sentForChatId = activeChatId
try {
const response = await aiSessionsApi.sendChatMessage(activeChatId, {
message: userMessage,
upload_ids: completedUploadIds.length > 0 ? completedUploadIds : undefined,
})
+ // Guard: discard if user switched to a different chat while this was in flight
+ if (currentChatRef.current !== sentForChatId) return
analytics.aiFeatureUsed({ feature: 'assistant_chat' })
setMessages(prev => [
...prev,
@@ -327,20 +330,20 @@ export default function AssistantChatPage() {
])
setChats(prev =>
prev.map(c =>
- c.id === activeChatId
+ c.id === sentForChatId
? { ...c, message_count: c.message_count + 2, title: c.message_count === 0 ? userMessage.slice(0, 100) : c.title, updated_at: new Date().toISOString() }
: c
)
)
// Load branches if fork was created
- if (response.fork && activeChatId) {
- branching.loadBranches(activeChatId)
+ if (response.fork && sentForChatId) {
+ branching.loadBranches(sentForChatId)
}
// Show task lane if AI sent questions or actions
const hasQuestions = response.questions && response.questions.length > 0
const hasActions = response.actions && response.actions.length > 0
if (hasQuestions || hasActions) {
- if (activeChatId) clearTaskState(activeChatId)
+ clearTaskState(sentForChatId)
setActiveQuestions(response.questions || [])
setActiveActions(response.actions || [])
setShowTaskLane(true)
@@ -377,19 +380,22 @@ export default function AssistantChatPage() {
setMessages(prev => [...prev, { role: 'user', content: userMessage }])
setLoading(true)
+ const sentForChatId = activeChatId
try {
const response = await aiSessionsApi.sendChatMessage(activeChatId, { message: userMessage })
+ // Guard: discard if user switched to a different chat while this was in flight
+ if (currentChatRef.current !== sentForChatId) return
setMessages(prev => [
...prev,
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows, fork: response.fork, actions: response.actions, questions: response.questions },
])
- if (response.fork && activeChatId) {
- branching.loadBranches(activeChatId)
+ if (response.fork && sentForChatId) {
+ branching.loadBranches(sentForChatId)
}
// Update task lane based on AI response
const hasQuestions = response.questions && response.questions.length > 0
const hasActions = response.actions && response.actions.length > 0
- if (activeChatId) clearTaskState(activeChatId)
+ clearTaskState(sentForChatId)
if (hasQuestions || hasActions) {
setActiveQuestions(response.questions || [])
setActiveActions(response.actions || [])
@@ -430,6 +436,10 @@ export default function AssistantChatPage() {
}
const handleResumeNew = async (summary: string) => {
+ // Clear stale state immediately — don't wait for API to return
+ setShowTaskLane(false)
+ setActiveQuestions([])
+ setActiveActions([])
try {
const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.`
const session = await aiSessionsApi.createChatSession({
From 1811889ed92b123d05cce6885473768bf60b0734 Mon Sep 17 00:00:00 2001
From: chihlasm
Date: Mon, 6 Apr 2026 20:17:47 +0000
Subject: [PATCH 3/3] chore: update docs and redesign landing page hero
- CLAUDE.md: correct Docker container names, update migration format
docs (hash IDs now default), fix Node path in Lesson 63, update
design system values to electric blue accent, add retracted lessons
note, add GitNexus section
- .gitignore: add .gitnexus
- Landing page: replace animated chat preview with ticket-comparison
hero layout; remove backdrop-filter from scrolled nav (aligns with
design system); clean up removed chat animation CSS
Co-Authored-By: Claude Sonnet 4.6
---
.gitignore | 1 +
CLAUDE.md | 156 ++++++++++---
frontend/src/pages/LandingPage.tsx | 51 -----
frontend/src/styles/landing.css | 346 ++++++++++++++++++-----------
4 files changed, 343 insertions(+), 211 deletions(-)
diff --git a/.gitignore b/.gitignore
index e6cfa118..758ce880 100644
--- a/.gitignore
+++ b/.gitignore
@@ -233,3 +233,4 @@ package.json
package-lock.json
.worktrees/
.gstack/
+.gitnexus
diff --git a/CLAUDE.md b/CLAUDE.md
index 3ebb8970..4ce6373a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,6 +1,6 @@
# CLAUDE.md - Patherly / ResolutionFlow Project Context
-> **Last Updated:** March 27, 2026
+> **Last Updated:** April 6, 2026
---
@@ -16,7 +16,8 @@
| Context | Name Used |
|---------|-----------|
-| Repository / directory / database / Docker | `patherly` / `patherly_postgres` |
+| Repository / directory / database | `patherly` (internal name) |
+| Docker containers | `resolutionflow_postgres`, `resolutionflow_frontend`, `resolutionflow_backend` |
| Backend, frontend UI, production URLs | **ResolutionFlow** |
- **Design system:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — THE source of truth for all design decisions
@@ -44,7 +45,7 @@
- **Phase:** Go-to-Market Validation (Pre-PMF)
- **Backend:** Complete (55+ API endpoints, 100+ integration tests)
- **Frontend:** Core features complete, Tree Editor functional
-- **Database:** PostgreSQL with Docker, 98 migrations
+- **Database:** PostgreSQL with Docker, 101 migrations
- **Detailed status:** [CURRENT-STATE.md](CURRENT-STATE.md)
### What's In Progress
@@ -96,7 +97,7 @@ patherly/
│ │ ├── services/knowledge_flywheel.py # AI session analysis → flow proposals
│ │ ├── services/knowledge_flywheel_scheduler.py # APScheduler job for batch analysis
│ │ └── services/knowledge_gap_service.py # Weak options & escalation signal detection
-│ ├── alembic/ # Database migrations (001-029+)
+│ ├── alembic/ # Database migrations (001-070 sequential, then hash IDs)
│ ├── scripts/ # seed_data.py, seed_trees.py
│ └── tests/ # pytest integration tests
├── frontend/
@@ -188,8 +189,8 @@ Official ConnectWise developer guides live in `docs/connectwise/best-practices/`
## Development Commands
```powershell
-# Start PostgreSQL
-docker start patherly_postgres
+# Start PostgreSQL (run from VPS SSH — docker not available inside code-server, see Lesson 103)
+docker start resolutionflow_postgres
# Backend (from backend/)
source venv/bin/activate # Linux/Mac
@@ -203,21 +204,19 @@ npm run dev
pytest --override-ini="addopts="
# First time only: create test database
-docker exec -it patherly_postgres psql -U postgres -c "CREATE DATABASE patherly_test;"
+docker exec -it resolutionflow_postgres psql -U postgres -c "CREATE DATABASE resolutionflow_test;"
# Frontend build (IMPORTANT: stricter than tsc --noEmit — always use as final check)
cd frontend && npm run build
# Database migrations
cd backend && alembic upgrade head
-alembic revision --autogenerate -m "Description" --rev-id=NNN # NNN = next sequential number
-# IMPORTANT: Migrations use sequential 3-digit IDs (001, 002, ..., 068, 069).
-# Check the latest: ls backend/alembic/versions/ | grep -E '^\d{3}_' | sort | tail -1
-# The revision ID and filename prefix MUST match (e.g., revision="068", file=068_description.py).
-# down_revision MUST point to the previous sequential number. Never use hex hash IDs for new migrations.
+alembic revision --autogenerate -m "Description"
+# Sequential 3-digit IDs (001–070) were used historically. New migrations use Alembic's default hex hash IDs.
+# Do NOT pass --rev-id — let Alembic generate the hash automatically.
-# Access PostgreSQL
-docker exec -it patherly_postgres psql -U postgres -d patherly
+# Access PostgreSQL (run from VPS SSH — docker not available inside code-server, see Lesson 103)
+docker exec -it resolutionflow_postgres psql -U postgres -d resolutionflow
# Seed data
cd backend && pip install httpx && python -m scripts.seed_trees
@@ -292,7 +291,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
**62. Playwright strict mode — scope selectors to avoid ambiguity:** Step titles appear in both the sidebar checklist and main content heading. Use `getByRole('heading', { name })` for the main content, or scope with `page.locator('.animate-scale-in')` for command palette items. `getByText()` frequently matches multiple elements due to the sidebar + main content layout.
-**63. Node 20 required for frontend builds:** Vite 7+ requires Node 20.19+. The system Node may be v18; use nvm: `export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm use 20`. For direct binary access without nvm sourcing: `PATH="/home/michaelchihlas/.nvm/versions/node/v20.19.0/bin:$PATH"`.
+**63. Node 20 required for frontend builds:** Vite 7+ requires Node 20.19+. The system Node may be v18; use nvm: `export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm use 20`. For direct binary access without nvm sourcing: `PATH="$HOME/.nvm/versions/node/v20.19.0/bin:$PATH"`.
**64. PostHog product analytics:** Initialized via `PostHogProvider` in `main.tsx` with explicit `posthog.init()` + `client` prop pattern. Event helpers in `lib/analytics.ts` — use `analytics.eventName(props)` to track. `identifyUser()` called in `authStore.fetchUser()`, `resetAnalytics()` on logout. Env vars: `VITE_PUBLIC_POSTHOG_KEY`, `VITE_PUBLIC_POSTHOG_HOST`. Autocapture enabled.
@@ -332,7 +331,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
**82. `bun` requires PATH setup on devserver01:** `export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$PATH"`. The gstack browse binary and Playwright need this. Chromium system deps: `libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2`.
-**83. FlowPilot ActionBar is `position: fixed; bottom: 0`:** Any UI element placed in normal document flow below the session content will be hidden behind it. New fixed-position elements (like the message bar) must use `bottom: 68px` (action bar height) and the same `left: var(--sidebar-w)` pattern. The conversation column uses `pb-32` for clearance.
+**83. ~~FlowPilot ActionBar fixed bottom~~ (Superseded by Lesson 93):** Actions moved to the page header. `FlowPilotActionBar` component exists but is no longer used in the main session flow. The only fixed-bottom element is the message input.
**84. AI session `abandoned` status is fully wired:** `POST /ai-sessions/{id}/abandon` sets status to `abandoned` with optional `reason` param. Frontend: `aiSessionsApi.abandonSession()`, `useFlowPilotSession().abandonSession()`, "Close" button in `FlowPilotActionBar`. Redirects to `/sessions` after closing.
@@ -344,6 +343,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
**88. Charcoal palette — sidebar-darkest approach:** Sidebar `#0e1016`, page `#16181f`, cards `#1e2028`, borders `#2a2e3a`. This gives more contrast range than true-dark. All colors via CSS variables in `index.css` `@theme` block. Accent is electric blue (#60a5fa), not orange or cyan.
+*(Lessons 89–91 were retracted.)*
**92. `tsc -b` in Dockerfile is stricter than `npx tsc --noEmit`:** The production build (`tsc -b && vite build`) enforces `noUnusedLocals` and `noUnusedParameters` as hard errors. After any refactor that moves logic between components or removes features, trace every import and destructured prop to remove orphans. IDE warnings (yellow squiggles) flag these — check them before pushing.
@@ -353,7 +353,7 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
**95. Image upload → AI vision pipeline:** Paste/attach images → upload to Railway S3 bucket via `uploadsApi.upload()` → send `upload_ids` with chat message → backend fetches from S3 via `storage_service.download_file()` → resized via `storage_service.resize_image_for_vision()` (Pillow, 1568px max, PNG→JPEG) → base64-encoded → sent as Claude multimodal content blocks. Max 3 images/message. Images are NOT stored in conversation history (text-only). Vision helpers live in `storage_service.py`.
-**96. `bg-accent` is ember orange — never use for code/kbd elements:** In Tailwind v4, `bg-accent` maps to `--color-accent: #f97316`. Use `bg-code` for code blocks, `bg-white/[0.12] border border-white/[0.06]` for inline code/badges, `bg-white/[0.08]` for kbd shortcuts. Orange is reserved for interactive elements only (buttons, active nav, links).
+**96. `bg-accent` is electric blue — never use for code/kbd elements:** In Tailwind v4, `bg-accent` maps to `--color-accent: #60a5fa` (dark) / `#2563eb` (light). Use `bg-code` for code blocks, `bg-white/[0.12] border border-white/[0.06]` for inline code/badges, `bg-white/[0.08]` for kbd shortcuts. Blue accent is reserved for interactive elements only (buttons, active nav, links). Ember orange (#f97316) is deprecated — do not use.
**97. Railway Object Storage (S3 bucket) is provisioned:** Bucket `resolutionflow-uploads` on Railway canvas. Variables: `STORAGE_ENDPOINT`, `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY`, `STORAGE_BUCKET_NAME`, `STORAGE_REGION` — mapped via variable references on the `patherly` backend service. Accessed via boto3 in `storage_service.py`. Pillow (`Pillow>=10.0.0`) + `libjpeg-dev`/`zlib1g-dev` in Dockerfile for image resize.
@@ -390,16 +390,16 @@ gh run view --json jobs --jq '.jobs[] | {name: .name, conclusion: .conclusi
**Source of truth:** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) — always read this before making visual or UI decisions.
-- **Theme:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. Light mode planned.
-- **Backgrounds:** `bg-page` (`#1a1c23`), `bg-sidebar` (`#10121a`), `bg-card` (`#22252e`), `bg-elevated` (`#2e3140`)
-- **Cards:** `bg-card` with 1px `border-default` (`#2e3240`), 8px radius. No shadows, no blur, no gradients. Hover: `border-hover` (`#3d4252`)
-- **Buttons:** Primary: solid `accent` (#f97316), white text, 5px radius. Ghost: transparent + 1px border, hover `bg-elevated`
-- **Inputs:** `bg-input` (`#282b35`) with 1px `border-default`, 5px radius. Focus: `border-color: accent` + `box-shadow: 0 0 0 2px accent-dim`
-- **Text:** `text-heading` (`#f0f2f5`) → `text-primary` (`#e2e5eb`) → `text-muted-foreground` (`#848b9b`) → `text-muted` (`#4f5666`). NEVER use `text-secondary` — in Tailwind v4 it maps to a surface color (#2e3140), not a text color.
-- **Borders:** `border-default` (`#2e3240`), `border-hover` (`#3d4252`)
-- **Functional colors:** `#34d399` (success), `#eab308` (warning), `#f87171` (danger) — each with `-dim` variant at 10% opacity
-- **Accent:** Ember orange `#f97316` — used sparingly (≤5% of UI). `accent-dim` = `rgba(249,115,22,0.10)`, `accent-text` = `#fdba74`
-- **Deprecated:** Do NOT use `glass-card`, `glass-stat`, `bg-gradient-brand`, `text-gradient-brand`, `backdrop-filter: blur()`, ambient orbs, purple gradients, or cyan accent (`#22d3ee`)
+- **Theme:** Flat, high-contrast dark theme (Sentry/PostHog-inspired). No glass morphism, no backdrop blur, no ambient orbs, no gradient backgrounds on surfaces. Light mode fully specified (v6).
+- **Backgrounds:** `bg-page` (`#16181f`), `bg-sidebar` (`#0e1016`), `bg-card` (`#1e2028`), `bg-elevated` (`#2a2d38`)
+- **Cards:** `bg-card` with 1px `border-default` (`#2a2e3a`), 8px radius. No shadows, no blur, no gradients. Hover: `border-hover` (`#3d4252`)
+- **Buttons:** Primary: solid `accent` (#60a5fa dark / #2563eb light), white text, 5px radius. Ghost: transparent + 1px border, hover `bg-elevated`
+- **Inputs:** `bg-input` (`#252830`) with 1px `border-default`, 5px radius. Focus: `border-color: accent` + `box-shadow: 0 0 0 2px accent-dim`
+- **Text:** `text-heading` (`#f0f2f5`) → `text-primary` (`#e2e5eb`) → `text-muted-foreground` (`#848b9b`) → `text-muted` (`#4f5666`). NEVER use `text-secondary` — in Tailwind v4 it maps to a surface color, not a text color.
+- **Borders:** `border-default` (`#2a2e3a`), `border-hover` (`#3d4252`)
+- **Functional colors:** `#34d399` (success), `#fbbf24` (warning/amber), `#f87171` (danger), `#67e8f9` (info/cyan) — each with `-dim` variant at 10% opacity
+- **Accent:** Electric blue `#60a5fa` (dark) / `#2563eb` (light) — used sparingly (≤5% of UI). `accent-dim` = `rgba(96,165,250,0.10)`, `accent-text` = `#93c5fd`
+- **Deprecated:** Do NOT use `glass-card`, `glass-stat`, `bg-gradient-brand`, `text-gradient-brand`, `backdrop-filter: blur()`, ambient orbs, purple gradients, ember orange (`#f97316`), or cyan (`#22d3ee`) as accent — cyan is now the info color only
---
@@ -518,3 +518,105 @@ When a feature, fix, or significant piece of work is finished and merged/committ
| Bugs & Fixes | CLAUDE.md → Critical Lessons Learned section |
| Design System | [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) |
| Dev Environment | [DEV-ENV.md](DEV-ENV.md) — 46.202.92.250 setup, Docker, CORS, networking |
+
+
+# GitNexus — Code Intelligence
+
+This project is indexed by GitNexus as **resolutionflow** (14787 symbols, 31366 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
+
+> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
+
+## Always Do
+
+- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
+- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
+- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
+- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
+- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.
+
+## When Debugging
+
+1. `gitnexus_query({query: ""})` — find execution flows related to the issue
+2. `gitnexus_context({name: ""})` — see all callers, callees, and process participation
+3. `READ gitnexus://repo/resolutionflow/process/{processName}` — trace the full execution flow step by step
+4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed
+
+## When Refactoring
+
+- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
+- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
+- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.
+
+## Never Do
+
+- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
+- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
+- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
+- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.
+
+## Tools Quick Reference
+
+| Tool | When to use | Command |
+|------|-------------|---------|
+| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
+| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
+| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
+| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
+| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
+| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |
+
+## Impact Risk Levels
+
+| Depth | Meaning | Action |
+|-------|---------|--------|
+| d=1 | WILL BREAK — direct callers/importers | MUST update these |
+| d=2 | LIKELY AFFECTED — indirect deps | Should test |
+| d=3 | MAY NEED TESTING — transitive | Test if critical path |
+
+## Resources
+
+| Resource | Use for |
+|----------|---------|
+| `gitnexus://repo/resolutionflow/context` | Codebase overview, check index freshness |
+| `gitnexus://repo/resolutionflow/clusters` | All functional areas |
+| `gitnexus://repo/resolutionflow/processes` | All execution flows |
+| `gitnexus://repo/resolutionflow/process/{name}` | Step-by-step execution trace |
+
+## Self-Check Before Finishing
+
+Before completing any code modification task, verify:
+1. `gitnexus_impact` was run for all modified symbols
+2. No HIGH/CRITICAL risk warnings were ignored
+3. `gitnexus_detect_changes()` confirms changes match expected scope
+4. All d=1 (WILL BREAK) dependents were updated
+
+## Keeping the Index Fresh
+
+After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
+
+```bash
+npx gitnexus analyze
+```
+
+If the index previously included embeddings, preserve them by adding `--embeddings`:
+
+```bash
+npx gitnexus analyze --embeddings
+```
+
+To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
+
+> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
+
+## CLI
+
+| Task | Read this skill file |
+|------|---------------------|
+| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
+| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
+| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
+| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
+| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
+| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
+
+
diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx
index ba49969e..aaff9d10 100644
--- a/frontend/src/pages/LandingPage.tsx
+++ b/frontend/src/pages/LandingPage.tsx
@@ -180,57 +180,6 @@ export default function LandingPage() {
Built by a 15-year MSP veteran who got tired of empty ticket notes.
-
-
-
-
-
- 🔒
- app.resolutionflow.com/pilot
-
-
-
-
-
-
- You
- User can't access shared drive after password reset
-
-
-
-
-
- FlowPilot is thinking…
-
-
-
-
- FlowPilot
- Likely a cached credential issue. Let's check:
-
-
-
-
- FlowPilot
- 1. Run klist purge to clear Kerberos tickets
-
-
-
-
- FlowPilot
- 2. Credential Manager → remove saved share entries
-
-
-
-
- Auto-doc
- 3 steps captured ✓
-
-
-
-
-
-
diff --git a/frontend/src/styles/landing.css b/frontend/src/styles/landing.css
index 56f36fc1..313b4d95 100644
--- a/frontend/src/styles/landing.css
+++ b/frontend/src/styles/landing.css
@@ -74,9 +74,8 @@
}
.landing-nav.scrolled {
- background: rgba(20, 22, 29, 0.95);
+ background: #0d0f15;
border-bottom: 1px solid var(--lp-border);
- backdrop-filter: blur(8px);
}
.landing-nav-inner {
@@ -230,19 +229,44 @@
/* ---- HERO ---- */
.landing-hero {
- padding: 9rem 2rem 5rem;
+ padding: 10rem 2rem 8rem;
+ position: relative;
+ overflow: hidden;
+ min-height: 580px;
+}
+
+/* Full-bleed image layer — positioned lower so terrain fills, hub upper-right */
+.landing-hero::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: url('/images/hero_001.jpg') 58% 38% / cover no-repeat;
+ opacity: 0.72;
+ z-index: 0;
+}
+
+/* Left-to-right bleed: solid dark where text lives, dissolves into raw image on the right */
+.landing-hero::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(to right, #14161d 22%, rgba(20, 22, 29, 0.80) 38%, rgba(20, 22, 29, 0.20) 58%, transparent 78%),
+ linear-gradient(to top, #14161d 0%, rgba(20, 22, 29, 0) 16%);
+ z-index: 1;
}
.landing-hero-inner {
max-width: 1200px;
margin: 0 auto;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 4rem;
- align-items: center;
+ display: flex;
+ align-items: flex-start;
+ position: relative;
+ z-index: 2;
}
.landing-hero-content {
+ max-width: 520px;
animation: landingFadeInUp 0.8s ease-out;
}
@@ -286,7 +310,7 @@
}
.landing-hero-accent {
- color: var(--lp-accent-text);
+ color: var(--lp-accent);
}
.landing-hero-sub {
@@ -359,72 +383,205 @@
animation: landingPreviewEntrance 1s cubic-bezier(0.22, 1, 0.36, 1) 0.3s both;
}
-/* ---- PREVIEW WINDOW ---- */
-.landing-preview-window {
+/* ---- TICKET COMPARISON (hero visual) ---- */
+.landing-ticket-comparison {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ width: 100%;
+}
+
+.landing-tc-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ min-width: 0;
+}
+
+.landing-tc-label {
+ font-size: 0.6rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ padding: 0 0.25rem;
+}
+
+.landing-tc-label.before-label {
+ color: var(--lp-text-dim);
+}
+
+.landing-tc-label.after-label {
+ color: var(--lp-accent);
+}
+
+.landing-tc-card {
border-radius: 8px;
border: 1px solid var(--lp-border);
background: var(--lp-card);
- overflow: hidden;
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
+ padding: 0.875rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ flex: 1;
}
-.landing-preview-titlebar {
+.landing-tc-card.before-card {
+ opacity: 0.55;
+}
+
+.landing-tc-card.after-card {
+ border-color: rgba(96, 165, 250, 0.28);
+ box-shadow: 0 0 36px rgba(96, 165, 250, 0.10), 0 16px 48px rgba(0, 0, 0, 0.55);
+}
+
+.landing-tc-header {
display: flex;
align-items: center;
- gap: 12px;
- padding: 10px 14px;
- background: rgba(255, 255, 255, 0.02);
+ justify-content: space-between;
+ padding-bottom: 0.5rem;
border-bottom: 1px solid var(--lp-border);
}
-.landing-preview-dots {
- display: flex;
- gap: 6px;
+.tc-ticket-id {
+ font-size: 0.65rem;
+ font-weight: 700;
+ color: var(--lp-text-secondary);
+ font-family: 'IBM Plex Sans', sans-serif;
}
-.landing-preview-dots span {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: var(--lp-elevated);
+.tc-status {
+ font-size: 0.58rem;
+ font-weight: 600;
+ padding: 2px 7px;
+ border-radius: 100px;
+ letter-spacing: 0.02em;
}
-.landing-preview-dots span:first-child {
- background: #ef4444;
+.tc-status.open {
+ background: rgba(248, 113, 113, 0.1);
+ color: #f87171;
+ border: 1px solid rgba(248, 113, 113, 0.2);
}
-.landing-preview-dots span:nth-child(2) {
- background: #eab308;
-}
-
-.landing-preview-dots span:last-child {
- background: #22c55e;
-}
-
-.landing-preview-url {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 3px 10px;
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid var(--lp-border);
- font-size: 0.6rem;
- color: var(--lp-text-dim);
- flex: 1;
- max-width: 260px;
-}
-
-.landing-lock-icon {
+.tc-status.resolved {
+ background: rgba(52, 211, 153, 0.1);
color: var(--lp-success);
- font-size: 0.55rem;
+ border: 1px solid rgba(52, 211, 153, 0.2);
}
-.landing-preview-body {
- padding: 1.25rem;
- min-height: 260px;
+.landing-tc-subject {
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: var(--lp-text-heading);
+ line-height: 1.4;
+}
+
+.landing-tc-notes-heading {
+ font-size: 0.58rem;
+ font-weight: 600;
+ color: var(--lp-text-dim);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.landing-tc-notes {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ flex: 1;
+}
+
+.landing-tc-notes.before-notes {
+ padding: 0.6rem 0.75rem;
+ border-radius: 5px;
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px dashed rgba(255, 255, 255, 0.06);
+ font-size: 0.65rem;
+ color: var(--lp-text-dim);
+ font-style: italic;
+ min-height: 80px;
+ align-items: flex-start;
+ justify-content: flex-start;
+}
+
+.tc-note {
+ display: flex;
+ align-items: baseline;
+ gap: 5px;
+ font-size: 0.62rem;
+ color: var(--lp-text-secondary);
+ line-height: 1.5;
+ animation: tcNoteIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
+ animation-delay: calc(1.2s + var(--note-i, 0) * 0.55s);
+ opacity: 0;
+}
+
+@keyframes tcNoteIn {
+ from { opacity: 0; transform: translateY(5px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.tc-note.resolution-note {
+ margin-top: 0.15rem;
+ padding-top: 0.35rem;
+ border-top: 1px solid var(--lp-border);
+}
+
+.tc-time {
+ font-size: 0.55rem;
+ color: var(--lp-text-dim);
+ font-family: 'JetBrains Mono', monospace;
+ flex-shrink: 0;
+ min-width: 28px;
+}
+
+.tc-check {
+ color: var(--lp-success);
+ flex-shrink: 0;
+ font-size: 0.6rem;
+}
+
+.tc-resolution-tag {
+ font-size: 0.52rem;
+ font-weight: 700;
+ padding: 1px 5px;
+ border-radius: 3px;
+ background: rgba(96, 165, 250, 0.12);
+ color: var(--lp-accent-text);
+ flex-shrink: 0;
+ letter-spacing: 0.02em;
+}
+
+.landing-tc-footer {
+ font-size: 0.58rem;
+ padding-top: 0.4rem;
+ border-top: 1px solid var(--lp-border);
+ line-height: 1.4;
+}
+
+.landing-tc-footer.before-footer {
+ color: var(--lp-text-dim);
+}
+
+.landing-tc-footer.after-footer {
+ color: var(--lp-accent-text);
+ font-weight: 500;
+}
+
+.landing-tc-divider {
display: flex;
align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 32px;
+ padding-top: 1.4rem;
+ color: var(--lp-text-dim);
+}
+
+.landing-tc-divider svg {
+ width: 14px;
+ height: 14px;
}
/* ---- MOCK ELEMENTS ---- */
@@ -1255,79 +1412,6 @@
to { opacity: 1; transform: translateY(0) scale(1); }
}
-/* ---- CHAT ANIMATION ---- */
-.landing-chat-animated {
- opacity: 0;
- transform: translateX(-16px);
- animation: landingChatSlideIn 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
- animation-delay: calc(1.5s + var(--chat-index, 0) * 1.2s);
-}
-
-.landing-chat-animated:nth-child(2) {
- animation: landingTypingLifecycle 3s ease both;
- animation-delay: 2.7s;
-}
-
-.landing-chat-animated:nth-child(3) { animation-delay: 5.7s; }
-.landing-chat-animated:nth-child(4) { animation-delay: 6.7s; }
-.landing-chat-animated:nth-child(5) { animation-delay: 7.7s; }
-.landing-chat-animated:nth-child(6) { animation-delay: 9s; }
-
-@keyframes landingChatSlideIn {
- from { opacity: 0; transform: translateX(-16px); }
- to { opacity: 1; transform: translateX(0); }
-}
-
-@keyframes landingTypingLifecycle {
- 0% { opacity: 0; transform: translateX(-16px); }
- 10% { opacity: 1; transform: translateX(0); }
- 75% { opacity: 1; transform: translateX(0); }
- 90% { opacity: 0; transform: translateX(0); }
- 100% { opacity: 0; height: 0; padding: 0; margin: 0; overflow: hidden; }
-}
-
-.landing-typing-indicator {
- display: flex;
- align-items: center;
- gap: 5px;
- padding: 6px 10px;
- border-radius: 6px;
- background: var(--lp-accent-soft);
- border: 1px solid rgba(96, 165, 250, 0.1);
- width: fit-content;
-}
-
-.landing-typing-indicator span {
- display: block;
- width: 5px;
- height: 5px;
- border-radius: 50%;
- background: var(--lp-accent);
- opacity: 0.5;
- animation: landingTypingBounce 1s ease-in-out infinite;
-}
-
-.landing-typing-indicator span:nth-child(2) { animation-delay: 0.15s; }
-.landing-typing-indicator span:nth-child(3) { animation-delay: 0.3s; }
-
-.landing-typing-label {
- font-size: 0.6rem;
- color: var(--lp-accent);
- font-weight: 500;
- white-space: nowrap;
- /* Override dot styles */
- width: auto !important;
- height: auto !important;
- border-radius: 0 !important;
- background: transparent !important;
- opacity: 1 !important;
- animation: none !important;
-}
-
-@keyframes landingTypingBounce {
- 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
- 30% { transform: translateY(-4px); opacity: 1; }
-}
/* ---- SCROLL REVEAL ---- */
.landing-reveal {
@@ -1617,7 +1701,6 @@
/* ---- REDUCED MOTION ---- */
@media (prefers-reduced-motion: reduce) {
- .landing-chat-animated,
.landing-hero-visual,
.landing-hero-content {
opacity: 1;
@@ -1625,13 +1708,10 @@
animation: none;
}
- .landing-typing-indicator span {
+ .tc-note {
+ opacity: 1;
+ transform: none;
animation: none;
- opacity: 0.6;
- }
-
- .landing-chat-animated:nth-child(2) {
- display: none;
}
.landing-reveal {