feat: update source files, Dockerfile, vite config and service worker

This commit is contained in:
kcar 2026-02-21 16:25:42 +00:00
parent 5e5aad54cb
commit 5c100a015d
11 changed files with 287 additions and 275 deletions

View File

@ -1,4 +1,4 @@
FROM node:20 as builder FROM node:20 AS builder
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
# TODO: Remove this or review embedded variables # TODO: Remove this or review embedded variables
@ -20,19 +20,24 @@ RUN echo 'server { \
root /usr/share/nginx/html; \ root /usr/share/nginx/html; \
index index.html; \ index index.html; \
location / { \ location / { \
try_files $uri $uri/ /index.html; \ try_files $uri $uri/ /index.html; \
expires 30d; \ expires 30d; \
add_header Cache-Control "public, no-transform"; \ add_header Cache-Control "public, no-transform"; \
} \ } \
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \
expires 30d; \ expires 30d; \
add_header Cache-Control "public, no-transform"; \ add_header Cache-Control "public, no-transform"; \
} \
location /searxng-api/ { \
proxy_pass https://search.kevlarai.com/; \
proxy_ssl_server_name on; \
proxy_set_header Host search.kevlarai.com; \
} \ } \
location ~ /\. { \ location ~ /\. { \
deny all; \ deny all; \
} \ } \
error_page 404 /index.html; \ error_page 404 /index.html; \
}' > /etc/nginx/conf.d/default.conf }' > /etc/nginx/conf.d/default.conf
# Set up permissions # Set up permissions
RUN chown -R nginx:nginx /usr/share/nginx/html \ RUN chown -R nginx:nginx /usr/share/nginx/html \

View File

@ -57,14 +57,6 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.15.0", "@eslint/js": "^9.15.0",
"@storybook/addon-actions": "^8.6.12",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-links": "^8.6.12",
"@storybook/addon-onboarding": "^8.6.12",
"@storybook/react": "^8.6.12",
"@storybook/react-vite": "^8.6.12",
"@storybook/testing-library": "^0.2.2",
"@testing-library/jest-dom": "^6.4.5", "@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/react": "^18.2.66", "@types/react": "^18.2.66",
@ -90,7 +82,6 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"react-refresh": "^0.14.2", "react-refresh": "^0.14.2",
"storybook": "^8.6.12",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite-plugin-pwa": "^0.21.1", "vite-plugin-pwa": "^0.21.1",

View File

