diff --git a/src/renderer/src/components/logs/LogViewer.tsx b/src/renderer/src/components/logs/LogViewer.tsx index 1aad5a7..0fcd023 100644 --- a/src/renderer/src/components/logs/LogViewer.tsx +++ b/src/renderer/src/components/logs/LogViewer.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react' -import { X } from 'lucide-react' +import { Check, Copy, X } from 'lucide-react' import { clsx } from 'clsx' import { useLogStream } from '../../hooks/useLogStream' @@ -14,6 +14,7 @@ function LogPane({ }): React.JSX.Element { const { lines, isStreaming } = useLogStream(name, service) const scrollRef = useRef(null) + const [copied, setCopied] = useState(false) const filteredLines = useMemo(() => { if (!filter.trim()) return lines @@ -25,22 +26,41 @@ function LogPane({ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }) }, [filteredLines.length]) + const copyLogs = async (): Promise => { + const report = [ + 'Aurora Dockside service log', + `Project: ${name}`, + `Service: ${service}`, + `Captured: ${new Date().toISOString()}`, + '', + ...filteredLines.map((line) => line.text) + ].join('\n') + await navigator.clipboard.writeText(report) + setCopied(true) + window.setTimeout(() => setCopied(false), 1600) + } + return ( <> - +
- {isStreaming ? 'streaming' : 'stopped'} - + > + + {isStreaming ? 'streaming' : 'stopped'} · {filteredLines.length} lines + + +
s.setPanelOpen) + const setActiveOperation = useTerminalStore((s) => s.setActiveOperation) + const operationMap = useTerminalStore((s) => s.operations) + const operations = Object.values(operationMap) const scrollRef = useRef(null) + const [copied, setCopied] = useState(false) useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }) @@ -17,17 +21,59 @@ export function TerminalPanel(): React.JSX.Element | null { if (!isPanelOpen || !activeOperationId || !operation) return null + const copyDiagnosticLog = async (): Promise => { + const report = [ + 'Aurora Dockside diagnostic log', + `Operation: ${operation.label}`, + `Status: ${operation.status}`, + `Exit code: ${operation.exitCode ?? 'not finished'}`, + `Started: ${operation.startedAt}`, + `Finished: ${operation.finishedAt ?? 'not finished'}`, + '', + operation.lines.join('').trimEnd() + ].join('\n') + await navigator.clipboard.writeText(report) + setCopied(true) + window.setTimeout(() => setCopied(false), 1600) + } + return (
- {operation.label} - +
+ + + {operation.status}{operation.exitCode !== null ? ` · exit ${operation.exitCode}` : ''} + +
+
+ + +
((set) => ({ set((state) => ({ operations: { ...state.operations, - [id]: { id, label, lines: [], status: 'running', exitCode: null } + [id]: { + id, + label, + lines: [], + status: 'running', + exitCode: null, + startedAt: new Date().toISOString(), + finishedAt: null + } }, activeOperationId: id, isPanelOpen: true @@ -56,7 +66,7 @@ export const useTerminalStore = create((set) => ({ return { operations: { ...state.operations, - [id]: { ...op, status, exitCode } + [id]: { ...op, status, exitCode, finishedAt: new Date().toISOString() } } } }),