2026-04-09 01:03:34 +00:00
|
|
|
|
import React, { useState, useCallback, useRef, useEffect } from "react";
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
import { useNavigate, useParams } from "react-router-dom";
|
2026-04-09 01:03:34 +00:00
|
|
|
|
import { Upload, File, Trash2, ShieldCheck, AlertTriangle, Image as ImageIcon } from "lucide-react";
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from "@/components/ui";
|
|
|
|
|
|
import { PiiDiffViewer } from "@/components/PiiDiffViewer";
|
|
|
|
|
|
import { useSessionStore } from "@/stores/sessionStore";
|
|
|
|
|
|
import {
|
|
|
|
|
|
uploadLogFileCmd,
|
|
|
|
|
|
detectPiiCmd,
|
2026-04-09 01:03:34 +00:00
|
|
|
|
uploadImageAttachmentCmd,
|
|
|
|
|
|
uploadPasteImageCmd,
|
2026-04-09 22:14:41 +00:00
|
|
|
|
uploadFileToDatastoreCmd,
|
2026-04-09 01:03:34 +00:00
|
|
|
|
listImageAttachmentsCmd,
|
|
|
|
|
|
deleteImageAttachmentCmd,
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
type LogFile,
|
|
|
|
|
|
type PiiSpan,
|
|
|
|
|
|
type PiiDetectionResult,
|
2026-04-09 01:03:34 +00:00
|
|
|
|
type ImageAttachment,
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
} from "@/lib/tauriCommands";
|
2026-04-09 01:03:34 +00:00
|
|
|
|
import ImageGallery from "@/components/ImageGallery";
|
2026-04-09 22:14:41 +00:00
|
|
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
|
|
|
|
|
|
export default function LogUpload() {
|
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const { piiSpans, approvedRedactions, setPiiSpans, setApprovedRedactions } = useSessionStore();
|
|
|
|
|
|
|
|
|
|
|
|
const [files, setFiles] = useState<{ file: File; uploaded?: LogFile }[]>([]);
|
2026-04-09 01:03:34 +00:00
|
|
|
|
const [images, setImages] = useState<ImageAttachment[]>([]);
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
const [piiResult, setPiiResult] = useState<PiiDetectionResult | null>(null);
|
|
|
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
|
|
const [isDetecting, setIsDetecting] = useState(false);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
2026-04-09 01:03:34 +00:00
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
const handleDrop = useCallback(
|
|
|
|
|
|
(e: React.DragEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
|
|
|
|
|
setFiles((prev) => [...prev, ...droppedFiles.map((f) => ({ file: f }))]);
|
|
|
|
|
|
},
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
if (e.target.files) {
|
|
|
|
|
|
const selected = Array.from(e.target.files);
|
|
|
|
|
|
setFiles((prev) => [...prev, ...selected.map((f) => ({ file: f }))]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeFile = (index: number) => {
|
|
|
|
|
|
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpload = async () => {
|
|
|
|
|
|
if (!id || files.length === 0) return;
|
|
|
|
|
|
setIsUploading(true);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const uploaded = await Promise.all(
|
|
|
|
|
|
files.map(async (entry) => {
|
|
|
|
|
|
if (entry.uploaded) return entry;
|
|
|
|
|
|
const content = await entry.file.text();
|
|
|
|
|
|
const logFile = await uploadLogFileCmd(id, entry.file.name);
|
|
|
|
|
|
return { ...entry, uploaded: logFile };
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
setFiles(uploaded);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(String(err));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDetectPii = async () => {
|
|
|
|
|
|
const allContent = files
|
|
|
|
|
|
.map((f) => f.uploaded?.file_name)
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join("\n---\n");
|
|
|
|
|
|
if (!allContent) return;
|
|
|
|
|
|
setIsDetecting(true);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = files[0]?.uploaded ? await detectPiiCmd(files[0].uploaded.id) : null;
|
|
|
|
|
|
setPiiResult(result);
|
|
|
|
|
|
if (result) { setPiiSpans(result.detections);
|
|
|
|
|
|
setApprovedRedactions(result.detections); };
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(String(err));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsDetecting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleToggleSpan = (span: PiiSpan, approved: boolean) => {
|
|
|
|
|
|
if (approved) {
|
|
|
|
|
|
setApprovedRedactions([...approvedRedactions, span]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setApprovedRedactions(
|
|
|
|
|
|
approvedRedactions.filter(
|
|
|
|
|
|
(s) => !(s.start === span.start && s.end === span.end)
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-09 01:03:34 +00:00
|
|
|
|
const handleImageDrop = useCallback(
|
2026-04-09 22:14:41 +00:00
|
|
|
|
async (e: React.DragEvent) => {
|
2026-04-09 01:03:34 +00:00
|
|
|
|
e.preventDefault();
|
2026-04-09 22:14:41 +00:00
|
|
|
|
// Use file dialog to get actual paths (drag-and-drop doesn't give paths in Tauri v2)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const selected = await open({
|
|
|
|
|
|
multiple: true,
|
|
|
|
|
|
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }],
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!selected) return;
|
|
|
|
|
|
const paths = Array.isArray(selected) ? selected : [selected];
|
|
|
|
|
|
|
|
|
|
|
|
if (paths.length > 0) {
|
|
|
|
|
|
handleImagesUpload(paths as unknown as File[]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(`Attachment failed: ${String(err)}`);
|
2026-04-09 01:03:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[id]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-09 22:14:41 +00:00
|
|
|
|
const handleImageFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
if (e.target.files && e.target.files.length > 0) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const selected = await open({
|
|
|
|
|
|
multiple: true,
|
|
|
|
|
|
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }],
|
|
|
|
|
|
});
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
const paths = Array.isArray(selected) ? selected : [selected];
|
|
|
|
|
|
|
|
|
|
|
|
if (paths.length > 0) {
|
|
|
|
|
|
handleImagesUpload(paths as unknown as File[]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(`Attachment failed: ${String(err)}`);
|
2026-04-09 01:03:34 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handlePaste = useCallback(
|
|
|
|
|
|
async (e: React.ClipboardEvent) => {
|
|
|
|
|
|
const items = e.clipboardData?.items;
|
|
|
|
|
|
const imageItems = items ? Array.from(items).filter((item: DataTransferItem) => item.type.startsWith("image/")) : [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of imageItems) {
|
|
|
|
|
|
const file = item.getAsFile();
|
|
|
|
|
|
if (file) {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = async () => {
|
|
|
|
|
|
const base64Data = reader.result as string;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await uploadPasteImageCmd(id || "", base64Data, file.type);
|
|
|
|
|
|
setImages((prev) => [...prev, result]);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(String(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[id]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleImagesUpload = async (imageFiles: File[]) => {
|
|
|
|
|
|
if (!id || imageFiles.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
setIsUploading(true);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
try {
|
2026-04-09 22:14:41 +00:00
|
|
|
|
const uploaded: ImageAttachment[] = await Promise.all(
|
2026-04-09 01:03:34 +00:00
|
|
|
|
imageFiles.map(async (file) => {
|
2026-04-09 22:14:41 +00:00
|
|
|
|
// Extract the path from the file name (which contains the full path from file dialog)
|
|
|
|
|
|
const filePath = file.name;
|
|
|
|
|
|
// Get just the filename for display
|
|
|
|
|
|
const fileName = filePath.split(/[\/\\]/).pop() || "unknown";
|
|
|
|
|
|
// Use uploadLogFileCmd which properly handles file paths
|
|
|
|
|
|
const result = await uploadLogFileCmd(id, filePath);
|
|
|
|
|
|
// Convert LogFile to ImageAttachment for the state
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: result.id,
|
|
|
|
|
|
issue_id: result.issue_id,
|
|
|
|
|
|
file_name: result.file_name,
|
|
|
|
|
|
file_path: result.file_path,
|
|
|
|
|
|
file_size: result.file_size,
|
|
|
|
|
|
mime_type: result.mime_type,
|
|
|
|
|
|
upload_hash: result.content_hash,
|
|
|
|
|
|
uploaded_at: result.uploaded_at,
|
|
|
|
|
|
pii_warning_acknowledged: true,
|
|
|
|
|
|
is_paste: false,
|
|
|
|
|
|
};
|
2026-04-09 01:03:34 +00:00
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
setImages((prev) => [...prev, ...uploaded]);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(String(err));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeleteImage = async (image: ImageAttachment) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await deleteImageAttachmentCmd(image.id);
|
|
|
|
|
|
setImages((prev) => prev.filter((img) => img.id !== image.id));
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(String(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fileToBase64 = (file: File): Promise<string> => {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => resolve(reader.result as string);
|
|
|
|
|
|
reader.onerror = (err) => reject(err);
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
const allUploaded = files.length > 0 && files.every((f) => f.uploaded);
|
|
|
|
|
|
const piiReviewed = piiResult != null;
|
|
|
|
|
|
|
2026-04-09 01:03:34 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleGlobalPaste = (e: ClipboardEvent) => {
|
|
|
|
|
|
if (document.activeElement?.tagName === "INPUT" ||
|
|
|
|
|
|
document.activeElement?.tagName === "TEXTAREA" ||
|
|
|
|
|
|
(document.activeElement as HTMLElement)?.isContentEditable || false) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const items = e.clipboardData?.items;
|
|
|
|
|
|
const imageItems = items ? Array.from(items).filter((item: DataTransferItem) => item.type.startsWith("image/")) : [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of imageItems) {
|
|
|
|
|
|
const file = item.getAsFile();
|
|
|
|
|
|
if (file) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = async () => {
|
|
|
|
|
|
const base64Data = reader.result as string;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await uploadPasteImageCmd(id || "", base64Data, file.type);
|
|
|
|
|
|
setImages((prev) => [...prev, result]);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(String(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("paste", handleGlobalPaste);
|
|
|
|
|
|
return () => window.removeEventListener("paste", handleGlobalPaste);
|
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (id) {
|
|
|
|
|
|
listImageAttachmentsCmd(id).then(setImages).catch(setError);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-3xl font-bold">Upload Logs</h1>
|
|
|
|
|
|
<p className="text-muted-foreground mt-1">
|
|
|
|
|
|
Upload log files for PII detection and redaction before triage.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Drop zone */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
onDragOver={(e) => e.preventDefault()}
|
|
|
|
|
|
onDrop={handleDrop}
|
|
|
|
|
|
className="border-2 border-dashed rounded-lg p-8 text-center hover:border-primary transition-colors cursor-pointer"
|
|
|
|
|
|
onClick={() => document.getElementById("file-input")?.click()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Upload className="w-8 h-8 mx-auto text-muted-foreground mb-2" />
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Drag and drop log files here, or click to browse
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="file-input"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
className="hidden"
|
|
|
|
|
|
onChange={handleFileSelect}
|
|
|
|
|
|
accept=".log,.txt,.json,.csv,.xml,.yaml,.yml"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* File list */}
|
|
|
|
|
|
{files.length > 0 && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg">Files ({files.length})</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-2">
|
|
|
|
|
|
{files.map((entry, idx) => (
|
|
|
|
|
|
<div key={idx} className="flex items-center justify-between rounded-md border p-2">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<File className="w-4 h-4 text-muted-foreground" />
|
|
|
|
|
|
<span className="text-sm">{entry.file.name}</span>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
({(entry.file.size / 1024).toFixed(1)} KB)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{entry.uploaded && (
|
|
|
|
|
|
<Badge variant="outline">Uploaded</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => removeFile(idx)}
|
|
|
|
|
|
className="text-muted-foreground hover:text-destructive"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{!allUploaded && (
|
|
|
|
|
|
<Button onClick={handleUpload} disabled={isUploading} className="mt-2">
|
|
|
|
|
|
{isUploading ? "Uploading..." : "Upload Files"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-09 01:03:34 +00:00
|
|
|
|
{/* Image Upload */}
|
|
|
|
|
|
{id && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
|
|
|
|
<ImageIcon className="w-6 h-6" />
|
|
|
|
|
|
Image Attachments
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p className="text-muted-foreground mt-1">
|
|
|
|
|
|
Upload or paste screenshots and images.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Image drop zone */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
onDragOver={(e) => e.preventDefault()}
|
|
|
|
|
|
onDrop={handleImageDrop}
|
|
|
|
|
|
className="border-2 border-dashed border-primary/30 rounded-lg p-8 text-center hover:border-primary transition-colors cursor-pointer bg-primary/5"
|
|
|
|
|
|
onClick={() => document.getElementById("image-input")?.click()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Upload className="w-8 h-8 mx-auto text-primary mb-2" />
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Drag and drop images here, or click to browse
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
|
|
|
|
Supported: PNG, JPEG, GIF, WebP, SVG
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<input
|
|
|
|
|
|
id="image-input"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
className="hidden"
|
|
|
|
|
|
onChange={handleImageFileSelect}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Paste button */}
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
document.execCommand("paste");
|
|
|
|
|
|
}}
|
|
|
|
|
|
variant="secondary"
|
|
|
|
|
|
>
|
|
|
|
|
|
Paste from Clipboard
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
|
Use Ctrl+V / Cmd+V or the button above to paste images
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* PII warning for images */}
|
|
|
|
|
|
<div className="bg-amber-50 border border-amber-200 rounded-md p-3">
|
|
|
|
|
|
<AlertTriangle className="w-5 h-5 text-amber-600 inline mr-2" />
|
|
|
|
|
|
<span className="text-sm text-amber-800">
|
|
|
|
|
|
⚠️ PII cannot be automatically redacted from images. Use at your own risk.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Image Gallery */}
|
|
|
|
|
|
{images.length > 0 && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
|
|
|
|
<ImageIcon className="w-5 h-5" />
|
|
|
|
|
|
Attached Images ({images.length})
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent>
|
|
|
|
|
|
<ImageGallery
|
|
|
|
|
|
images={images}
|
|
|
|
|
|
onDelete={handleDeleteImage}
|
|
|
|
|
|
showWarning={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
feat: initial implementation of TFTSR IT Triage & RCA application
Implements Phases 1-8 of the TFTSR implementation plan.
Rust backend (Tauri 2.x, src-tauri/):
- Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama
- PII detection engine: 11 regex patterns with overlap resolution
- SQLCipher AES-256 encrypted database with 10 versioned migrations
- 28 Tauri IPC commands for triage, analysis, document, and system ops
- Ollama: hardware probe, model recommendations, pull/delete with events
- RCA and blameless post-mortem Markdown document generators
- PDF export via printpdf
- Audit log: SHA-256 hash of every external data send
- Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2)
Frontend (React 18 + TypeScript + Vite, src/):
- 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings
- 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives
- 3 Zustand stores: session, settings (persisted), history
- Type-safe tauriCommands.ts matching Rust backend types exactly
- 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs)
DevOps:
- .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push
- .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload
Verified:
- cargo check: zero errors
- tsc --noEmit: zero errors
- vitest run: 13/13 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 03:36:25 +00:00
|
|
|
|
{/* PII Detection */}
|
|
|
|
|
|
{allUploaded && (
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
|
|
|
|
<ShieldCheck className="w-5 h-5" />
|
|
|
|
|
|
PII Detection
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
{!piiResult && (
|
|
|
|
|
|
<Button onClick={handleDetectPii} disabled={isDetecting}>
|
|
|
|
|
|
{isDetecting ? "Detecting PII..." : "Detect PII"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{piiResult && (
|
|
|
|
|
|
<PiiDiffViewer
|
|
|
|
|
|
originalText="[original log content]"
|
|
|
|
|
|
redactedText="[redacted log content]"
|
|
|
|
|
|
spans={piiSpans}
|
|
|
|
|
|
approvedSpans={approvedRedactions}
|
|
|
|
|
|
onToggleSpan={handleToggleSpan}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Error */}
|
|
|
|
|
|
{error && (
|
|
|
|
|
|
<div className="text-sm text-destructive bg-destructive/10 rounded-md p-3">
|
|
|
|
|
|
{error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Continue */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => navigate(`/issue/${id}/triage`)}
|
|
|
|
|
|
disabled={!piiReviewed}
|
|
|
|
|
|
className="w-full"
|
|
|
|
|
|
size="lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
Continue to Triage
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|