# Database ## Overview TRCAA uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AES-256 encryption in production. 22 versioned migrations are tracked in the `_migrations` table. **DB file location:** `{app_data_dir}/tftsr.db` --- ## Encryption | Build type | Encryption | Key | |-----------|-----------|-----| | Debug (`debug_assertions`) | None (plain SQLite) | — | | Release | SQLCipher AES-256 | `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) env var | **SQLCipher settings (production):** - Cipher: AES-256-CBC - Page size: 4096 bytes - KDF: PBKDF2-HMAC-SHA512, 256,000 iterations - HMAC: HMAC-SHA512 ```rust // Simplified init logic pub fn init_db(data_dir: &Path) -> anyhow::Result { let key = env::var("TRCAA_DB_KEY") .unwrap_or_else(|_| "dev-key-change-in-prod".to_string()); let conn = if cfg!(debug_assertions) { Connection::open(db_path)? // plain SQLite } else { open_encrypted_db(db_path, &key)? // SQLCipher AES-256 }; run_migrations(&conn)?; Ok(conn) } ``` --- ## Schema (18 Migrations) ### 001 — issues ```sql CREATE TABLE issues ( id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT, severity TEXT NOT NULL, -- 'critical', 'high', 'medium', 'low' status TEXT NOT NULL, -- 'open', 'investigating', 'resolved', 'closed' category TEXT, source TEXT, created_at TEXT NOT NULL, -- 'YYYY-MM-DD HH:MM:SS' updated_at TEXT NOT NULL, resolved_at TEXT, -- nullable assigned_to TEXT, tags TEXT -- JSON array stored as TEXT ); ``` ### 002 — log_files ```sql CREATE TABLE log_files ( id TEXT PRIMARY KEY, issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, file_name TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER, mime_type TEXT, content_hash TEXT, -- SHA-256 hex of original content uploaded_at TEXT NOT NULL, redacted INTEGER DEFAULT 0 -- boolean: 0/1 ); ``` ### 003 — pii_spans ```sql CREATE TABLE pii_spans ( id TEXT PRIMARY KEY, log_file_id TEXT NOT NULL REFERENCES log_files(id) ON DELETE CASCADE, pii_type TEXT NOT NULL, start_offset INTEGER NOT NULL, end_offset INTEGER NOT NULL, original_value TEXT NOT NULL, replacement TEXT NOT NULL ); ``` ### 004 — ai_conversations ```sql CREATE TABLE ai_conversations ( id TEXT PRIMARY KEY, issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, provider TEXT NOT NULL, model TEXT NOT NULL, created_at TEXT NOT NULL, title TEXT ); ``` ### 005 — ai_messages ```sql CREATE TABLE ai_messages ( id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL REFERENCES ai_conversations(id) ON DELETE CASCADE, role TEXT NOT NULL CHECK(role IN ('system', 'user', 'assistant')), content TEXT NOT NULL, token_count INTEGER DEFAULT 0, created_at TEXT NOT NULL ); ``` ### 006 — resolution_steps (5-Whys) ```sql CREATE TABLE resolution_steps ( id TEXT PRIMARY KEY, issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, step_order INTEGER NOT NULL, -- 1–5 why_question TEXT NOT NULL, answer TEXT, evidence TEXT, created_at TEXT NOT NULL ); ``` ### 007 — documents ```sql CREATE TABLE documents ( id TEXT PRIMARY KEY, issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, doc_type TEXT NOT NULL, -- 'rca', 'postmortem' title TEXT NOT NULL, content_md TEXT NOT NULL, created_at INTEGER NOT NULL, -- milliseconds since epoch updated_at INTEGER NOT NULL ); ``` > **Note:** `documents` uses INTEGER milliseconds; `issues` and `log_files` use TEXT timestamps. ### 008 — audit_log ```sql CREATE TABLE audit_log ( id TEXT PRIMARY KEY, timestamp TEXT NOT NULL DEFAULT (datetime('now')), action TEXT NOT NULL, -- e.g., 'ai_send', 'publish_to_confluence' entity_type TEXT NOT NULL, -- e.g., 'issue', 'document' entity_id TEXT NOT NULL, user_id TEXT DEFAULT 'local', details TEXT -- JSON with hashes, log_file_ids, etc. ); ``` ### 009 — settings ```sql CREATE TABLE settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL ); ``` ### 010 — issues_fts (Full-Text Search) ```sql CREATE VIRTUAL TABLE issues_fts USING fts5( id UNINDEXED, title, description, content='issues', content_rowid='rowid' ); ``` ### 011 — credentials & integration_config (v0.2.3+) **Integration credentials table:** ```sql CREATE TABLE credentials ( id TEXT PRIMARY KEY, service TEXT NOT NULL CHECK(service IN ('confluence','servicenow','azuredevops')), token_hash TEXT NOT NULL, -- SHA-256 hash for audit encrypted_token TEXT NOT NULL, -- AES-256-GCM encrypted created_at TEXT NOT NULL, expires_at TEXT, UNIQUE(service) ); ``` **Integration configuration table:** ```sql CREATE TABLE integration_config ( id TEXT PRIMARY KEY, service TEXT NOT NULL CHECK(service IN ('confluence','servicenow','azuredevops')), base_url TEXT NOT NULL, username TEXT, -- ServiceNow only project_name TEXT, -- Azure DevOps only space_key TEXT, -- Confluence only auto_create_enabled INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL, UNIQUE(service) ); ``` ### 012 — image_attachments (v0.2.7+) ```sql CREATE TABLE image_attachments ( id TEXT PRIMARY KEY, issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, file_name TEXT NOT NULL, file_path TEXT NOT NULL DEFAULT '', file_size INTEGER NOT NULL DEFAULT 0, mime_type TEXT NOT NULL DEFAULT 'image/png', upload_hash TEXT NOT NULL DEFAULT '', uploaded_at TEXT NOT NULL DEFAULT (datetime('now')), pii_warning_acknowledged INTEGER NOT NULL DEFAULT 1, is_paste INTEGER NOT NULL DEFAULT 0 ); ``` **Features:** - Image file metadata stored in database - `upload_hash`: SHA-256 hash of file content (for deduplication) - `pii_warning_acknowledged`: User confirmation that PII may be present - `is_paste`: Flag for screenshots copied from clipboard **Encryption:** - OAuth2 tokens encrypted with AES-256-GCM - Key derived from `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) environment variable - Random 96-bit nonce per encryption - Format: `base64(nonce || ciphertext || tag)` **Usage:** - OAuth2 flows (Confluence, Azure DevOps): Store encrypted bearer token - Basic auth (ServiceNow): Store encrypted password - One credential per service (enforced by UNIQUE constraint) ### 017 — timeline_events (Incident Response Timeline) ```sql CREATE TABLE timeline_events ( id TEXT PRIMARY KEY, issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE, event_type TEXT NOT NULL, description TEXT NOT NULL, metadata TEXT, -- JSON object with event-specific data created_at TEXT NOT NULL ); CREATE INDEX idx_timeline_events_issue ON timeline_events(issue_id); CREATE INDEX idx_timeline_events_time ON timeline_events(created_at); ``` **Event Types:** - `triage_started` — Incident response begins, initial issue properties recorded - `log_uploaded` — Log file uploaded and analyzed - `why_level_advanced` — 5-Whys entry completed, progression to next level - `root_cause_identified` — Root cause determined from analysis - `rca_generated` — Root Cause Analysis document created - `postmortem_generated` — Post-mortem document created - `document_exported` — Document exported to file (MD or PDF) **Metadata Structure (JSON):** ```json { "triage_started": {"severity": "high", "category": "network"}, "log_uploaded": {"file_name": "app.log", "file_size": 2048576}, "why_level_advanced": {"from_level": 2, "to_level": 3, "question": "Why did the service timeout?"}, "root_cause_identified": {"root_cause": "DNS resolution failure", "confidence": 0.95}, "rca_generated": {"doc_id": "doc_abc123", "section_count": 7}, "postmortem_generated": {"doc_id": "doc_def456", "timeline_events_count": 12}, "document_exported": {"format": "pdf", "file_path": "/home/user/docs/rca.pdf"} } ``` **Design Notes:** - Timeline events are **queryable** (indexed by issue_id and created_at) for document generation - Dual-write: Events recorded to both `timeline_events` and `audit_log` — timeline for chronological reporting, audit_log for security/compliance - `created_at`: TEXT UTC timestamp (`YYYY-MM-DD HH:MM:SS`) - Non-blocking writes: Timeline events recorded asynchronously at key triage moments - Cascade delete from issues ensures cleanup ### 018 — mcp_servers, mcp_tools, mcp_resources (MCP Server Support) **MCP server registry:** ```sql CREATE TABLE mcp_servers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, url TEXT NOT NULL, transport_type TEXT NOT NULL CHECK(transport_type IN ('stdio', 'http')), transport_config TEXT NOT NULL DEFAULT '{}', auth_type TEXT NOT NULL CHECK(auth_type IN ('none', 'api_key', 'bearer', 'oauth2')), auth_value TEXT, -- AES-256-GCM encrypted enabled INTEGER NOT NULL DEFAULT 1, last_discovered_at TEXT, discovery_status TEXT NOT NULL DEFAULT 'pending' CHECK(discovery_status IN ('pending','connected','unreachable','error')), discovery_error TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); ``` **Discovered tools (populated by discovery):** ```sql CREATE TABLE mcp_tools ( id TEXT PRIMARY KEY, server_id TEXT NOT NULL, name TEXT NOT NULL, -- Original tool name from server tool_key TEXT NOT NULL, -- Sanitised key: mcp_{server}_{tool} description TEXT, parameters TEXT NOT NULL DEFAULT '{}', -- JSON Schema FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE ); ``` **Discovered resources:** ```sql CREATE TABLE mcp_resources ( id TEXT PRIMARY KEY, server_id TEXT NOT NULL, uri TEXT NOT NULL, name TEXT, description TEXT, FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE ); ``` **Design notes:** - `auth_value` stored as AES-256-GCM ciphertext (same encryption as integration credentials) - `transport_type` and `auth_type` enforce valid values via CHECK constraints - `discovery_status` tracks connection state: `pending` → `connected` | `unreachable` | `error` - Cascade deletes ensure removing a server cleans up all associated tools and resources - Tools and resources are replaced atomically on each discovery run (delete-all + re-insert) ### 020 — log_files content storage (Attachment Recall v0.4+) ```sql ALTER TABLE log_files ADD COLUMN content_compressed BLOB; ``` Stores gzip-compressed extracted text for every log file uploaded after migration 020. Existing rows remain `NULL` and fall back to the `file_path` column. **Compression:** pure-Rust gzip via `flate2` (`rust_backend` / `miniz_oxide`) — no external binary dependency, works identically on Linux, Windows, macOS. **Usage:** The `get_log_file_content` command reads and decompresses this column. The column is never serialised to the frontend directly — content is requested on demand via IPC. ### 021 — image_attachments byte storage (Attachment Recall v0.4+) ```sql ALTER TABLE image_attachments ADD COLUMN image_data BLOB; ``` Stores raw image bytes for every image uploaded after migration 021. Existing rows fall back to `file_path`. Images are already compressed (PNG/JPEG) so no additional compression is applied. **Usage:** `get_image_attachment_data` reads this column and base64-encodes it into a data URL for frontend display. The `ImageThumbnail` component in the History → Attachments tab calls this on mount for each visible image. ### 022 — attachment cross-incident views (Attachment Recall v0.4+) Two read-only views joining attachments with their parent issue titles: ```sql CREATE VIEW IF NOT EXISTS v_log_files_with_issue AS SELECT lf.id, lf.issue_id, lf.file_name, lf.file_path, lf.file_size, lf.mime_type, lf.content_hash, lf.uploaded_at, lf.redacted, i.title AS issue_title FROM log_files lf JOIN issues i ON i.id = lf.issue_id; CREATE VIEW IF NOT EXISTS v_image_attachments_with_issue AS SELECT ia.id, ia.issue_id, ia.file_name, ia.file_path, ia.file_size, ia.mime_type, ia.upload_hash, ia.uploaded_at, ia.pii_warning_acknowledged, ia.is_paste, i.title AS issue_title FROM image_attachments ia JOIN issues i ON i.id = ia.issue_id; ``` Used by `list_all_log_files` and `list_all_image_attachments` to power the cross-incident Attachments tab in the History page. Explicitly selects named columns (not `SELECT *`) to avoid including the BLOB data in list queries. --- ## Key Design Notes - All primary keys are **UUID v7** (time-sortable) - Boolean flags stored as `INTEGER` (`0`/`1`) - JSON arrays (e.g., `tags`) stored as `TEXT` - `issues` / `log_files` timestamps: `TEXT` (`YYYY-MM-DD HH:MM:SS`) - `documents` timestamps: `INTEGER` (milliseconds since epoch) - All foreign keys with `ON DELETE CASCADE` - Migration history tracked in `_migrations` table (name + applied_at) --- ## Rust Model Types Key structs in `db/models.rs`: ```rust pub struct Issue { pub id: String, pub title: String, pub description: Option, pub severity: String, pub status: String, // ... } pub struct IssueDetail { // Nested — returned by get_issue() pub issue: Issue, pub log_files: Vec, pub resolution_steps: Vec, pub conversations: Vec, } pub struct AuditEntry { pub id: String, pub timestamp: String, pub action: String, // NOT event_type pub entity_type: String, // NOT destination pub entity_id: String, // NOT status pub user_id: String, pub details: Option, } pub struct TimelineEvent { pub id: String, pub issue_id: String, pub event_type: String, pub description: String, pub metadata: Option, // JSON pub created_at: String, } ```