feat: add PSA ticket export format and Quick-Start landing page

PSA Export:
- New "PSA / Ticket Note" export format optimized for ConnectWise
- Structured output: Problem, Steps Taken, Resolution, Time Spent, Notes
- Prominent "Copy for Ticket" button on session detail page
- 24 unit tests for PSA export generator

Quick-Start Landing:
- New default landing page with search-first UX
- Auto-focused search bar with debounced tree search
- "Continue Session" cards for active sessions
- "Recent Trees" section from session history
- Home nav item and logo links updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-08 19:36:51 -05:00
parent f2ae3a51fa
commit 4f8b7dd7ca
11 changed files with 621 additions and 13 deletions

View File

@@ -1,15 +1,31 @@
"""
Session export generators for ResolutionFlow.
Provides markdown, plain text, and HTML export formatters
Provides markdown, plain text, HTML, and PSA/ticket note export formatters
for troubleshooting sessions.
"""
import html
from datetime import datetime, timezone
from app.models.session import Session
from app.schemas.session import SessionExport
def _format_duration(started_at: datetime, completed_at: datetime | None) -> str:
"""Format duration between two datetimes as human-readable string."""
if not completed_at:
return "In progress"
delta = completed_at - started_at
total_seconds = int(delta.total_seconds())
if total_seconds < 0:
return "0 minutes"
hours, remainder = divmod(total_seconds, 3600)
minutes = remainder // 60
if hours > 0:
return f"{hours}h {minutes}m"
return f"{minutes} minutes"
def generate_markdown_export(session: Session, options: SessionExport) -> str:
"""Generate markdown export."""
lines = []
@@ -160,3 +176,65 @@ def generate_html_export(session: Session, options: SessionExport) -> str:
html_parts.extend(['</body>', '</html>'])
return "\n".join(html_parts)
def generate_psa_export(session: Session, options: SessionExport) -> str:
"""Generate PSA/ticket note export optimized for ConnectWise and similar PSA tools."""
lines = []
tree_name = session.tree_snapshot.get("name", "Troubleshooting Session")
tree_description = session.tree_snapshot.get("description", "")
ticket_number = session.ticket_number or "N/A"
client_name = session.client_name or "N/A"
date_str = session.started_at.strftime("%Y-%m-%d %H:%M")
lines.append("=== TROUBLESHOOTING NOTES ===")
lines.append(f"Ticket: {ticket_number} | Client: {client_name}")
lines.append(f"Tree: {tree_name} | Date: {date_str}")
lines.append("")
# Problem section
lines.append("--- PROBLEM ---")
lines.append(tree_description if tree_description else "No description provided.")
lines.append("")
# Steps taken
lines.append("--- STEPS TAKEN ---")
if session.decisions:
for i, decision in enumerate(session.decisions, 1):
question = decision.get("question") or decision.get("action_performed", "Step")
answer = decision.get("answer", "")
notes = decision.get("notes", "")
line = f"{i}. {question}"
if answer:
line += f" -> {answer}"
lines.append(line)
if notes:
lines.append(f" Notes: {notes}")
else:
lines.append("No steps recorded.")
lines.append("")
# Resolution - last decision answer
lines.append("--- RESOLUTION ---")
if session.decisions:
last_decision = session.decisions[-1]
resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded")
lines.append(resolution)
else:
lines.append("No resolution recorded.")
lines.append("")
# Time spent
lines.append("--- TIME SPENT ---")
duration = _format_duration(session.started_at, session.completed_at)
lines.append(f"Duration: {duration}")
lines.append("")
# Engineer notes (scratchpad)
lines.append("--- ENGINEER NOTES ---")
scratchpad = getattr(session, 'scratchpad', '') or ''
lines.append(scratchpad.strip() if scratchpad.strip() else "None")
return "\n".join(lines)