When adding a remote with a URL like https://172.0.0.18:8006, the code
was previously passing the port as part of the hostname
(172.0.0.18:8006) while also setting the port separately, causing
connection failures.
Now properly extracts the port from the URL if present, falling back
to default ports (8006 for PVE, 8007 for PBS) if not specified.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Replace hardcoded dummy data in VMs, Containers, Storage, Backup, and
Firewall pages with live API calls; show empty-state UI when no
clusters are configured
- Add list_proxmox_containers backend command (LXC via cluster/resources)
and register it in the Tauri handler and frontend proxmoxClient.ts
- Fix add_proxmox_cluster to store credentials without requiring a live
Proxmox connection; persist username in DB (migration 034); update
list/get queries to read username column from new schema
- Replace alert() in RemotesPage with toast.error() + rethrow so errors
surface correctly in Tauri WebView
- Replace tauri-plugin-updater with direct Gitea HTTP API call for
update checks; use tauri-plugin-opener for browser launch; Updater UI
now shows current/latest version and release notes
- Add gogs.tftsr.com to CSP connect-src
- Fix all 74 pre-existing ESLint no-explicit-any warnings in
proxmoxClient.ts; remove stale eslint-disable directive in ACLPage.tsx
- All checks pass: cargo fmt, clippy -D warnings, 411 Rust tests,
tsc --noEmit, eslint --max-warnings 0, 386 frontend tests
- Replace NetworkPage placeholder with live network interface list (type, address, gateway, active/autostart badges)
- Replace TasksPage placeholder with real cluster task log including running/completed/failed summary cards
- Create ViewsPage with create/delete UI for custom dashboard views
- Fix createClusterView TS client to pass viewId + name params matching Rust command signature
- Fix ClusterView TS interface to use view_id matching Rust DashboardView serialization
- Add ClusterInfoWithHealth struct to list_proxmox_clusters command with connected field reflecting in-memory pool state
- Add connected? field to ClusterInfo domain type
- Wire /proxmox/views route and Views nav entry in App.tsx
- CertificateList: full table with CN/SANs/Issuer/validity columns,
expandable rows for full subject/fingerprint, color-coded status badges
(green valid / yellow expiring <30d / red expired), View Details dialog,
Renew action per row, empty state
- CertificatesPage: real data via listCertificates(), cluster selector for
multi-cluster setups, Upload Custom Certificate dialog (file picker + PEM
input), Order via ACME dialog with domain/node fields, error banner
- SubscriptionPage: two-panel layout — left panel for subscription key entry
and activation with masked key display; right panel cluster status tree
with Active/Expired/None badges, registration and next-due dates
- domain.ts: add Certificate interface (filename, subject, san, issuer,
notbefore, notafter, fingerprint, pem)
- App.tsx: wire /proxmox/subscriptions route and nav entry
- HAGroupsList: replace stub with real HaGroup type from proxmoxClient;
columns: Name, Nodes, Restricted, No-Quorum Policy, Comment, Actions;
empty state; onCreate/onEdit/onDelete props wired
- HAResourcesList: replace stub with real HaResource type; columns:
Resource ID, Group, State, Max Restart, Max Relocate, Actions;
onEnable/onRemove props; empty state
- HAPage: add useEffect data fetching for listHaGroups/listHaResources;
auto-selects first cluster from listProxmoxClusters; multi-cluster
dropdown when >1 cluster; wires deleteHaGroup and enableHaResource
- AclList: migrate from local AclInfo to canonical AclEntry type
(ugid/roleid fields); composite key for rows without unique id
- UserList: migrate from local UserInfo to ProxmoxUser type; adds
Realm, Name, Expire columns; deriveRealm helper; proper icon buttons
- RealmList: migrate from local RealmInfo to AuthRealm type (realm/type/
comment); trimmed to three columns matching backend shape
- ACLPage: replace hardcoded dummy ACL array with real data fetching;
add Tabs (ACL / Users / Auth Realms) with controlled state; calls
listAcls, listUsers, listRealms on mount and cluster change; removes
all hardcoded stub data
- ProxmoxSettings: load all six settings from localStorage on mount via
useEffect, wire Save button to write values and show a 2s confirmation,
wire Reset button to clear keys and restore defaults
- RemotesPage: attach loadRemotes() to the header Refresh button onClick
and replace the no-op onRefresh prop passed to RemotesList
- EditRemoteForm: add password field to RemoteConfig interface and form
so handleEditRemote receives a complete config; use DialogFooter for
consistent button layout
- Add Proxmox cluster management commands to tauriCommands.ts
- Fix RemotesPage.tsx to use actual IPC calls instead of mock data
- Add Proxmox settings section to App.tsx settings navigation
- Create ProxmoxSettings page with update management (stable/pre-release)
- Add Proxmox submenu navigation to sidebar with expandable section
- Update docs/RELEASE_NOTES.md to include v1.2.0 Proxmox features
This fixes critical bugs preventing cluster persistence and navigation.
- Correct start_pty_exec_session and start_pty_attach_session invoke calls
to use pod/container keys matching Rust command parameter names; drop
unused shell arg from the invoke payload
- Fix ansi-to-react CJS/ESM interop in LogStreamPanel: unwrap .default on
CJS module so React does not receive a plain object at render time; add
optimizeDeps entry to vite.config.ts so Vite pre-bundles it in dev
- Replace Badge + getPodStatusColor with StatusBadge in PodList; remove
now-unused helper; extend getStatusVariant in Badge.tsx to handle
crashloopbackoff, OOM, backoff, terminating, and evicted states
- Fix pre-existing lint issues: remove unused listPodsCmd/listNamespacesCmd
imports from PortForwardPage, wrap loadPortForwards in useCallback, and
remove unused logLine variable from LogStreamPanel test
Replace LogsModal with LogStreamPanel in PodList for streaming logs
Add smart positioning to ResourceActionMenu to flip when near bottom
Fix dark mode text visibility by applying class to html element
Fix YAML editor loading race condition
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix LogStreamPanel event listener cleanup with synchronous unlisten
- Fix eventBus async-unsafe unsubscribe with proper error handling
- Fix KubernetesPage infinite loading by resetting state on section change
- Add ErrorBoundary component with reset capability
- Add Badge component with multiple variants
- Add ResourceDetailsDrawer for slide-out details panel
- Add useFavorites hook with localStorage persistence
- Add useKeyboardShortcuts hook for declarative shortcuts
- Add comprehensive test coverage for all new components/hooks
- Add keyboard shortcuts documentation to README
- Wrap KubernetesPage with ErrorBoundary for crash recovery
- Install react-window for virtual scrolling support
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add PortForwardPage.tsx as standalone page for port forwarding management
with complete CRUD operations (Start, Stop, Delete). Includes real-time
status updates, auto-refresh, and integrated form for creating new forwards.
All 6 network resource list components already exist and are complete:
- ServiceList.tsx: Name, Type, Cluster IP, External IP, Ports, Age, Status
- IngressList.tsx: Name, Namespace, Load Balancers, Rules, Age
- NetworkPolicyList.tsx: Name, Namespace, Pod Selector, Age
- EndpointList.tsx: Name, Namespace, Endpoints, Age
- EndpointSliceList.tsx: Name, Namespace, Endpoints, Address Type, Age
- IngressClassList.tsx: Name, Controller, Age
Backend commands verified in kube.rs:
- start_port_forward, stop_port_forward, list_port_forwards, delete_port_forward
Navigation already integrated in KubernetesPage.tsx Network group.
- workloads_overview now fetches pods/deployments/statefulsets/daemonsets/jobs/
cronjobs in parallel via Promise.allSettled
- loadInitialData initializedRef guard prevents double connectClusterFromKubeconfig
- connection errors now surface as a dismissible banner instead of being swallowed
Replace hardcoded light-mode Tailwind colors with dark: variants
across six components. Issues that broke readability:
- PiiDiffViewer / Security: toggle knob was bg-white (invisible on
bg-muted in dark mode) -> bg-background
- ImageGallery: thumbnail container, filename labels, alert banners,
and modal chrome all used hardcoded gray/white backgrounds with dark
text; added full dark: variants throughout
- ShellExecution TIER_CONFIG: tier cards used bg-green/yellow/red-50
(near-white) with dark text; added dark:bg-*-950/30 backgrounds and
light text for all three tiers
- ShellApprovalModal: tier 2 badge hardcoded bg-yellow-50/text-yellow-700;
added dark: variants
- LogUpload: PII warning alert used bg-amber-50/text-amber-800; added
dark:bg-amber-900/20 and lighter text for dark mode
Credential error persists: switch all 40 kubectl invocations from using
KUBECONFIG env var to the explicit --kubeconfig CLI flag. The flag has higher
precedence in kubectl's lookup order and is unambiguous regardless of any
inherited KUBECONFIG env var in the parent process environment.
Also adds test_kubectl_connection Tauri command (runs kubectl cluster-info
with the stored kubeconfig) and a Test button in Settings → Kubeconfig so
the exact kubectl output — context name, exit code, full stdout/stderr — is
visible without needing to inspect tracing logs. This output will reveal
whether the issue is expired certs, a missing exec-auth plugin, wrong context,
or something else entirely.
1. kubectl credentials error (41 places in kube.rs)
Every kubectl invocation used .env("KUBERNETES_CONTEXT", context) which
is not a real kubectl environment variable — kubectl silently ignores it
and falls back to whatever current-context is set in the kubeconfig YAML.
If that context has expired or wrong credentials the auth failure occurs.
Replaced all 41 instances with .arg("--context").arg(context) so kubectl
always uses the correct context from the stored kubeconfig.
2. Cluster name still showed UUID (two causes)
a) Hotbar read from kubernetesStore.clusters (ClusterInfo[]) which is never
populated by the kubeconfig-based flow — always empty, so selectedCluster
was always undefined. Removed the Zustand cluster lookup from Hotbar and
added a clusterName prop passed from KubernetesPage.tsx (selectedConfig?.name).
b) ClusterOverview fell back to showing raw clusterId UUID when clusterName
was undefined. Changed subtitle to render conditionally so UUID never shows.
3. Bell dialog had no way to close
Custom DialogContent had no X button and no backdrop-click handler.
Added X close button (top-right) and backdrop-click-to-close.
4. Hotbar icons invisible in dark mode
variant="ghost" only styles hover state with no baseline text color.
Added className="text-foreground" to all icon-only ghost buttons.
Bug 1 — Dead multi-word tier3 entries / missing single-token commands
parse_single_command() extracts only the first token as `command`, so
multi-word entries like "kill -9", "init 0", "service stop" in the tier3
array never matched. Adding the single-token forms "kill", "pkill",
"killall", "init" to TIER3_COMMANDS ensures these commands are always
denied. Removed all dead multi-word entries.
Bug 2 — systemctl Tier 1 special case was dead code
systemctl was not in tier1_general, so the block that was supposed to
auto-execute `systemctl status` never ran. Moved systemctl handling into
its own block (TIER1_SYSTEMCTL_SUBCOMMANDS / TIER2_SYSTEMCTL_SUBCOMMANDS)
evaluated before the general tier checks. status, is-active, is-enabled,
list-units, list-unit-files → Tier 1; all others → Tier 2.
Bug 3 — ldapmodify / ldapdelete / ldapadd misclassified as Tier 1
Both appeared in the old tier1_general and tier2_general arrays; the tier1
check ran first, so LDAP write operations auto-executed. Removed them from
tier1. ldapsearch (read-only) remains Tier 1.
Dynamic Safety Architecture UI
Extracted all tier classification arrays to module-level pub const slices
(TIER3_COMMANDS, TIER1_KUBECTL_SUBCOMMANDS, etc.) so both the classifier
logic and a new get_classifier_rules() Tauri command share a single source
of truth. ShellExecution.tsx now calls getClassifierRulesCmd() on mount and
renders the actual command lists in collapsible per-tier cards — any change
to the const arrays is automatically reflected in the UI with no manual
documentation update needed.
Also fixes the cargo fmt CI failure introduced in the previous commit
(ClusterClient::new call reformatted to a single line).
Resolves four bugs in the Kubernetes management interface:
1. **Cluster not found error** - commands/kube.rs::list_nodes (and all other
kube resource commands) look up clusters from state.clusters (in-memory map)
which was never populated from the kubeconfig_files table. Add a new
connect_cluster_from_kubeconfig Tauri command that reads the encrypted
kubeconfig from the DB, decrypts it, and inserts a ClusterClient into
state.clusters. Wire it into KubernetesPage on initial load and cluster
change so the in-memory map is always populated before any kube command runs.
2. **Dropdown selection has no effect** - same root cause as #1; activating a
kubeconfig only updated the DB flag but never loaded the client into memory.
handleClusterChange now calls connectClusterFromKubeconfigCmd after activation.
3. **GUID shown instead of cluster name** - ClusterOverview displayed the raw
internal UUID as the page subtitle. Now accepts a clusterName prop (populated
from kubeconfig.context) and renders that instead. ClusterDetails similarly
changed to show kubeconfig.context in the header, not the UUID.
4. **Bell icon not clickable** - Hotbar bell button had no onClick handler. Add
optional onNotifications / notificationCount props; badge count is now dynamic
rather than hardcoded. KubernetesPage wires up a notifications dialog showing
active cluster context and a link to the Events section.
All changes follow TDD: failing tests written first, then implementation.
Complete overhaul of the Kubernetes management page from a basic config
panel into a full Lens-style IDE shell with 26 resource types, real-time
data, and a comprehensive test suite.
Layout & navigation:
- Rewrite KubernetesPage as a Lens v5-style shell: collapsible sidebar
(Workloads / Services & Networking / Config & Storage / Access Control /
Cluster), top hotbar with cluster+namespace selectors, Ctrl+K command
palette
- All 26 resource types now accessible via sidebar navigation (previously 5)
New resource types (Rust + TypeScript + React):
- StorageClasses, NetworkPolicies, ResourceQuotas, LimitRanges
- 4 new Tauri commands registered in generate_handler![]
Component implementations (replacing stubs with real IPC):
- Terminal: full xterm.js with multi-tab sessions and exec_pod IPC
- YamlEditor: Monaco editor with YAML syntax highlighting
- MetricsChart: recharts LineChart/BarChart
- ClusterOverview: live node/pod/deployment/namespace counts
- ClusterDetails: real kubeconfig + node data
- PodDetail, DeploymentDetail, ServiceDetail, ConfigMapDetail, SecretDetail:
all connected to real IPC data, zero hardcoded values
- CreateResourceModal, EditResourceModal: wired to createResourceCmd /
editResourceCmd
- RbacViewer: live data from 4 RBAC IPC commands
- RbacEditor: create roles/cluster-roles via YAML editor
- CommandPalette: 12 real navigation commands, keyboard nav
Dependencies added: xterm@5, xterm-addon-fit, xterm-addon-web-links,
@monaco-editor/react@4, recharts@2
Tooling:
- Replace eslint-plugin-react (incompatible with ESLint 10) with
@eslint-react/eslint-plugin; fix eslint.config.js for flat config
- Fix pre-existing hoisting lint errors in Security.tsx, PortForwardForm.tsx
- Fix eventBus.ts: replace all `any` generics with `unknown`
Tests: 251 passing across 35 test files (was 94/19)
- 16 new test files covering all new and fixed components (TDD)
- npx tsc --noEmit: 0 errors
- cargo clippy -- -D warnings: 0 warnings
- cargo fmt --check: passes
- eslint src/ --max-warnings 0: 0 issues
- Update handleActivateKubeconfig to call setSelectedCluster after activation
- ResourceBrowser now loads resources for the activated kubeconfig's cluster
- All tests passing, build successful
- Remove duplicate 'Add Cluster' button and modal
- Remove duplicate 'Start Port Forward' button and modal
- KubernetesPage now uses kubeconfig files from Settings → Kubeconfig
- Clusters section displays kubeconfig files with active indicator
- Port forwarding section shows active port forwards without duplicate controls
- All tests passing, build successful
- Backend: kube module with ClusterClient, PortForwardSession, RefreshRegistry
- 7 Tauri IPC commands: add_cluster, remove_cluster, list_clusters, start_port_forward, stop_port_forward, list_port_forwards, delete_port_forward, shutdown_port_forwards
- AppState extended with clusters, port_forwards, refresh_registry fields
- Version bumped to 1.1.0 in Cargo.toml and package.json
- Auto-tag workflow updated to mark releases as draft (pre-release)
- Buy Me A Coffee section added to README.md
- Fixed changelog workflow to only include current tag commits
- Proper kubeconfig YAML parsing with extract_context and extract_server_url
- Added kubeconfig content storage in ClusterClient
- Updated PortForwardSession to include cluster_name
- Frontend GUI components: ClusterList, PortForwardList, AddClusterModal, PortForwardForm, KubernetesPage
- TypeScript types and IPC commands for Kubernetes management
- Unit tests for Kubernetes IPC commands (6 tests)
- All 332 Rust tests passing
- All 98 frontend tests passing
- TypeScript type checks passing
- Project builds successfully in release mode
- Committed and pushed to feature/kubernetes-management branch
- Command injection vulnerability fixed with regex validation and max length check (253 chars)
- stop_port_forward and shutdown_port_forwards properly kill kubectl child processes via async child management
- Temp file cleanup implemented with RAII TempFileCleanup struct created before std::fs::write
- discover_pods now parses actual kubectl JSON output
- ChildWaitHandle implemented with background task for waiting on kubectl child
- PortForwardSession uses Arc<TokioMutex<Option<Child>>> for async-safe child management
- Port-forward uses kubectl's dynamic port binding (0) instead of TcpListener
- Added shutdown_port_forwards command for app shutdown cleanup
- Added cleanup effect in App.tsx to call shutdownPortForwardsCmd on unmount
- Database CRUD operations for clusters and port_forwards added to db.rs
- validate_resource_name uses lazy_static! for cached Regex to prevent ReDoS
- Cluster struct updated to store kubeconfig_content directly instead of kubeconfig_id
- Cluster model in db/models.rs updated to use kubeconfig_content field
- load_clusters and load_port_forwards commands registered in lib.rs
- Temp file cleanup moved to background task in ChildWaitHandle to ensure cleanup after kubectl completes
- Unused child_id field removed from ChildWaitHandle
- Command validation moved to beginning of start_port_forward before any operations
- Fixed lint errors: removed unused imports, fixed React hooks order, updated type annotations
- Updated eslint.config.js to properly configure file patterns
- Add separate onDelete handler for PortForwardList (Stop vs Delete actions)
- Add namespace validation in PortForwardForm (required field)
- Update KubernetesPage to pass onDelete handler to PortForwardList
- Add eslint-disable comment for unused handlePaste in LogUpload
- Fix unused 'get' parameter in attachmentStore.ts
- Fix ESLint setup.ts parsing error by adding it to test files config
Change plaintext_env input field from type='password' to type='text' since
this field is explicitly for non-sensitive values (DEBUG, LOG_LEVEL, etc.).
Using password type for plaintext config was misleading and prevented
copy/paste of legitimate non-sensitive configuration.
Only the encrypted_env and http_headers fields remain as type='password'
for sensitive values like API keys and tokens.
Add clearer placeholder and helper text to explain that encrypted environment
variables are never displayed for security reasons. When editing an existing
server, the encrypted_env field shows a placeholder explaining that leaving it
blank will preserve existing values.
Also apply cargo fmt formatting fixes to store.rs.
Add dual-mode environment variable support for stdio MCP servers and custom
HTTP headers for HTTP-based MCP servers to enable proper authentication and
configuration.
Backend changes (Rust):
- Add migration 023 for env_config column in mcp_servers table
- Add env_config field to McpServer, CreateMcpServerRequest, UpdateMcpServerRequest
- Encrypt env_config using AES-256-GCM on create/update in store.rs
- Add get_server_env_config() helper to decrypt and parse env vars
- Parse plaintext env from transport_config.env (stdio only)
- Parse custom headers from transport_config.headers (HTTP only)
- Merge plaintext and encrypted env vars (encrypted takes precedence)
- Update connect_stdio() to accept HashMap<String, String> for env vars
- Update connect_http() to accept HashMap<String, String> for headers
- Apply env vars to tokio::process::Command via .env() method
- Add warning for HTTP headers (rmcp v1.7.0 limitation - no .header() method)
- Add comprehensive tests for encryption, merging, and clearing
Frontend changes (TypeScript/React):
- Add env_config field to CreateMcpServerRequest and UpdateMcpServerRequest
- Add plaintext_env, encrypted_env, http_headers to ServerForm interface
- Add parsing helpers: parseEnvVars(), formatEnvVars(), parseHeaders(), formatHeaders()
- Update startEdit() to extract and format env vars/headers from transport_config
- Update handleSave() to build transport_config with env/headers and env_config JSON
- Add conditional UI fields: stdio (plaintext + encrypted env), HTTP (custom headers)
- Use password input type for all sensitive fields
Security:
- Encrypted env vars stored using AES-256-GCM (matching auth_value pattern)
- Plaintext env vars in transport_config for non-sensitive values
- UI masks all env/header fields with password input type
- Never display decrypted values when editing
Fixes inability to configure MCP servers that require environment variables
(e.g., GitHub MCP server with GITHUB_PERSONAL_ACCESS_TOKEN).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add was_pii_redacted and pii_types_redacted to the ai_chat audit log
entry. Both are tracked through the full_message build block (typed
message + attachments) so any redaction that occurs is always
reflected in the compliance record.
Fix response.user_message + suffix potentially yielding 'undefined...'
when user_message is absent. Now unconditionally calls
updateMessageContent with (response.user_message ?? message) + suffix,
so the bubble always shows a valid string regardless of backend build.
Update TICKET-pii-bypass-chat-attachments.md to reflect the final
auto-redact design (not block/warn) so automated review comparisons
against the ticket stop flagging design decisions as defects.
Remove frontend detectPiiCmd pre-scan loop — backend is sole redaction
authority; bubble update via response.user_message covers user feedback.
Detect PII on full file content before truncating. Previous order
(truncate to 8000 bytes then scan) could miss PII straddling the
boundary. Now: read full content, scan, redact, then truncate to
EMBED_LIMIT (8000 bytes) at a valid UTF-8 char boundary.
logFileIds IPC: pass undefined (not null) for empty array so Tauri
serialises it correctly to Rust Option::None.
Add MAX_TEXT_SCAN_BYTES (32 KB) guard in scan_text_for_pii to prevent
unbounded regex evaluation on oversized payloads.
Fix clippy uninlined_format_args in ai.rs.
Addresses three findings from the third automated review:
[BLOCKER] No frontend PII pre-check on attachments.
Added detectPiiCmd call for each logFileId before chatMessageCmd.
PII is not blocked (per explicit product decision: auto-redact and
send) but the user now sees a non-blocking amber notice listing
each file and the PII types that will be auto-redacted. Backend
remains the authoritative redaction layer.
[WARNING 2] Chat bubble showed original PII-laden message even though
only the redacted form was sent to AI.
Added updateMessageContent to sessionStore. After chatMessageCmd
returns, if response.user_message is set the user bubble is updated
to reflect what was actually stored in the DB, so the UI is
consistent with the audit log.
CI fix: cargo fmt changes to analysis.rs were not staged in the prior
commit. Committed here — fmt check now passes cleanly.
Resolves all three findings from the second automated review and
fixes the cargo fmt --check CI failure (formatting drift in analysis.rs
from a prior merge).
[BLOCKER 1 + BLOCKER 2 + WARNING]
Frontend no longer performs any PII scanning or redaction. All three
concerns stemmed from the same root cause: outMessage was derived
on the frontend and used for display, DB storage (via lastUserMsgRef
and the chat bubble), and the AI payload — causing the original message
to be silently replaced before the backend received it.
Fix: frontend sends the original message verbatim. Backend is now the
sole authority. chat_message auto-redacts the typed message text using
PiiDetector + apply_redactions() before building the full payload, logs
the PII types via tracing::warn, and stores only the redacted form in
ai_messages and the audit log. The redacted form is returned to the
caller as ChatResponse.user_message (Option<String>, absent from direct
provider calls).
Frontend uses message (original) for the chat bubble and
lastUserMsgRef — resolution steps show natural language, not
[Password] tokens. The AI and DB see only the redacted version.
CI fix: cargo fmt applied to analysis.rs; all format checks now pass.
Resolves all four findings from the automated review:
[BLOCKER 1] Attachment PII scan error path left pendingFiles intact,
allowing retry with stale file references. Fix: file content is no
longer held in frontend state at all — PendingFile drops the content
field entirely. logFileIds are captured before setPendingFiles([]) and
passed directly to the backend.
[BLOCKER 2] Raw file content stored in PendingFile.content created a
UI-visible PII surface and a data-residency risk. Fix: frontend never
reads or stores file content. The backend loads file data from disk,
auto-redacts PII in-memory using pii::apply_redactions(), and embeds
the clean text into the AI message. No PII ever touches the frontend.
[WARNING 1] String-based attachment header parsing was fragile and
bypassable. Fix: parsing is gone — backend identifies attachments by
log_file_id, reads them directly from the DB/disk path, and applies
redaction at that level.
[WARNING 2] Error message disclosed PII type list to the caller. Fix:
PII types are logged via tracing::warn only; no type details in the
user-facing error or API response.
Additionally: typed chat messages are now auto-redacted rather than
blocked. scanTextForPiiCmd runs on the typed text; detected spans are
replaced in reverse-offset order before the message is sent to the AI
and stored in the DB. The user sees the redacted form in their chat
bubble.
Architecture:
- chat_message now accepts log_file_ids: Option<Vec<String>>
- Backend reads file → detects PII → redacts in memory → embeds
- Frontend: no readTextFile, no content field, no frontend PII gate
File attachments were embedded into AI messages without any PII
scanning, allowing credentials, tokens, and other sensitive data
to be forwarded to AI providers in plaintext.
Typed chat messages had the same gap: a user could type a password
or API key directly and it would be sent unscanned.
Changes:
- chat_message (Rust): defence-in-depth scan of all attachment body
content (between --- Attached: markers); hard rejects if PII found
- detect_pii (Rust): fix return type from pii::PiiDetectionResult
(spans/original_text) to db::models::PiiDetectionResult
(detections/total_pii_found) to match the TypeScript contract; the
LogUpload PII review workflow was receiving undefined for detections
- scan_text_for_pii (Rust): new command — scans arbitrary text for PII
without creating DB records; used for typed message warnings
- Triage/index.tsx: PendingFile now carries logFileId; handleSend gates
each text attachment through detectPiiCmd (hard block on PII found);
typed message text scanned via scanTextForPiiCmd with a one-time
warning — second send of same message proceeds as acknowledgment
- compress_text now returns Result<Vec<u8>, String>; callers propagate
the error instead of silently storing empty BLOB on gzip failure
- upload_image_attachment_by_content and upload_paste_image now validate
decoded byte length against MAX_IMAGE_FILE_BYTES before DB storage,
closing the size-bypass gap that existed for base64 content uploads
- Image View modal in AttachmentsTab now surfaces the error string when
get_image_attachment_data fails, replacing the opaque "could not be
loaded" message with actionable diagnostic text
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
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>
Fixes two bugs identified in the AI code review:
1. INSERT OR REPLACE with a freshly generated UUID never matches the
existing primary key, so it appended rows instead of replacing.
Switch to DELETE-then-INSERT to guarantee exactly one row.
2. Username defaulted to empty string. Resolve it to the current OS
user (USER/LOGNAME env vars, fallback 'local') so credentials are
always bound to a specific user identity.
test_sudo_password now passes -u <username> to sudo so the test
runs scoped to the stored user, not an arbitrary one.
UI: show the configured username prominently in status; relabel the
field and add a scope hint below it.
Tests: test_set_sudo_singleton_delete_then_insert, three username
resolution tests.
- Add extension allowlist (SAFE_TEXT_EXTENSIONS + SAFE_BINARY_EXTENSIONS)
rejecting unsupported file types at both upload_log_file and
upload_log_file_by_content entry points
- Add extract_text_content() with PDF text extraction via lopdf and
DOCX extraction via zip+quick-xml
- Binary files (PDF/DOCX) get extracted text written to .extracted.txt
for downstream PII detection
- Expand frontend file input accept list and add collapsible
supported-formats disclosure element
- Add 11 unit tests covering allowlist logic and extraction paths
- Implement AgentRegistry system with devops-incident-responder agent
- Add domain detection based on conversation keywords
- Inject devops-incident-responder as primary system prompt
- Auto-switch domain prompts silently when context shifts
- Fix version update script to handle JSON format correctly
- Always display version in bottom-left corner
- Add release notes fallback to git commits if CHANGELOG empty
This implements the full devops-incident-responder agent as the primary
system prompt, with domain-specific SME prompts layered on top based on
conversation content analysis. The version display bug is fixed by removing
the collapsed condition, and release notes now have a fallback mechanism.