feat(frontend): enhance API credentials system and build configuration

Add real API support in demo mode with credential checking, implement build-time version injection from package.json, and refactor update checking with 24-hour caching. Migrate landing page from Vue to Astro with comprehensive UI components including Hero, Features, Benefits, and Tech Stack sections. Update CI/CD workflow with expanded cache paths and security scanner version pinned.
This commit is contained in:
Tomas Dvorak
2026-02-10 16:25:57 +01:00
parent d27cf14110
commit b083dac3f0
95 changed files with 17610 additions and 2692 deletions
@@ -4,7 +4,8 @@ import { type BraveSearchResult } from '@/lib/brave-search';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { isEnvDemoMode } from '@/lib/demo-mode';
import { isEnvDemoMode, shouldUseRealSearch } from '@/lib/demo-mode';
import { getSearchProvider, getApiBaseUrl } from '@/lib/credentials';
export const BrowserSearch = () => {
const [searchQuery, setSearchQuery] = createSignal('');
@@ -15,10 +16,15 @@ export const BrowserSearch = () => {
const [searchType, setSearchType] = createSignal<'web' | 'news'>('web');
// Check if we're in demo mode
const isDemoMode = () => {
const isDemo = () => {
return isEnvDemoMode();
};
// Check if we should use real search APIs
const shouldUseReal = () => {
return shouldUseRealSearch();
};
const handleSearch = async () => {
const query = searchQuery().trim();
if (!query) return;
@@ -28,12 +34,65 @@ export const BrowserSearch = () => {
setHasSearched(true);
try {
const isDemo = isDemoMode();
const isDemoMode = isDemo();
const useRealAPIs = shouldUseReal();
// In demo mode, use the demo mode API interceptor
if (isDemo) {
console.log(`[BrowserSearch] Demo mode: ${isDemoMode}, Use real APIs: ${useRealAPIs}`);
// If we have credentials and should use real APIs, try them first
if (useRealAPIs) {
console.log('Using real search APIs...');
// Try the configured search provider first
const searchProvider = getSearchProvider();
console.log(`Using search provider: ${searchProvider}`);
if (searchProvider === 'brave' && import.meta.env.VITE_BRAVE_API_KEY) {
try {
const { searchBrave } = await import('@/lib/brave-search');
const results = await searchBrave(query, 8, searchType());
if (results && results.length > 0) {
setSearchResults(results);
return;
}
} catch (err) {
console.warn('Brave Search failed:', err);
}
}
// Try backend as fallback
const API_BASE_URL = getApiBaseUrl();
const token = localStorage.getItem('token') ||
localStorage.getItem('auth_token') ||
localStorage.getItem('trackeep_token');
const endpoint = searchType() === 'news' ? '/api/v1/search/news' : '/api/v1/search/web';
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
},
body: JSON.stringify({ query, count: 8 }),
});
if (response.ok) {
const data = await response.json();
if (data.results && data.results.length > 0) {
setSearchResults(data.results);
return;
}
}
} catch (err) {
console.warn('Backend search failed:', err);
}
}
// In demo mode or as fallback, use the demo mode API interceptor
if (isDemoMode) {
console.log('Demo mode detected, using demo API interceptor...');
const API_BASE_URL = import.meta.env.VITE_API_URL?.replace('/api/v1', '') || 'http://localhost:8080';
const API_BASE_URL = getApiBaseUrl();
const endpoint = searchType() === 'news' ? '/api/v1/search/news' : '/api/v1/search/web';
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
@@ -53,43 +112,7 @@ export const BrowserSearch = () => {
return;
}
}
console.warn('Demo API failed, falling back to direct Brave API...');
}
// Try Brave Search API directly (for production mode or as fallback)
const { searchBrave } = await import('@/lib/brave-search');
const results = await searchBrave(query, 8, searchType());
if (results && results.length > 0) {
setSearchResults(results);
return;
}
// If no results from Brave API, try backend as last resort (only in non-demo mode)
if (!isDemo) {
console.warn('Brave Search returned no results, trying backend...');
const API_BASE_URL = import.meta.env.VITE_API_URL?.replace('/api/v1', '') || 'http://localhost:8080';
const token = localStorage.getItem('token') ||
localStorage.getItem('auth_token') ||
localStorage.getItem('trackeep_token');
const endpoint = searchType() === 'news' ? '/api/v1/search/news' : '/api/v1/search/web';
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
},
body: JSON.stringify({ query, count: 8 }),
});
if (response.ok) {
const data = await response.json();
if (data.results && data.results.length > 0) {
setSearchResults(data.results);
return;
}
}
console.warn('Demo API failed, falling back to mock results...');
}
// If all APIs fail or return no results, show appropriate message
@@ -148,14 +171,14 @@ export const BrowserSearch = () => {
const bookmarkResult = async (result: BraveSearchResult) => {
// If in demo mode, just show success message
if (isDemoMode()) {
if (isDemo()) {
// In demo mode, just show success without actual API call
console.log('Demo mode: Bookmark created for', result.title);
return;
}
try {
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
const API_BASE_URL = getApiBaseUrl();
const bookmarkData = {
title: result.title,
url: result.url,
+1 -8
View File
@@ -1,7 +1,4 @@
import { createSignal, For, onMount, createEffect, Show } from 'solid-js';
import { useDebounce } from '@/hooks/useDebounce';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { cn } from '@/lib/utils';
import { createSignal, For, onMount, createEffect } from 'solid-js';
import {
IconBookmark,
IconChecklist,
@@ -42,10 +39,6 @@ export const ActivityFeed = (props: ActivityFeedProps) => {
const [activities, setActivities] = createSignal<ActivityItem[]>([]);
const [filter, setFilter] = createSignal<'all' | 'trackeep' | 'github'>('all');
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
// Debounce filter changes to prevent excessive re-renders
const debouncedFilter = useDebounce(filter, 300);
const getActivityIcon = (type: string) => {
switch (type) {
+1 -1
View File
@@ -55,7 +55,7 @@ export const BookmarkModal = (props: BookmarkModalProps) => {
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
{/* Modal */}
@@ -74,7 +74,7 @@ export const EditBookmarkModal = (props: EditBookmarkModalProps) => {
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
{/* Modal */}
+1 -1
View File
@@ -1,4 +1,4 @@
import { createSignal, For, Show } from 'solid-js';
import { createSignal } from 'solid-js';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { RichTextEditor } from '@/components/ui/RichTextEditor';
+1 -1
View File
@@ -1,5 +1,5 @@
import { createSignal, For, Show, createEffect } from 'solid-js';
import { IconTag, IconX, IconChevronDown } from '@tabler/icons-solidjs';
import { IconTag, IconX } from '@tabler/icons-solidjs';
interface TagPickerProps {
availableTags: string[];
+1 -1
View File
@@ -81,7 +81,7 @@ export const TaskModal = (props: TaskModalProps) => {
<>
{/* Backdrop */}
{props.isOpen && (
<div class="fixed inset-0 bg-black/50 z-40" onClick={props.onClose} />
<div class="fixed inset-0 bg-black/50 z-40 mt-0" onClick={props.onClose} />
)}
{/* Modal */}
+1 -1
View File
@@ -1,4 +1,4 @@
import { createSignal, createEffect, onCleanup, Show } from 'solid-js';
import { createSignal, createEffect, onCleanup } from 'solid-js';
import { IconX, IconCheck, IconAlertTriangle, IconInfoCircle } from '@tabler/icons-solidjs';
interface Toast {
+23 -6
View File
@@ -39,6 +39,9 @@ export function UpdateChecker(props: UpdateCheckerProps) {
setUpdateInfo(response.updateInfo || null);
setCurrentVersion(response.currentVersion);
// Save last check time
localStorage.setItem('lastUpdateCheck', Date.now().toString());
if (response.updateAvailable && response.updateInfo) {
setUpdateStatus(prev => ({ ...prev, available: true }));
}
@@ -96,14 +99,22 @@ export function UpdateChecker(props: UpdateCheckerProps) {
};
onMount(() => {
// Check for updates on component mount
checkForUpdates();
// Set current version
setCurrentVersion(updateService.getCurrentVersion());
// Check for updates periodically (every 30 minutes)
const intervalId = setInterval(checkForUpdates, 30 * 60 * 1000);
// Check for updates periodically (every 24 hours)
const intervalId = setInterval(checkForUpdates, 24 * 60 * 60 * 1000);
// Check if last check was more than 24 hours ago
const lastCheckTime = localStorage.getItem('lastUpdateCheck');
const now = Date.now();
const twentyFourHours = 24 * 60 * 60 * 1000;
if (!lastCheckTime || (now - parseInt(lastCheckTime)) > twentyFourHours) {
// Check for updates on component mount if it's been more than 24 hours
checkForUpdates();
localStorage.setItem('lastUpdateCheck', now.toString());
}
onCleanup(() => {
clearInterval(intervalId);
@@ -134,7 +145,13 @@ export function UpdateChecker(props: UpdateCheckerProps) {
return (
<>
<div class={`flex items-center gap-2 ${props.class || ''}`}>
<div class={`flex flex-col gap-2 ${props.class || ''}`}>
{/* Current Version Display */}
<div class="text-xs text-muted-foreground px-2 text-center">
Version {currentVersion()}
</div>
{/* Check Updates Button */}
<button
onClick={() => updateAvailable() ? setShowUpdateModal(true) : checkForUpdates()}
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate relative overflow-hidden w-full"