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:
@@ -13,7 +13,7 @@ from app.models.user import User
|
||||
from app.schemas.session import SessionCreate, SessionUpdate, SessionResponse, SessionExport, ScratchpadUpdate, SaveAsTreeRequest, SaveAsTreeResponse
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.core.permissions import can_access_tree
|
||||
from app.services.export_service import generate_markdown_export, generate_text_export, generate_html_export
|
||||
from app.services.export_service import generate_markdown_export, generate_text_export, generate_html_export, generate_psa_export
|
||||
|
||||
router = APIRouter(prefix="/sessions", tags=["sessions"])
|
||||
|
||||
@@ -288,6 +288,9 @@ async def export_session(
|
||||
elif export_options.format == "html":
|
||||
content = generate_html_export(session, export_options)
|
||||
media_type = "text/html"
|
||||
elif export_options.format == "psa":
|
||||
content = generate_psa_export(session, export_options)
|
||||
media_type = "text/plain"
|
||||
else: # text
|
||||
content = generate_text_export(session, export_options)
|
||||
media_type = "text/plain"
|
||||
|
||||
@@ -70,7 +70,7 @@ class SessionResponse(BaseModel):
|
||||
|
||||
|
||||
class SessionExport(BaseModel):
|
||||
format: str = Field(default="markdown", pattern="^(text|markdown|html)$")
|
||||
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
|
||||
include_timestamps: bool = True
|
||||
include_tree_info: bool = True
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user