TFTSR — IT Triage & Root-Cause Analysis Desktop Application
Implementation Plan
Overview
TFTSR is a desktop-first, offline-capable application that helps IT teams
perform structured incident triage using the 5-Whys methodology, backed by
pluggable AI providers (Ollama local, OpenAI, Anthropic, Mistral, Gemini).
It automates PII redaction, guides engineers through root-cause analysis, and
produces post-mortem documents (Markdown / PDF / DOCX).
Architecture Decisions
Area
Choice
Rationale
Desktop framework
Tauri 2.x
Small binary, native webview, Rust backend for security
Frontend framework
React 18
Large ecosystem, component model fits wizard-style UX
State management
Zustand
Minimal boilerplate, TypeScript-friendly, no context nesting
Local database
SQLCipher (via rusqlite + bundled-sqlcipher)
Encrypted SQLite — secrets and PII at rest
Secret storage
Tauri Stronghold
OS-keychain-grade encrypted vault for API keys
AI providers
Ollama (local), OpenAI, Anthropic, Mistral, Gemini
User choice; local-first with cloud fallback
Unit tests (frontend)
Vitest
Fast, Vite-native, first-class TS support
E2E tests
WebdriverIO + tauri-driver
Official Tauri E2E path, cross-platform
CI/CD
Woodpecker CI (Gogs at 172.0.0.29:3000)
Self-hosted, Docker-native, YAML pipelines
Bundling
Vite 6
Dev server + production build, used by Tauri CLI
Directory Structure
tftsr/
├── .woodpecker/
│ ├── test.yml # lint + unit tests on push / PR
│ └── release.yml # multi-platform build on tag
├── cli/
│ ├── package.json
│ └── src/
│ └── main.ts # minimal CLI entry point
├── src/ # React frontend
│ ├── assets/
│ ├── components/
│ │ ├── common/ # Button, Card, Modal, DropZone …
│ │ ├── dashboard/ # IssueList, StatsCards
│ │ ├── triage/ # WhyStep, ChatBubble, ProgressBar
│ │ ├── rca/ # DocEditor, ExportBar
│ │ ├── settings/ # ProviderForm, ThemeToggle
│ │ └── pii/ # PiiHighlighter, RedactionPreview
│ ├── hooks/ # useInvoke, useListener, useTheme …
│ ├── lib/
│ │ ├── tauriCommands.ts # typed invoke wrappers & TS types
│ │ └── utils.ts # date formatting, debounce, etc.
│ ├── pages/
│ │ ├── DashboardPage.tsx
│ │ ├── NewIssuePage.tsx
│ │ ├── TriagePage.tsx
│ │ ├── RcaPage.tsx
│ │ ├── LogViewerPage.tsx
│ │ └── SettingsPage.tsx
│ ├── stores/
│ │ ├── sessionStore.ts # current triage session state
│ │ └── settingsStore.ts # theme, providers, preferences
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ ├── capabilities/
│ │ └── default.json
│ ├── icons/
│ ├── src/
│ │ ├── main.rs # Tauri entry point
│ │ ├── db.rs # SQLCipher connection & migrations
│ │ ├── commands/ # IPC command modules
│ │ │ ├── mod.rs
│ │ │ ├── issues.rs
│ │ │ ├── triage.rs
│ │ │ ├── logs.rs
│ │ │ ├── pii.rs
│ │ │ ├── rca.rs
│ │ │ ├── ai.rs
│ │ │ └── settings.rs
│ │ ├── ai/ # AI provider abstractions
│ │ │ ├── mod.rs
│ │ │ ├── ollama.rs
│ │ │ ├── openai_compat.rs
│ │ │ └── prompt_templates.rs
│ │ ├── pii/ # PII detection engine
│ │ │ ├── mod.rs
│ │ │ └── patterns.rs
│ │ └── export/ # Document export
│ │ ├── mod.rs
│ │ ├── markdown.rs
│ │ ├── pdf.rs
│ │ └── docx.rs
│ └── migrations/
│ └── 001_init.sql
├── tests/
│ ├── unit/
│ │ ├── setup.ts
│ │ ├── pii.test.ts
│ │ ├── sessionStore.test.ts
│ │ └── settingsStore.test.ts
│ └── e2e/
│ ├── wdio.conf.ts
│ ├── helpers/
│ │ └── app.ts
│ └── specs/
│ ├── onboarding.spec.ts
│ ├── log-upload.spec.ts
│ ├── triage-flow.spec.ts
│ └── rca-export.spec.ts
├── package.json
├── tsconfig.json
├── vite.config.ts
└── PLAN.md # ← this file
Database Schema (SQLCipher)
All tables live in a single encrypted tftsr.db file under the Tauri
app-data directory.
1. issues
CREATE TABLE issues (
id TEXT PRIMARY KEY ,
title TEXT NOT NULL ,
domain TEXT NOT NULL CHECK ( domain IN
( 'linux' , 'windows' , 'network' , 'k8s' , 'db' , 'virt' , 'hw' , 'obs' )),
status TEXT NOT NULL DEFAULT 'open'
CHECK ( status IN ( 'open' , 'triaging' , 'resolved' , 'closed' )),
severity TEXT CHECK ( severity IN ( 'p1' , 'p2' , 'p3' , 'p4' )),
created_at INTEGER NOT NULL ,
updated_at INTEGER NOT NULL
);
2. triage_messages
CREATE TABLE triage_messages (
id TEXT PRIMARY KEY ,
issue_id TEXT NOT NULL REFERENCES issues ( id ),
role TEXT NOT NULL CHECK ( role IN ( 'user' , 'assistant' , 'system' )),
content TEXT NOT NULL ,
why_level INTEGER NOT NULL DEFAULT 0 ,
created_at INTEGER NOT NULL
);
CREATE INDEX idx_triage_msg_issue ON triage_messages ( issue_id );
3. log_files
CREATE TABLE log_files (
id TEXT PRIMARY KEY ,
issue_id TEXT NOT NULL REFERENCES issues ( id ),
filename TEXT NOT NULL ,
content TEXT NOT NULL ,
mime_type TEXT ,
size_bytes INTEGER ,
created_at INTEGER NOT NULL
);
4. pii_spans
CREATE TABLE pii_spans (
id TEXT PRIMARY KEY ,
log_file_id TEXT NOT NULL REFERENCES log_files ( id ),
pii_type TEXT NOT NULL ,
start_pos INTEGER NOT NULL ,
end_pos INTEGER NOT NULL ,
original TEXT NOT NULL ,
replacement TEXT NOT NULL
);
5. rca_documents
CREATE TABLE rca_documents (
id TEXT PRIMARY KEY ,
issue_id TEXT NOT NULL REFERENCES issues ( id ) UNIQUE ,
content TEXT NOT NULL DEFAULT '' ,
format TEXT NOT NULL DEFAULT 'markdown' ,
created_at INTEGER NOT NULL ,
updated_at INTEGER NOT NULL
);
6. ai_providers
CREATE TABLE ai_providers (
id TEXT PRIMARY KEY ,
name TEXT NOT NULL UNIQUE ,
api_url TEXT NOT NULL ,
model TEXT NOT NULL ,
created_at INTEGER NOT NULL
);
7. settings
CREATE TABLE settings (
key TEXT PRIMARY KEY ,
value TEXT NOT NULL
);
8. export_history
CREATE TABLE export_history (
id TEXT PRIMARY KEY ,
issue_id TEXT NOT NULL REFERENCES issues ( id ),
format TEXT NOT NULL CHECK ( format IN ( 'md' , 'pdf' , 'docx' )),
file_path TEXT NOT NULL ,
created_at INTEGER NOT NULL
);
IPC Command Interface
All frontend ↔ backend communication goes through Tauri's invoke().
Issue commands
Command
Payload
Returns
create_issue
{ title, domain, severity }
Issue
list_issues
{ status?, domain? }
Issue[]
get_issue
{ id }
Issue
update_issue
{ id, title?, status?, severity? }
Issue
delete_issue
{ id }
void
Triage commands
Command
Payload
Returns
send_triage_message
{ issueId, content, whyLevel }
TriageMessage (assistant reply)
get_triage_history
{ issueId }
TriageMessage[]
set_why_level
{ issueId, level }
void
Log commands
Command
Payload
Returns
upload_log
{ issueId, filename, content }
LogFile
list_logs
{ issueId }
LogFile[]
delete_log
{ id }
void
PII commands
Command
Payload
Returns
detect_pii
{ logFileId }
PiiDetectionResult
apply_redactions
{ logFileId, spanIds }
string (redacted text)
RCA / Export commands
Command
Payload
Returns
generate_rca
{ issueId }
RcaDocument
update_rca
{ id, content }
RcaDocument
export_document
{ issueId, format }
string (file path)
AI / Settings commands
Command
Payload
Returns
test_provider
{ name, apiUrl, apiKey?, model }
{ ok, message }
save_provider
{ provider }
void
get_settings
{}
Settings
update_settings
{ key, value }
void
CI/CD Approach
Infrastructure
Git server : Gogs at http://172.0.0.29:3000
CI runner : Woodpecker CI with Docker executor
Artifacts : Uploaded to Gogs releases via API
Pipelines
Pipeline
Trigger
Steps
.woodpecker/test.yml
push, PR
rustfmt check → Clippy → Rust tests → TS typecheck → Vitest → coverage (main only)
.woodpecker/release.yml
v* tag
Build linux-amd64 → Build linux-arm64 → Upload to Gogs release
Security Implementation
Database encryption — SQLCipher with a key derived from Tauri Stronghold.
API key storage — Stronghold vault, never stored in plaintext.
PII redaction — Regex + heuristic engine runs before any text leaves the device.
CSP — Strict Content-Security-Policy in tauri.conf.json; only allowlisted AI API origins.
Least-privilege capabilities — capabilities/default.json grants only required Tauri permissions.
No remote code — All assets bundled; no CDN scripts.
Testing Strategy
Layer
Tool
Location
What it covers
Rust unit
cargo test
src-tauri/src/**
DB operations, PII regex, AI prompt building
Frontend unit
Vitest
tests/unit/
Stores, command wrappers, component logic
E2E
WebdriverIO + tauri-driver
tests/e2e/
Full user flows: onboarding, triage, export
Lint
rustfmt + Clippy + tsc --noEmit
CI
Code style, type safety
Implementation Phases
Phase 1 — Project Scaffold & CI ✅ COMPLETE
Phase 2 — Database & Migrations ✅ COMPLETE
Phase 3 — Stronghold Integration ✅ COMPLETE (scaffold)
Phase 4 — Issue CRUD ✅ COMPLETE
Phase 5 — Log Ingestion & PII Detection ✅ COMPLETE
upload_log_file, detect_pii, apply_redactions commands
PII engine: 11 regex patterns (IPv4, IPv6, email, phone, SSN, CC, MAC, bearer, password, API key, URL)
PiiDiffViewer component
LogUpload page
Phase 6 — AI Provider Abstraction ✅ COMPLETE
Phase 7 — 5-Whys Triage Engine ✅ COMPLETE
Phase 8 — RCA & Post-Mortem Generation ✅ COMPLETE
Phase 9 — Document Export ✅ COMPLETE (MD + PDF)
Phase 10 — Polish & Settings ✅ COMPLETE
Phase 11 — Woodpecker CI Integration ✅ COMPLETE
Phase 12 — Release Package 🔲 PENDING
Known Issues & Gotchas
Gogs Token Authentication
The sha1 in the Gogs CREATE token API response IS the actual bearer token
Gogs stores sha1(token) and sha256(token) in the DB — these are HASHES, not the token itself
Woodpecker user token stored in Woodpecker SQLite DB only (never commit token values)
Woodpecker CI + Gogs v0.15.4 Compatibility
The SPA form login uses login= field but Gogs backend reads username=
Workaround: nginx proxy at :8085 serves custom HTML login page
The webhook ?token= URL param is NOT read by Woodpecker's token.ParseRequest()
Use ?access_token=<JWT> instead (JWT must be HS256 signed with repo_hash as key)
Gogs 0.14 has no OAuth2 provider support — blocks upgrade to Woodpecker 2.x
Rust/DB Type Notes
IssueDetail is NESTED: { issue: Issue, log_files, resolution_steps, conversations }
DB uses TEXT timestamps for created_at/updated_at (not INTEGER)
All commands use the and_then pattern with rusqlite to avoid lifetime issues