@ -8,6 +8,9 @@ import { DatabaseNameService } from '../services/graph/databaseNameService';
import { provisionUser } from '../services/provisioningService'; import { provisionUser } from '../services/provisioningService';
import { storageService, StorageKeys } from '../services/auth/localStorageService'; import { storageService, StorageKeys } from '../services/auth/localStorageService';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export interface UserContextType { export interface UserContextType {
user: CCUser | null; user: CCUser | null;
loading: boolean; loading: boolean;
@ -29,9 +32,9 @@ export const UserContext = createContext<UserContextType>({
preferences: {}, preferences: {},
isMobile: false, isMobile: false,
isInitialized: false, isInitialized: false,
updateProfile: async () => {}, updateProfile: async () => { },
updatePreferences: async () => {}, updatePreferences: async () => { },
clearError: () => {} clearError: () => { }
}); });
export const UserProvider = ({ children }: { children: React.ReactNode }) => { export const UserProvider = ({ children }: { children: React.ReactNode }) => {
@ -110,7 +113,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
email: userInfo.email email: userInfo.email
}); });
let profileRow: Record<string, unknown> | null = null; let profileRow: Record<string, unknown> | null = null;
logger.debug('user-context', '🔧 Step 5: Querying profiles table...', { logger.debug('user-context', '🔧 Step 5: Querying profiles table...', {
userId: userInfo.id userId: userInfo.id
@ -131,9 +134,9 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => {
queryStarted: true queryStarted: true
}); });
const { data, error } = await fetch(`http://localhost:8000/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, { const { data, error } = await fetch(`${supabaseUrl}/rest/v1/profiles?select=*&id=eq.${userInfo.id}`, {
headers: { headers: {
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0`, 'Authorization': `Bearer ${supabaseAnonKey}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}) })

View File

@ -110,7 +110,8 @@ export type LogCategory =
| 'auth-service' | 'auth-service'
| 'user-context' | 'user-context'
| 'neo-user-context' | 'neo-user-context'
| 'neo-institute-context'; | 'neo-institute-context'
| 'search-service';
interface LogConfig { interface LogConfig {
enabled: boolean; // Master switch to turn logging on/off enabled: boolean; // Master switch to turn logging on/off
@ -338,6 +339,7 @@ logger.setConfig({
'user-context', 'user-context',
'neo-user-context', 'neo-user-context',
'neo-institute-context', 'neo-institute-context',
'search-service'
], ],
}); });

View File

@ -823,7 +823,7 @@ const SimpleUploadTest: React.FC = () => {
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2"> <Typography variant="body2">
<strong>{directoryStats.fileCount} files</strong> in{' '} <strong>{directoryStats.fileCount} files</strong> in{' '}
<strong>{directoryStats.directoryCount} folders</strong><br/> <strong>{directoryStats.directoryCount} folders</strong><br />
Total size: <strong>{directoryStats.formattedSize}</strong> Total size: <strong>{directoryStats.formattedSize}</strong>
</Typography> </Typography>
</Alert> </Alert>
@ -843,12 +843,12 @@ const SimpleUploadTest: React.FC = () => {
size="small" size="small"
color={ color={
item.status === 'done' ? 'success' : item.status === 'done' ? 'success' :
item.status === 'error' ? 'error' : item.status === 'error' ? 'error' :
item.status === 'uploading' ? 'primary' : 'default' item.status === 'uploading' ? 'primary' : 'default'
} }
icon={ icon={
item.status === 'done' ? <SuccessIcon /> : item.status === 'done' ? <SuccessIcon /> :
item.status === 'error' ? <ErrorIcon /> : undefined item.status === 'error' ? <ErrorIcon /> : undefined
} }
/> />
</Box> </Box>

View File

@ -1,3 +1,5 @@
import logger from "../../debugConfig"
interface SearXNGResult { interface SearXNGResult {
title?: string title?: string
url?: string url?: string
@ -26,19 +28,25 @@ export class SearchService {
engines: 'google,bing,duckduckgo', engines: 'google,bing,duckduckgo',
}) })
const url = `${import.meta.env.VITE_FRONTEND_SITE_URL}/searxng-api/search?${searchParams.toString()}` const url = `/searxng-api/search?${searchParams.toString()}`
logger.debug('search-service', "Search URL: ", url)
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
}, },
}) })
logger.debug('search-service', "Search response: ", response)
if (!response.ok) { if (!response.ok) {
throw new Error(`Search failed with status: ${response.status}`) throw new Error(`Search failed with status: ${response.status}`)
} }
const data = await response.json() const data = await response.json()
logger.debug('search-service', "Search data: ", data)
return (data.results || []).map((result: SearXNGResult) => ({ return (data.results || []).map((result: SearXNGResult) => ({
title: result.title || '', title: result.title || '',
url: result.url || '', url: result.url || '',

View File

@ -95,8 +95,8 @@ const navigationHandler = async ({ request }: { request: Request }): Promise<Res
// Try both root and /index.html paths // Try both root and /index.html paths
const cachedResponse = await cache.match(request) || const cachedResponse = await cache.match(request) ||
await cache.match('/') || await cache.match('/') ||
await cache.match('/index.html'); await cache.match('/index.html');
if (cachedResponse) { if (cachedResponse) {
return cachedResponse; return cachedResponse;
@ -169,9 +169,9 @@ registerRoute(
({ request }) => { ({ request }) => {
const destination = request.destination; const destination = request.destination;
return destination === 'style' || return destination === 'style' ||
destination === 'script' || destination === 'script' ||
destination === 'image' || destination === 'image' ||
destination === 'font' destination === 'font'
}, },
new CacheFirst({ new CacheFirst({
cacheName: CACHE_NAMES.static, cacheName: CACHE_NAMES.static,
@ -209,7 +209,7 @@ registerRoute(
// Cache SearXNG API with Network First strategy // Cache SearXNG API with Network First strategy
registerRoute( registerRoute(
({ url }) => url.pathname.startsWith('/searxng-api'), ({ url }) => url.pathname.startsWith('/searxng-api/'),
new NetworkFirst({ new NetworkFirst({
cacheName: CACHE_NAMES.api, cacheName: CACHE_NAMES.api,
plugins: [ plugins: [
@ -227,7 +227,7 @@ registerRoute(
// Cache SearXNG static assets // Cache SearXNG static assets
registerRoute( registerRoute(
({ url }) => url.pathname.startsWith('/searxng-api/static'), ({ url }) => url.pathname.startsWith('/searxng-api/static/'),
new CacheFirst({ new CacheFirst({
cacheName: CACHE_NAMES.static, cacheName: CACHE_NAMES.static,
plugins: [ plugins: [

View File

@ -25,7 +25,7 @@ export const CCCabinetsPanel: React.FC = () => {
return createTheme({ palette: { mode, divider: 'var(--color-divider)' } }); return createTheme({ palette: { mode, divider: 'var(--color-divider)' } });
}, [tldrawPreferences?.colorScheme, prefersDarkMode]); }, [tldrawPreferences?.colorScheme, prefersDarkMode]);
const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined; type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | undefined;
const apiFetch = async (url: string, init?: RequestInitLite) => { const apiFetch = async (url: string, init?: RequestInitLite) => {
@ -75,7 +75,7 @@ export const CCCabinetsPanel: React.FC = () => {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Box sx={{ p: 1, height: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}> <Box sx={{ p: 1, height: '100%', display: 'flex', flexDirection: 'column', gap: 1 }}>
<Toolbar> <Toolbar>
<Button size="small" variant="outlined" startIcon={<AddIcon/>} onClick={() => { setNewName(''); setCreateOpen(true); }}>New Cabinet</Button> <Button size="small" variant="outlined" startIcon={<AddIcon />} onClick={() => { setNewName(''); setCreateOpen(true); }}>New Cabinet</Button>
</Toolbar> </Toolbar>
<Grid container spacing={1} sx={{ overflow: 'auto' }}> <Grid container spacing={1} sx={{ overflow: 'auto' }}>
{cabinets.map(c => ( {cabinets.map(c => (

View File

@ -139,7 +139,7 @@ export const CCFilesPanel: React.FC = () => {
type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined; type RequestInitLike = { method?: string; body?: FormData | string | Blob | null; headers?: Record<string, string> } | undefined;
type HeadersInitLike = Record<string, string>; type HeadersInitLike = Record<string, string>;
const API_BASE: string = (import.meta as unknown as { env?: { VITE_API_BASE?: string } })?.env?.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api');
const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => { const apiFetch = useCallback(async (url: string, init?: RequestInitLike) => {
const headers: HeadersInitLike = { const headers: HeadersInitLike = {
@ -387,10 +387,10 @@ export const CCFilesPanel: React.FC = () => {
const iconForMime = (mime?: string, isDirectory?: boolean) => { const iconForMime = (mime?: string, isDirectory?: boolean) => {
if (isDirectory) return <FolderIcon />; if (isDirectory) return <FolderIcon />;
if (!mime) return <InsertDriveFileIcon/>; if (!mime) return <InsertDriveFileIcon />;
if (mime.startsWith('image/')) return <ImageIcon/>; if (mime.startsWith('image/')) return <ImageIcon />;
if (mime === 'application/pdf' || mime.startsWith('application/')) return <DescriptionIcon/>; if (mime === 'application/pdf' || mime.startsWith('application/')) return <DescriptionIcon />;
return <InsertDriveFileIcon/>; return <InsertDriveFileIcon />;
}; };
const formatFileSize = (bytes: number): string => { const formatFileSize = (bytes: number): string => {
@ -565,7 +565,7 @@ export const CCFilesPanel: React.FC = () => {
}}> }}>
{loading ? ( {loading ? (
<Box sx={{ p: 2, textAlign: 'center' }}> <Box sx={{ p: 2, textAlign: 'center' }}>
<CircularProgress size={20}/> <CircularProgress size={20} />
<Typography variant="caption" display="block" sx={{ mt: 1 }}> <Typography variant="caption" display="block" sx={{ mt: 1 }}>
Loading files... Loading files...
</Typography> </Typography>
@ -593,10 +593,10 @@ export const CCFilesPanel: React.FC = () => {
secondaryAction={ secondaryAction={
<> <>
<IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions"> <IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions">
<MoreVertIcon/> <MoreVertIcon />
</IconButton> </IconButton>
<IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file"> <IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file">
<DeleteIcon/> <DeleteIcon />
</IconButton> </IconButton>
</> </>
} }
@ -632,10 +632,10 @@ export const CCFilesPanel: React.FC = () => {
secondaryAction={ secondaryAction={
<> <>
<IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions"> <IconButton size="small" onClick={(e) => openMenu(e.currentTarget, f.id)} title="File actions">
<MoreVertIcon/> <MoreVertIcon />
</IconButton> </IconButton>
<IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file"> <IconButton edge="end" size="small" onClick={() => handleDelete(f.id)} title="Delete file">
<DeleteIcon/> <DeleteIcon />
</IconButton> </IconButton>
</> </>
} }
@ -769,7 +769,7 @@ export const CCFilesPanel: React.FC = () => {
{artefacts.length > 0 && ( {artefacts.length > 0 && (
<> <>
<Divider/> <Divider />
<List dense sx={{ <List dense sx={{
border: '1px solid var(--color-divider)', border: '1px solid var(--color-divider)',
borderRadius: '4px', borderRadius: '4px',
@ -810,7 +810,7 @@ export const CCFilesPanel: React.FC = () => {
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2"> <Typography variant="body2">
<strong>{directoryStats.fileCount} files</strong> in{' '} <strong>{directoryStats.fileCount} files</strong> in{' '}
<strong>{directoryStats.directoryCount} folders</strong><br/> <strong>{directoryStats.directoryCount} folders</strong><br />
Total size: <strong>{directoryStats.formattedSize}</strong> Total size: <strong>{directoryStats.formattedSize}</strong>
</Typography> </Typography>
</Alert> </Alert>

View File

@ -87,7 +87,7 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
swDest: 'dist/sw.js', swDest: 'dist/sw.js',
manifestTransforms: [ manifestTransforms: [
// Transform manifest entries to ensure proper caching // Transform manifest entries to ensure proper caching
(entries) => ({ (entries: any[]) => ({
manifest: entries.map(entry => ({ manifest: entries.map(entry => ({
...entry, ...entry,
url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}` url: entry.url.startsWith(base) ? entry.url : `${base}${entry.url.startsWith('/') ? entry.url.slice(1) : entry.url}`
@ -106,6 +106,8 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
'import.meta.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY), 'import.meta.env.VITE_SUPABASE_ANON_KEY': JSON.stringify(env.VITE_SUPABASE_ANON_KEY),
'import.meta.env.VITE_SUPER_ADMIN_EMAIL': JSON.stringify(env.VITE_SUPER_ADMIN_EMAIL), 'import.meta.env.VITE_SUPER_ADMIN_EMAIL': JSON.stringify(env.VITE_SUPER_ADMIN_EMAIL),
'import.meta.env.VITE_DEV': env.VITE_DEV === 'true', 'import.meta.env.VITE_DEV': env.VITE_DEV === 'true',
'import.meta.env.VITE_API_BASE': JSON.stringify(env.VITE_API_BASE),
'import.meta.env.VITE_SEARCH_URL': JSON.stringify(env.VITE_SEARCH_URL),
}, },
envPrefix: 'VITE_', envPrefix: 'VITE_',
base, base,
@ -125,6 +127,13 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
port: parseInt(env.VITE_PORT_FRONTEND || '5173'), port: parseInt(env.VITE_PORT_FRONTEND || '5173'),
clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'), clientPort: parseInt(env.VITE_PORT_FRONTEND_HMR || '5173'),
overlay: false overlay: false
},
proxy: {
'/searxng-api': {
target: env.VITE_SEARCH_URL,
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/searxng-api/, '')
}
} }
}, },
clearScreen: false, clearScreen: false,
@ -143,12 +152,6 @@ export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> =>
} : undefined, } : undefined,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-mui': ['@mui/material', '@mui/icons-material'],
'vendor-tldraw': ['@tldraw/tldraw', '@tldraw/store', '@tldraw/tlschema'],
'vendor-utils': ['axios', 'zustand', '@supabase/supabase-js']
},
// Ensure chunk filenames include content hash // Ensure chunk filenames include content hash
chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js', chunkFileNames: isProd ? 'assets/[name].[hash].js' : 'assets/[name].js',
assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]' assetFileNames: isProd ? 'assets/[name].[hash][extname]' : 'assets/[name][extname]'