diff --git a/PLAN.md b/PLAN.md index e69de29b..e981e6d9 100644 --- a/PLAN.md +++ b/PLAN.md @@ -0,0 +1,416 @@ +# 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` +```sql +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` +```sql +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` +```sql +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` +```sql +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` +```sql +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` +```sql +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` +```sql +CREATE TABLE settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); +``` + +### 8. `export_history` +```sql +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 + +1. **Database encryption** — SQLCipher with a key derived from Tauri Stronghold. +2. **API key storage** — Stronghold vault, never stored in plaintext. +3. **PII redaction** — Regex + heuristic engine runs before any text leaves the device. +4. **CSP** — Strict Content-Security-Policy in `tauri.conf.json`; only allowlisted AI API origins. +5. **Least-privilege capabilities** — `capabilities/default.json` grants only required Tauri permissions. +6. **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 +- [x] Initialise repo with Tauri 2.x + React 18 + Vite +- [x] Configure `tauri.conf.json` and capabilities +- [x] Set up Woodpecker CI pipelines (`test.yml`, `release.yml`) +- [x] Write Vitest setup and mock harness +- [x] Write initial unit tests (PII, sessionStore, settingsStore) — 13/13 passing +- [x] Write E2E scaffolding (wdio config, helpers, skeleton specs) +- [x] Create CLI stub (`cli/`) +- [x] Push to Gogs at http://172.0.0.29:3000/sarman/tftsr-devops_investigation +- [x] Write README.md +- [x] Deploy Woodpecker CI v0.15.4 (server + agent + nginx proxy) +- [ ] **BLOCKED**: Verify CI green on push (Woodpecker hook auth issue — see below) + +### Phase 2 — Database & Migrations ✅ COMPLETE +- [x] Integrate `rusqlite` + `bundled-sqlcipher` +- [x] Write migrations (10 tables: issues, log_files, pii_spans, ai_conversations, ai_messages, resolution_steps, documents, audit_log, settings, integration_publishes) +- [x] Implement migration runner in `db/migrations.rs` +- [x] DB models with all required types + +### Phase 3 — Stronghold Integration ✅ COMPLETE (scaffold) +- [x] `tauri-plugin-stronghold` registered in `lib.rs` +- [x] Password derivation function configured +- [ ] Full key lifecycle tests (deferred to Phase 3 proper) + +### Phase 4 — Issue CRUD ✅ COMPLETE +- [x] All issue CRUD commands: create, get, list, update, delete, search +- [x] 5-Whys tracking: add_five_why, update_five_why +- [x] Timeline events: add_timeline_event +- [x] Dashboard, NewIssue, History pages + +### Phase 5 — Log Ingestion & PII Detection ✅ COMPLETE +- [x] `upload_log_file`, `detect_pii`, `apply_redactions` commands +- [x] PII engine: 11 regex patterns (IPv4, IPv6, email, phone, SSN, CC, MAC, bearer, password, API key, URL) +- [x] PiiDiffViewer component +- [x] LogUpload page + +### Phase 6 — AI Provider Abstraction ✅ COMPLETE +- [x] OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama providers +- [x] `analyze_logs`, `chat_message`, `list_providers` IPC commands +- [x] Settings/AIProviders page +- [x] 8 IT domain system prompts + +### Phase 7 — 5-Whys Triage Engine ✅ COMPLETE +- [x] Triage page with ChatWindow +- [x] TriageProgress component (5-step indicator) +- [x] Auto-detection of why level from AI responses +- [x] Session store with message persistence + +### Phase 8 — RCA & Post-Mortem Generation ✅ COMPLETE +- [x] `generate_rca`, `generate_postmortem` commands +- [x] RCA and post-mortem Markdown templates +- [x] DocEditor component with export (MD, PDF) +- [x] RCA and Postmortem pages + +### Phase 9 — Document Export ✅ COMPLETE (MD + PDF) +- [x] Markdown export +- [x] PDF export via `printpdf` +- [ ] DOCX export (not yet implemented — docx-rs dep removed for simplicity) + +### Phase 10 — Polish & Settings ✅ COMPLETE +- [x] Dark/light theme via Tailwind + CSS variables +- [x] Ollama settings page with hardware detection + model management +- [x] Security page with audit log +- [x] Integrations page (v0.2 stubs) + +### Phase 11 — Woodpecker CI Integration ✅ COMPLETE +- [x] Woodpecker CI v0.15.4 deployed at http://172.0.0.29:8084 +- [x] Webhook delivery: Gogs pushes trigger Woodpecker via `?access_token=` +- [x] Repo activated (DB direct): `repo_active=1`, `repo_trusted=1`, `repo_config_path=.woodpecker/test.yml` +- [x] Clone override: `CI_REPO_CLONE_URL` + `network_mode: gogs_default` for step containers +- [x] All CI steps green (build #19): fmt → clippy → rust-tests (64/64) → ts-check → vitest +- [x] Token security: old tokens rotated, removed from git history, `.gitignore` updated +- [x] Gogs repo set to public (for unauthenticated clone from step containers) + +### Phase 12 — Release Package 🔲 PENDING +- [ ] Tag v0.1.0-alpha +- [ ] Verify Woodpecker builds Linux amd64 + arm64 +- [ ] Verify artifacts upload to Gogs release +- [ ] Smoke-test installed packages + +--- + +## 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=` 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