diff --git a/autobot-frontend/src/components/examples/AsyncOperationExample.vue b/autobot-frontend/src/components/examples/AsyncOperationExample.vue
index 354769693..a957b7078 100644
--- a/autobot-frontend/src/components/examples/AsyncOperationExample.vue
+++ b/autobot-frontend/src/components/examples/AsyncOperationExample.vue
@@ -25,8 +25,8 @@
-
useAsyncOperation Examples
-
+
useAsyncOperation Examples
+
Practical demonstrations of the async operation composable pattern
@@ -1162,7 +1162,7 @@ const loadAnalytics = () => analytics.execute(async () => {
}
.benefit-item {
- background: var(--bg-white-alpha-10);
+ background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 20px;
@@ -1173,7 +1173,7 @@ const loadAnalytics = () => analytics.execute(async () => {
.benefit-item:hover {
transform: translateY(-4px);
- background: var(--bg-white-alpha-15);
+ background: rgba(255, 255, 255, 0.15);
}
.benefit-icon {
diff --git a/autobot-frontend/src/components/feature-flags/__tests__/FlagChangeHistory.spec.ts b/autobot-frontend/src/components/feature-flags/__tests__/FlagChangeHistory.spec.ts
new file mode 100644
index 000000000..043a771a5
--- /dev/null
+++ b/autobot-frontend/src/components/feature-flags/__tests__/FlagChangeHistory.spec.ts
@@ -0,0 +1,221 @@
+import { describe, it, expect, beforeEach } from 'vitest'
+import { screen } from '@testing-library/vue'
+import FlagChangeHistory from '../FlagChangeHistory.vue'
+import { renderComponent } from '@/test/utils/test-utils'
+import type { EnforcementMode } from '@/utils/FeatureFlagsApiClient'
+
+interface HistoryEntry {
+ timestamp: string
+ mode: EnforcementMode
+ changed_by: string
+}
+
+describe('FlagChangeHistory', () => {
+ const mockHistory: HistoryEntry[] = [
+ {
+ timestamp: new Date(Date.now() - 3600000).toISOString(),
+ mode: 'enforced',
+ changed_by: 'admin@example.com',
+ },
+ {
+ timestamp: new Date(Date.now() - 86400000).toISOString(),
+ mode: 'log_only',
+ changed_by: 'moderator@example.com',
+ },
+ {
+ timestamp: new Date(Date.now() - 604800000).toISOString(),
+ mode: 'disabled',
+ changed_by: 'developer@example.com',
+ },
+ ]
+
+ function renderHistory(props = {}) {
+ return renderComponent(FlagChangeHistory, {
+ props: {
+ history: [],
+ loading: false,
+ ...props,
+ },
+ })
+ }
+
+ describe('Rendering', () => {
+ it('renders the component layout', () => {
+ renderHistory()
+ const container = document.querySelector('.flag-change-history')
+ expect(container).not.toBeNull()
+ })
+
+ it('renders section header with icon and title', () => {
+ renderHistory()
+ const header = document.querySelector('.section-header')
+ expect(header).not.toBeNull()
+
+ const icon = header?.querySelector('i.fa-history')
+ expect(icon).not.toBeNull()
+ })
+ })
+
+ describe('Empty State', () => {
+ it('shows empty state when history is empty', () => {
+ renderHistory({ history: [] })
+ const emptyState = document.querySelector('.empty-state')
+ expect(emptyState).not.toBeNull()
+ })
+
+ it('shows empty state icon', () => {
+ renderHistory({ history: [] })
+ const icon = document.querySelector('.empty-icon i.fa-clock')
+ expect(icon).not.toBeNull()
+ })
+ })
+
+ describe('Loading State', () => {
+ it('shows loading state when loading is true and no history', () => {
+ renderHistory({ loading: true, history: [] })
+ const loadingState = document.querySelector('.loading-state')
+ expect(loadingState).not.toBeNull()
+ })
+
+ it('hides loading state when history is present', () => {
+ renderHistory({ loading: true, history: mockHistory })
+ const loadingState = document.querySelector('.loading-state')
+ expect(loadingState).toBeNull()
+ })
+ })
+
+ describe('Timeline Display', () => {
+ it('renders timeline when history is present', () => {
+ renderHistory({ history: mockHistory })
+ const timeline = document.querySelector('.history-timeline')
+ expect(timeline).not.toBeNull()
+ })
+
+ it('renders correct number of timeline entries', () => {
+ renderHistory({ history: mockHistory })
+ const entries = document.querySelectorAll('.timeline-entry')
+ expect(entries.length).toBe(mockHistory.length)
+ })
+
+ it('applies correct mode class to entries', () => {
+ renderHistory({ history: mockHistory })
+ const entries = document.querySelectorAll('.timeline-entry')
+
+ expect(entries[0]).toHaveClass('enforced')
+ expect(entries[1]).toHaveClass('log_only')
+ expect(entries[2]).toHaveClass('disabled')
+ })
+
+ it('renders timeline markers with correct icons', () => {
+ renderHistory({ history: mockHistory })
+ const markers = document.querySelectorAll('.marker-dot')
+
+ expect(markers[0]).toHaveClass('enforced')
+ expect(markers[1]).toHaveClass('log_only')
+ expect(markers[2]).toHaveClass('disabled')
+ })
+
+ it('renders mode badges for each entry', () => {
+ renderHistory({ history: mockHistory })
+ const badges = document.querySelectorAll('.mode-badge')
+
+ expect(badges.length).toBe(mockHistory.length)
+ expect(badges[0]).toHaveClass('enforced')
+ expect(badges[1]).toHaveClass('log_only')
+ expect(badges[2]).toHaveClass('disabled')
+ })
+
+ it('renders timeline lines between entries but not after last', () => {
+ renderHistory({ history: mockHistory })
+ const lines = document.querySelectorAll('.marker-line')
+
+ expect(lines.length).toBe(mockHistory.length - 1)
+ })
+ })
+
+ describe('Entry Details', () => {
+ it('renders changed_by information in each entry', () => {
+ renderHistory({ history: mockHistory })
+ const changedByElements = document.querySelectorAll('.changed-by')
+
+ expect(changedByElements.length).toBe(mockHistory.length)
+ expect(changedByElements[0]).toHaveTextContent('admin@example.com')
+ expect(changedByElements[1]).toHaveTextContent('moderator@example.com')
+ expect(changedByElements[2]).toHaveTextContent('developer@example.com')
+ })
+
+ it('renders relative time for recent changes', () => {
+ renderHistory({ history: mockHistory })
+ const relativeTimes = document.querySelectorAll('.relative-time')
+
+ expect(relativeTimes.length).toBe(mockHistory.length)
+ // First entry should show "about 1 hour ago" or similar
+ expect(relativeTimes[0].textContent).toBeTruthy()
+ })
+
+ it('shows system author when changed_by is empty', () => {
+ const historyWithoutAuthor: HistoryEntry[] = [
+ {
+ timestamp: new Date().toISOString(),
+ mode: 'enforced',
+ changed_by: '',
+ },
+ ]
+
+ renderHistory({ history: historyWithoutAuthor })
+ const changedBy = document.querySelector('.changed-by')
+ expect(changedBy?.textContent).toContain('system')
+ })
+ })
+
+ describe('Legend', () => {
+ it('shows legend when history is present', () => {
+ renderHistory({ history: mockHistory })
+ const legend = document.querySelector('.legend')
+ expect(legend).not.toBeNull()
+ })
+
+ it('hides legend when history is empty', () => {
+ renderHistory({ history: [] })
+ const legend = document.querySelector('.legend')
+ expect(legend).toBeNull()
+ })
+
+ it('renders all three mode legend items', () => {
+ renderHistory({ history: mockHistory })
+ const legendItems = document.querySelectorAll('.legend-item')
+ expect(legendItems.length).toBe(3)
+ })
+
+ it('legend items have correct mode classes', () => {
+ renderHistory({ history: mockHistory })
+ const dots = document.querySelectorAll('.legend-dot')
+
+ expect(dots[0]).toHaveClass('disabled')
+ expect(dots[1]).toHaveClass('log_only')
+ expect(dots[2]).toHaveClass('enforced')
+ })
+ })
+
+ describe('Timestamp Formatting', () => {
+ it('renders formatted timestamps for each entry', () => {
+ renderHistory({ history: mockHistory })
+ const timestamps = document.querySelectorAll('.timestamp')
+
+ expect(timestamps.length).toBe(mockHistory.length)
+ timestamps.forEach((ts) => {
+ expect(ts.textContent).toBeTruthy()
+ // Should contain date/time elements
+ expect(ts.textContent).toMatch(/\d+/)
+ })
+ })
+ })
+
+ describe('Responsive Behavior', () => {
+ it('renders timeline markers (desktop view)', () => {
+ renderHistory({ history: mockHistory })
+ const markers = document.querySelectorAll('.timeline-marker')
+ expect(markers.length).toBe(mockHistory.length)
+ })
+ })
+})
diff --git a/autobot-frontend/src/components/file-browser/FileBrowserHeader.vue b/autobot-frontend/src/components/file-browser/FileBrowserHeader.vue
index 77a30bf67..f621c0518 100644
--- a/autobot-frontend/src/components/file-browser/FileBrowserHeader.vue
+++ b/autobot-frontend/src/components/file-browser/FileBrowserHeader.vue
@@ -12,7 +12,7 @@
{{ $t('fileBrowser.header.newFolder') }}
{
}
.breadcrumb-item {
- @apply flex items-center text-sm bg-none border-none cursor-pointer text-blue-600 hover:text-blue-800 hover:underline p-0;
+ @apply flex items-center text-sm bg-none border-none cursor-pointer hover:underline p-0;
+ color: var(--text-link);
font: inherit;
}
+.breadcrumb-item:hover {
+ color: var(--text-link-hover);
+}
+
.breadcrumb-item .clickable {
- @apply cursor-pointer text-blue-600 hover:text-blue-800 hover:underline bg-none border-none p-0;
+ @apply cursor-pointer hover:underline bg-none border-none p-0;
+ color: var(--text-link);
font: inherit;
}
+.breadcrumb-item .clickable:hover {
+ color: var(--text-link-hover);
+}
+
.breadcrumb-separator {
@apply text-autobot-text-muted mx-1;
}
@@ -91,10 +101,18 @@ const getPathUpTo = (index: number): string => {
}
.path-field {
- @apply flex-1 px-3 py-2 border border-autobot-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500;
+ @apply flex-1 px-3 py-2 border border-autobot-border rounded-md focus:outline-none focus:ring-2;
+ --tw-ring-color: var(--color-primary);
}
.path-go-btn {
- @apply px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500;
+ @apply px-4 py-2 rounded-md focus:outline-none focus:ring-2;
+ background: var(--color-primary);
+ color: var(--text-inverse);
+ --tw-ring-color: var(--color-primary);
+}
+
+.path-go-btn:hover {
+ filter: brightness(1.1);
}
diff --git a/autobot-frontend/src/components/file-browser/FileUpload.vue b/autobot-frontend/src/components/file-browser/FileUpload.vue
index 39e2bb204..9655f989f 100644
--- a/autobot-frontend/src/components/file-browser/FileUpload.vue
+++ b/autobot-frontend/src/components/file-browser/FileUpload.vue
@@ -82,11 +82,21 @@ defineExpose({
}
.visible-file-input {
- @apply flex-1 min-w-[150px] text-sm text-autobot-text-muted py-0 file:mr-2 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100;
+ @apply flex-1 min-w-[150px] text-sm text-autobot-text-muted py-0 file:mr-2 file:py-1 file:px-3 file:rounded-md file:border-0 file:text-sm file:font-medium;
+}
+
+.visible-file-input::file-selector-button {
+ background: var(--color-info-bg);
+ color: var(--color-info);
+}
+
+.visible-file-input:hover::file-selector-button {
+ filter: brightness(1.1);
}
/* Drag and drop styling */
.file-upload-section.drag-over {
- @apply border-blue-400 bg-blue-50;
+ border-color: var(--color-primary);
+ background: var(--color-info-bg);
}
diff --git a/autobot-frontend/src/components/file-browser/index.ts b/autobot-frontend/src/components/file-browser/index.ts
new file mode 100644
index 000000000..fcf6f6626
--- /dev/null
+++ b/autobot-frontend/src/components/file-browser/index.ts
@@ -0,0 +1,16 @@
+/**
+ * AutoBot - AI-Powered Automation Platform
+ * Copyright (c) 2025 mrveiss
+ * Author: mrveiss
+ *
+ * File Browser Components Index
+ * Export all file browser components for easy imports
+ */
+
+export { default as FileBrowser } from './FileBrowser.vue'
+export { default as FileBrowserHeader } from './FileBrowserHeader.vue'
+export { default as FileListTable } from './FileListTable.vue'
+export { default as FilePathNavigation } from './FilePathNavigation.vue'
+export { default as FilePreview } from './FilePreview.vue'
+export { default as FileTreeView } from './FileTreeView.vue'
+export { default as FileUpload } from './FileUpload.vue'
diff --git a/autobot-frontend/src/components/knowledge/BulkActionsToolbar.vue b/autobot-frontend/src/components/knowledge/BulkActionsToolbar.vue
index 109d616ad..11523cfde 100644
--- a/autobot-frontend/src/components/knowledge/BulkActionsToolbar.vue
+++ b/autobot-frontend/src/components/knowledge/BulkActionsToolbar.vue
@@ -154,7 +154,7 @@ function handleClickOutside(event: MouseEvent): void {
-
+
{{ $t('knowledge.changeFeed.clearHistory') }}
-
+
{{ $t('knowledge.changeFeed.exportChanges') }}
diff --git a/autobot-frontend/src/components/knowledge/FailedVectorizationsManager.vue b/autobot-frontend/src/components/knowledge/FailedVectorizationsManager.vue
index 606daf43d..1b959c5e4 100644
--- a/autobot-frontend/src/components/knowledge/FailedVectorizationsManager.vue
+++ b/autobot-frontend/src/components/knowledge/FailedVectorizationsManager.vue
@@ -159,11 +159,11 @@ const retryJob = async (jobId: string) => {
logger.debug(`Job ${jobId} retry started as ${data.new_job_id}`)
} else {
- error.value = new Error(`Failed to retry job: ${data.message || 'Unknown error'}`)
+ throw new Error(`Failed to retry job: ${data.message || 'Unknown error'}`)
}
} catch (err) {
logger.error('Error retrying job:', err)
- error.value = new Error(`Error retrying job: ${err}`)
+ throw err instanceof Error ? err : new Error(`Error retrying job: ${err}`)
} finally {
retryingJobs.value.delete(jobId)
}
diff --git a/autobot-frontend/src/components/knowledge/KnowledgeBrowser.vue b/autobot-frontend/src/components/knowledge/KnowledgeBrowser.vue
index d96b5b7be..7fa047bbd 100644
--- a/autobot-frontend/src/components/knowledge/KnowledgeBrowser.vue
+++ b/autobot-frontend/src/components/knowledge/KnowledgeBrowser.vue
@@ -1,51 +1,13 @@
-
-
-
-
-
-
-
{{ mainCat.name }}
-
{{ mainCat.description }}
-
- {{ mainCat.count }} {{ $t('knowledge.browser.facts') }}
-
-
-
- {{ $t('knowledge.browser.populate') }}
- {{ populationStates[mainCat.id]?.progress || 0 }}%
-
-
-
-
- {{ $t('knowledge.browser.import') }}
-
-
-
-
-
+
router.push('/knowledge/upload')"
+ />