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
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
// ─── Issue ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Issue {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub severity: String,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub category: String,
|
|
|
|
|
pub source: String,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
pub updated_at: String,
|
|
|
|
|
pub resolved_at: Option<String>,
|
|
|
|
|
pub assigned_to: String,
|
|
|
|
|
pub tags: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Issue {
|
|
|
|
|
pub fn new(title: String, description: String, severity: String, category: String) -> Self {
|
|
|
|
|
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
|
|
|
|
Issue {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
severity,
|
|
|
|
|
status: "open".to_string(),
|
|
|
|
|
category,
|
|
|
|
|
source: "manual".to_string(),
|
|
|
|
|
created_at: now.clone(),
|
|
|
|
|
updated_at: now,
|
|
|
|
|
resolved_at: None,
|
|
|
|
|
assigned_to: String::new(),
|
|
|
|
|
tags: "[]".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Full detail view returned by get_issue.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct IssueDetail {
|
|
|
|
|
pub issue: Issue,
|
|
|
|
|
pub log_files: Vec<LogFile>,
|
2026-04-09 01:03:34 +00:00
|
|
|
pub image_attachments: Vec<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
|
|
|
pub resolution_steps: Vec<ResolutionStep>,
|
|
|
|
|
pub conversations: Vec<AiConversation>,
|
2026-04-19 23:02:38 +00:00
|
|
|
pub timeline_events: Vec<TimelineEvent>,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lightweight row returned by list/search commands.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct IssueSummary {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub severity: String,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub category: String,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
pub updated_at: String,
|
|
|
|
|
pub log_count: i64,
|
|
|
|
|
pub step_count: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct IssueListItem {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub domain: String,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub severity: String,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct IssueFilter {
|
|
|
|
|
pub status: Option<String>,
|
|
|
|
|
pub severity: Option<String>,
|
|
|
|
|
pub category: Option<String>,
|
|
|
|
|
pub domain: Option<String>,
|
|
|
|
|
pub search: Option<String>,
|
|
|
|
|
pub limit: Option<i64>,
|
|
|
|
|
pub offset: Option<i64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct NewIssue {
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub domain: String,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub severity: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct IssueUpdate {
|
|
|
|
|
pub title: Option<String>,
|
|
|
|
|
pub description: Option<String>,
|
|
|
|
|
pub severity: Option<String>,
|
|
|
|
|
pub status: Option<String>,
|
|
|
|
|
pub category: Option<String>,
|
|
|
|
|
pub domain: Option<String>,
|
|
|
|
|
pub assigned_to: Option<String>,
|
|
|
|
|
pub tags: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 5-Whys ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct FiveWhyEntry {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub why_number: i32,
|
|
|
|
|
pub question: String,
|
|
|
|
|
pub answer: Option<String>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Timeline ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct TimelineEvent {
|
|
|
|
|
pub id: String,
|
2026-04-19 23:02:38 +00:00
|
|
|
pub issue_id: String,
|
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
|
|
|
pub event_type: String,
|
|
|
|
|
pub description: String,
|
2026-04-19 23:02:38 +00:00
|
|
|
pub metadata: String,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TimelineEvent {
|
2026-04-19 23:13:30 +00:00
|
|
|
pub fn new(
|
|
|
|
|
issue_id: String,
|
|
|
|
|
event_type: String,
|
|
|
|
|
description: String,
|
|
|
|
|
metadata: String,
|
|
|
|
|
) -> Self {
|
2026-04-19 23:02:38 +00:00
|
|
|
TimelineEvent {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
issue_id,
|
|
|
|
|
event_type,
|
|
|
|
|
description,
|
|
|
|
|
metadata,
|
2026-04-19 23:13:30 +00:00
|
|
|
created_at: chrono::Utc::now()
|
|
|
|
|
.format("%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
|
.to_string(),
|
2026-04-19 23:02:38 +00:00
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Log File ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct LogFile {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub file_name: String,
|
|
|
|
|
pub file_path: String,
|
|
|
|
|
pub file_size: i64,
|
|
|
|
|
pub mime_type: String,
|
|
|
|
|
pub content_hash: String,
|
|
|
|
|
pub uploaded_at: String,
|
|
|
|
|
pub redacted: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LogFile {
|
|
|
|
|
pub fn new(issue_id: String, file_name: String, file_path: String, file_size: i64) -> Self {
|
|
|
|
|
LogFile {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
issue_id,
|
|
|
|
|
file_name,
|
|
|
|
|
file_path,
|
|
|
|
|
file_size,
|
|
|
|
|
mime_type: "text/plain".to_string(),
|
|
|
|
|
content_hash: String::new(),
|
|
|
|
|
uploaded_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
redacted: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── PII ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PiiSpanRecord {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub log_file_id: String,
|
|
|
|
|
pub pii_type: String,
|
|
|
|
|
pub start_offset: i64,
|
|
|
|
|
pub end_offset: i64,
|
|
|
|
|
pub original_value: String,
|
|
|
|
|
pub replacement: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PiiDetectionResult {
|
|
|
|
|
pub log_file_id: String,
|
|
|
|
|
pub detections: Vec<crate::pii::PiiSpan>,
|
|
|
|
|
pub total_pii_found: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct RedactedLogFile {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub original_file_id: String,
|
|
|
|
|
pub file_name: String,
|
|
|
|
|
pub file_hash: String,
|
|
|
|
|
pub redaction_count: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── AI Conversation ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AiConversation {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub provider: String,
|
|
|
|
|
pub model: String,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AiConversation {
|
|
|
|
|
pub fn new(issue_id: String, provider: String, model: String) -> Self {
|
|
|
|
|
AiConversation {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
issue_id,
|
|
|
|
|
provider,
|
|
|
|
|
model,
|
|
|
|
|
created_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
title: "Untitled".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AiMessage {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub conversation_id: String,
|
|
|
|
|
pub role: String,
|
|
|
|
|
pub content: String,
|
|
|
|
|
pub token_count: i64,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AiMessage {
|
|
|
|
|
pub fn new(conversation_id: String, role: String, content: String) -> Self {
|
|
|
|
|
AiMessage {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
conversation_id,
|
|
|
|
|
role,
|
|
|
|
|
content,
|
|
|
|
|
token_count: 0,
|
|
|
|
|
created_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Resolution Step ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ResolutionStep {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub step_order: i64,
|
|
|
|
|
pub why_question: String,
|
|
|
|
|
pub answer: String,
|
|
|
|
|
pub evidence: String,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ResolutionStep {
|
|
|
|
|
pub fn new(
|
|
|
|
|
issue_id: String,
|
|
|
|
|
step_order: i64,
|
|
|
|
|
why_question: String,
|
|
|
|
|
answer: String,
|
|
|
|
|
evidence: String,
|
|
|
|
|
) -> Self {
|
|
|
|
|
ResolutionStep {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
issue_id,
|
|
|
|
|
step_order,
|
|
|
|
|
why_question,
|
|
|
|
|
answer,
|
|
|
|
|
evidence,
|
|
|
|
|
created_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Document ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Document {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub doc_type: String,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub content_md: String,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Document {
|
|
|
|
|
pub fn new(issue_id: String, doc_type: String, title: String, content_md: String) -> Self {
|
|
|
|
|
let now = chrono::Utc::now().timestamp_millis();
|
|
|
|
|
Document {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
issue_id,
|
|
|
|
|
doc_type,
|
|
|
|
|
title,
|
|
|
|
|
content_md,
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Audit ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AuditEntry {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub timestamp: String,
|
|
|
|
|
pub action: String,
|
|
|
|
|
pub entity_type: String,
|
|
|
|
|
pub entity_id: String,
|
|
|
|
|
pub user_id: String,
|
|
|
|
|
pub details: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AuditEntry {
|
|
|
|
|
pub fn new(action: String, entity_type: String, entity_id: String, details: String) -> Self {
|
|
|
|
|
AuditEntry {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
timestamp: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
action,
|
|
|
|
|
entity_type,
|
|
|
|
|
entity_id,
|
|
|
|
|
user_id: "local".to_string(),
|
|
|
|
|
details,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct AuditFilter {
|
|
|
|
|
pub action: Option<String>,
|
|
|
|
|
pub entity_type: Option<String>,
|
|
|
|
|
pub entity_id: Option<String>,
|
|
|
|
|
pub from_date: Option<String>,
|
|
|
|
|
pub to_date: Option<String>,
|
|
|
|
|
pub limit: Option<i64>,
|
|
|
|
|
pub offset: Option<i64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Settings ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct SettingRecord {
|
|
|
|
|
pub key: String,
|
|
|
|
|
pub value: String,
|
|
|
|
|
pub updated_at: String,
|
|
|
|
|
}
|
2026-04-03 19:23:49 +00:00
|
|
|
|
|
|
|
|
// ─── Integrations ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Credential {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub service: String,
|
|
|
|
|
pub token_hash: String,
|
|
|
|
|
pub encrypted_token: String,
|
|
|
|
|
pub created_at: String,
|
|
|
|
|
pub expires_at: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Credential {
|
|
|
|
|
pub fn new(service: String, token_hash: String, encrypted_token: String) -> Self {
|
|
|
|
|
Credential {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
service,
|
|
|
|
|
token_hash,
|
|
|
|
|
encrypted_token,
|
|
|
|
|
created_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
expires_at: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct IntegrationConfig {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub service: String,
|
|
|
|
|
pub base_url: String,
|
|
|
|
|
pub username: Option<String>,
|
|
|
|
|
pub project_name: Option<String>,
|
|
|
|
|
pub space_key: Option<String>,
|
|
|
|
|
pub auto_create_enabled: bool,
|
|
|
|
|
pub updated_at: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IntegrationConfig {
|
|
|
|
|
pub fn new(service: String, base_url: String) -> Self {
|
|
|
|
|
IntegrationConfig {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
service,
|
|
|
|
|
base_url,
|
|
|
|
|
username: None,
|
|
|
|
|
project_name: None,
|
|
|
|
|
space_key: None,
|
|
|
|
|
auto_create_enabled: false,
|
|
|
|
|
updated_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-09 01:03:34 +00:00
|
|
|
|
|
|
|
|
// ─── Image Attachment ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ImageAttachment {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub file_name: String,
|
|
|
|
|
pub file_path: String,
|
|
|
|
|
pub file_size: i64,
|
|
|
|
|
pub mime_type: String,
|
|
|
|
|
pub upload_hash: String,
|
|
|
|
|
pub uploaded_at: String,
|
|
|
|
|
pub pii_warning_acknowledged: bool,
|
|
|
|
|
pub is_paste: bool,
|
|
|
|
|
}
|
|
|
|
|
|
feat: attachment DB storage and cross-incident recall
Store compressed log content and raw image bytes in SQLite so attachments
are self-contained regardless of source file availability on disk.
DB (migrations 020-022):
- log_files.content_compressed BLOB — gzip-compressed extracted text
- image_attachments.image_data BLOB — raw image bytes
- Views v_log_files_with_issue and v_image_attachments_with_issue for
cross-incident queries with joined issue title
Rust backend:
- compress_text / decompress_text helpers (flate2 rust_backend / miniz_oxide)
with 100 MB decompression-bomb guard
- upload_log_file*, upload_log_file_by_content store content_compressed
- upload_image_attachment*, upload_paste_image store image_data
- New commands: get_log_file_content, list_all_log_files (analysis.rs)
- New commands: get_image_attachment_data, list_all_image_attachments (image.rs)
- All commands fall back to file_path for pre-migration records
Frontend:
- LogFileSummary, ImageAttachmentSummary types in tauriCommands.ts
- attachmentStore (Zustand) — loadAttachments, searchAttachments
- History page: Issues tab (existing) + Attachments tab (new)
with log/image tables, search bar, View modals, lazy thumbnails
Tests: 227 Rust (+16 new), 103 frontend (+9 new), tsc clean, clippy clean
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 22:55:47 +00:00
|
|
|
// ─── Attachment Summaries (cross-incident list views) ───────────────────────
|
|
|
|
|
|
|
|
|
|
/// Lightweight log-file row joined with the parent issue title.
|
|
|
|
|
/// Returned by `list_all_log_files` — never contains the compressed content blob.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct LogFileSummary {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub issue_title: String,
|
|
|
|
|
pub file_name: String,
|
|
|
|
|
pub file_path: String,
|
|
|
|
|
pub file_size: i64,
|
|
|
|
|
pub mime_type: String,
|
|
|
|
|
pub content_hash: String,
|
|
|
|
|
pub uploaded_at: String,
|
|
|
|
|
pub redacted: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lightweight image-attachment row joined with the parent issue title.
|
|
|
|
|
/// Returned by `list_all_image_attachments` — never contains the raw image bytes.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ImageAttachmentSummary {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub issue_id: String,
|
|
|
|
|
pub issue_title: String,
|
|
|
|
|
pub file_name: String,
|
|
|
|
|
pub file_path: String,
|
|
|
|
|
pub file_size: i64,
|
|
|
|
|
pub mime_type: String,
|
|
|
|
|
pub upload_hash: String,
|
|
|
|
|
pub uploaded_at: String,
|
|
|
|
|
pub pii_warning_acknowledged: bool,
|
|
|
|
|
pub is_paste: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 20:14:04 +00:00
|
|
|
// ─── Kubernetes Cluster ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Represents a Kubernetes cluster configuration stored in the database.
|
|
|
|
|
/// The kubeconfig_content is encrypted before storage.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Cluster {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub context: String,
|
|
|
|
|
pub server_url: String,
|
|
|
|
|
pub kubeconfig_content: String,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Cluster {
|
|
|
|
|
pub fn new(
|
|
|
|
|
name: String,
|
|
|
|
|
context: String,
|
|
|
|
|
server_url: String,
|
|
|
|
|
kubeconfig_content: String,
|
|
|
|
|
) -> Self {
|
|
|
|
|
let now = chrono::Utc::now().timestamp();
|
|
|
|
|
Cluster {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
name,
|
|
|
|
|
context,
|
|
|
|
|
server_url,
|
|
|
|
|
kubeconfig_content,
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lightweight summary for cluster list views.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ClusterSummary {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub context: String,
|
|
|
|
|
pub server_url: String,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
pub port_forward_count: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Port Forward ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Represents a port forwarding session for a Kubernetes cluster.
|
|
|
|
|
/// The ports and local_ports are stored as JSON arrays of u16.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PortForward {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub cluster_id: String,
|
|
|
|
|
pub namespace: String,
|
|
|
|
|
pub pod: String,
|
|
|
|
|
pub container: Option<String>,
|
|
|
|
|
pub ports: Vec<u16>,
|
|
|
|
|
pub local_ports: Vec<u16>,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub error_message: Option<String>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PortForward {
|
|
|
|
|
pub fn new(
|
|
|
|
|
cluster_id: String,
|
|
|
|
|
namespace: String,
|
|
|
|
|
pod: String,
|
|
|
|
|
container: Option<String>,
|
|
|
|
|
ports: Vec<u16>,
|
|
|
|
|
local_ports: Vec<u16>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
let now = chrono::Utc::now().timestamp();
|
|
|
|
|
PortForward {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
cluster_id,
|
|
|
|
|
namespace,
|
|
|
|
|
pod,
|
|
|
|
|
container,
|
|
|
|
|
ports,
|
|
|
|
|
local_ports,
|
|
|
|
|
status: "Active".to_string(),
|
|
|
|
|
error_message: None,
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lightweight summary for port forward list views.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PortForwardSummary {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub cluster_id: String,
|
|
|
|
|
pub cluster_name: String,
|
|
|
|
|
pub namespace: String,
|
|
|
|
|
pub pod: String,
|
|
|
|
|
pub container: Option<String>,
|
|
|
|
|
pub ports: Vec<u16>,
|
|
|
|
|
pub local_ports: Vec<u16>,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Filter for listing clusters.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct ClusterFilter {
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
pub context: Option<String>,
|
|
|
|
|
pub limit: Option<i64>,
|
|
|
|
|
pub offset: Option<i64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Filter for listing port forwards.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct PortForwardFilter {
|
|
|
|
|
pub cluster_id: Option<String>,
|
|
|
|
|
pub status: Option<String>,
|
|
|
|
|
pub namespace: Option<String>,
|
|
|
|
|
pub limit: Option<i64>,
|
|
|
|
|
pub offset: Option<i64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// New cluster data for creation.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct NewCluster {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub context: String,
|
|
|
|
|
pub server_url: String,
|
|
|
|
|
pub kubeconfig_content: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update for existing cluster.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct ClusterUpdate {
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
pub context: Option<String>,
|
|
|
|
|
pub server_url: Option<String>,
|
|
|
|
|
pub kubeconfig_content: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// New port forward data for creation.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct NewPortForward {
|
|
|
|
|
pub cluster_id: String,
|
|
|
|
|
pub namespace: String,
|
|
|
|
|
pub pod: String,
|
|
|
|
|
pub container: Option<String>,
|
|
|
|
|
pub ports: Vec<u16>,
|
|
|
|
|
pub local_ports: Vec<u16>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Update for existing port forward.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
|
|
|
pub struct PortForwardUpdate {
|
|
|
|
|
pub status: Option<String>,
|
|
|
|
|
pub error_message: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 01:03:34 +00:00
|
|
|
impl ImageAttachment {
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub fn new(
|
|
|
|
|
issue_id: String,
|
|
|
|
|
file_name: String,
|
|
|
|
|
file_path: String,
|
|
|
|
|
file_size: i64,
|
|
|
|
|
mime_type: String,
|
|
|
|
|
upload_hash: String,
|
|
|
|
|
pii_warning_acknowledged: bool,
|
|
|
|
|
is_paste: bool,
|
|
|
|
|
) -> Self {
|
|
|
|
|
ImageAttachment {
|
|
|
|
|
id: Uuid::now_v7().to_string(),
|
|
|
|
|
issue_id,
|
|
|
|
|
file_name,
|
|
|
|
|
file_path,
|
|
|
|
|
file_size,
|
|
|
|
|
mime_type,
|
|
|
|
|
upload_hash,
|
|
|
|
|
uploaded_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
|
|
|
pii_warning_acknowledged,
|
|
|
|
|
is_paste,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|