import { createSignal, onMount, For, Show } from 'solid-js'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { SearchTagFilterBar } from '@/components/ui/SearchTagFilterBar'; import { FileUpload } from '@/components/ui/FileUpload'; import { FilePreviewModal } from '@/components/ui/FilePreviewModal'; import { getFileTypeConfig, formatFileSize, getFileCategoryColor } from '@/utils/fileTypes'; import { getApiV1BaseUrl } from '@/lib/api-url'; import { startGitHubSignIn } from '@/lib/oauth'; import { isDemoMode } from '@/lib/demo-mode'; import { getMockDocuments } from '@/lib/mockData'; import { IconUpload, IconEye, IconTrash, IconDownload, IconCopy, IconShare, IconBrandGithub, IconRefresh, IconGitFork, IconFolder, IconExternalLink, IconCheck, IconAlertTriangle, } from '@tabler/icons-solidjs'; import { useHaptics } from '@/lib/haptics'; const API_BASE_URL = getApiV1BaseUrl(); interface FileItem { id: number; name: string; size: number; type: string; uploadedAt: string; description?: string; tags: string[]; associations?: Association[]; url?: string; isLink?: boolean; preview?: string; downloadUrl?: string; viewUrl?: string; shareUrl?: string; } interface Association { id: string; type: 'task' | 'bookmark' | 'note' | 'project'; title: string; } interface GitHubRepo { id: number; name: string; full_name: string; description: string; html_url: string; stargazers_count: number; forks_count: number; watchers_count: number; language: string; updated_at: string; created_at: string; size: number; open_issues_count: number; default_branch: string; private?: boolean; } interface GitHubAppInstallation { installation_id: number; account_login: string; account_type: string; } interface GitHubAppStatus { app_slug: string; install_enabled: boolean; sign_in_configured: boolean; credentials_configured: boolean; installed: boolean; installation?: GitHubAppInstallation; } interface GitHubBackupRecord { id: number; repository_full_name: string; local_path: string; source: string; last_backup_status: string; last_backup_error?: string; last_backup_at?: string; last_backup_size: number; } interface GitHubBackupResponse { backed_up: number; failed: number; } type FilesTab = 'files' | 'github-backups'; const defaultGitHubAppStatus: GitHubAppStatus = { app_slug: '', install_enabled: false, sign_in_configured: false, credentials_configured: false, installed: false, }; export const Files = () => { const haptics = useHaptics(); const [activeTab, setActiveTab] = createSignal('files'); const [files, setFiles] = createSignal([]); const [isLoading, setIsLoading] = createSignal(true); const [searchTerm, setSearchTerm] = createSignal(''); const [selectedTags, setSelectedTags] = createSignal([]); const [showUploadModal, setShowUploadModal] = createSignal(false); const [showPreviewModal, setShowPreviewModal] = createSignal(false); const [selectedFile, setSelectedFile] = createSignal(null); const [copiedLink, setCopiedLink] = createSignal(false); const [isGitHubLoading, setIsGitHubLoading] = createSignal(true); const [isGitHubActionLoading, setIsGitHubActionLoading] = createSignal(false); const [gitHubError, setGitHubError] = createSignal(''); const [gitHubMessage, setGitHubMessage] = createSignal(''); const [gitHubOAuthConnected, setGitHubOAuthConnected] = createSignal(false); const [gitHubUsername, setGitHubUsername] = createSignal(''); const [gitHubAppStatus, setGitHubAppStatus] = createSignal(defaultGitHubAppStatus); const [gitHubBackupRoot, setGitHubBackupRoot] = createSignal(''); const [gitHubRepos, setGitHubRepos] = createSignal([]); const [gitHubBackups, setGitHubBackups] = createSignal([]); const [selectedRepos, setSelectedRepos] = createSignal([]); onMount(async () => { await Promise.all([loadFiles(), loadGitHubBackupWorkspace()]); }); const getAuthToken = () => localStorage.getItem('trackeep_token') || localStorage.getItem('token') || ''; const parseRepoPayload = (payload: unknown): GitHubRepo[] => { if (Array.isArray(payload)) { return payload as GitHubRepo[]; } if (payload && typeof payload === 'object' && Array.isArray((payload as { repos?: unknown[] }).repos)) { return (payload as { repos: GitHubRepo[] }).repos; } return []; }; const fetchWithAuth = async (path: string, init?: RequestInit): Promise => { const token = getAuthToken(); if (!token) { throw new Error('No authentication token'); } const headers = new Headers(init?.headers || {}); headers.set('Authorization', `Bearer ${token}`); return fetch(`${API_BASE_URL}${path}`, { ...init, headers, }); }; const loadFiles = async () => { try { setIsLoading(true); if (isDemoMode()) { const mockDocuments = getMockDocuments(); const mappedFiles: FileItem[] = mockDocuments.map((doc, index) => ({ id: index + 1, name: doc.name, size: parseFloat(doc.size) * 1024, type: doc.type, uploadedAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), description: doc.description, tags: doc.tags.map(tag => tag.name), url: '#', isLink: false, preview: doc.content, downloadUrl: '#', viewUrl: '#', shareUrl: '#', })); setFiles(mappedFiles); return; } const response = await fetchWithAuth('/files'); if (!response.ok) { throw new Error('Failed to load files'); } const filesData = await response.json(); const mappedFiles: FileItem[] = (Array.isArray(filesData) ? filesData : []).map((file: any, index) => ({ id: Number(file.id || index + 1), name: file.original_name || file.file_name || `File ${index + 1}`, size: Number(file.file_size || file.size || 0), type: file.mime_type || file.type || 'application/octet-stream', uploadedAt: file.created_at || file.uploadedAt || new Date().toISOString(), description: file.description, tags: Array.isArray(file.tags) ? file.tags.map((tag: any) => (typeof tag === 'string' ? tag : tag?.name)).filter(Boolean) : [], url: file.url, isLink: Boolean(file.is_link), preview: file.preview, downloadUrl: file.download_url, viewUrl: file.view_url, shareUrl: file.share_url, })); setFiles(mappedFiles); } catch (error) { console.error('Failed to load files:', error); if (isDemoMode()) { const mockDocuments = getMockDocuments(); const mappedFiles: FileItem[] = mockDocuments.map((doc, index) => ({ id: index + 1, name: doc.name, size: parseFloat(doc.size) * 1024, type: doc.type, uploadedAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), description: doc.description, tags: doc.tags.map(tag => tag.name), url: '#', isLink: false, preview: doc.content, downloadUrl: '#', viewUrl: '#', shareUrl: '#', })); setFiles(mappedFiles); } else { setFiles([]); } } finally { setIsLoading(false); } }; const loadGitHubBackupWorkspace = async () => { setIsGitHubLoading(true); setGitHubError(''); try { const token = getAuthToken(); if (!token) { setGitHubOAuthConnected(false); setGitHubUsername(''); setGitHubAppStatus(defaultGitHubAppStatus); setGitHubRepos([]); setGitHubBackups([]); setGitHubBackupRoot(''); setSelectedRepos([]); return; } let githubSignInConnected = false; const meResponse = await fetchWithAuth('/auth/me'); if (meResponse.ok) { const meData = await meResponse.json(); githubSignInConnected = Boolean(meData?.user?.github_id); setGitHubOAuthConnected(githubSignInConnected); setGitHubUsername(typeof meData?.user?.username === 'string' ? meData.user.username : ''); } else { setGitHubOAuthConnected(false); setGitHubUsername(''); } const appStatusResponse = await fetchWithAuth('/github/app/status'); if (appStatusResponse.ok) { const appStatusData = (await appStatusResponse.json()) as GitHubAppStatus; setGitHubAppStatus({ app_slug: appStatusData.app_slug || '', install_enabled: Boolean(appStatusData.install_enabled), sign_in_configured: Boolean(appStatusData.sign_in_configured), credentials_configured: Boolean(appStatusData.credentials_configured), installed: Boolean(appStatusData.installed), installation: appStatusData.installation, }); } else { setGitHubAppStatus(defaultGitHubAppStatus); } const backupsResponse = await fetchWithAuth('/github/backups'); if (backupsResponse.ok) { const backupsData = await backupsResponse.json(); const backups = Array.isArray(backupsData?.backups) ? (backupsData.backups as GitHubBackupRecord[]) : []; setGitHubBackups(backups); setGitHubBackupRoot(typeof backupsData?.backup_root === 'string' ? backupsData.backup_root : ''); } else { setGitHubBackups([]); setGitHubBackupRoot(''); } let reposResponse: Response | null = null; if (githubSignInConnected) { reposResponse = await fetchWithAuth('/github/repos'); } else if (gitHubAppStatus().installed && gitHubAppStatus().credentials_configured) { reposResponse = await fetchWithAuth('/github/app/repos'); } if (reposResponse && reposResponse.ok) { const reposData = await reposResponse.json(); const repos = parseRepoPayload(reposData); setGitHubRepos(repos); setSelectedRepos(prev => prev.filter(fullName => repos.some(repo => repo.full_name === fullName))); } else { setGitHubRepos([]); setSelectedRepos([]); } } catch (error) { console.error('Failed to load GitHub backup workspace:', error); const message = error instanceof Error ? error.message : 'Failed to load GitHub backup data'; setGitHubError(message); } finally { setIsGitHubLoading(false); } }; const handleInstallGitHubApp = async () => { try { setIsGitHubActionLoading(true); setGitHubError(''); const response = await fetchWithAuth('/github/app/install-url'); const data = await response.json().catch(() => ({})); if (!response.ok || !data?.install_url) { const errorMessage = typeof data?.error === 'string' ? data.error : 'Failed to generate install URL'; throw new Error(errorMessage); } const installUrl = data.install_url as string; if (installUrl && (installUrl.startsWith('https://github.com/') || installUrl.startsWith('https://api.github.com/'))) { window.location.href = installUrl; } else { throw new Error('Invalid install URL received'); } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to start GitHub App installation'; setGitHubError(message); haptics.warning(); } finally { setIsGitHubActionLoading(false); } }; const handleBackupSelectedRepos = async () => { try { if (selectedRepos().length === 0) { return; } setIsGitHubActionLoading(true); setGitHubError(''); setGitHubMessage(''); const source = gitHubOAuthConnected() ? 'github_user' : 'github_app'; const response = await fetchWithAuth('/github/backups', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ repositories: selectedRepos(), source, }), }); const data = (await response.json().catch(() => ({}))) as Partial & { error?: string }; if (!response.ok) { throw new Error(data.error || 'Backup failed'); } const backedUp = Number(data.backed_up ?? 0); const failed = Number(data.failed ?? 0); setGitHubMessage(`Backup completed: ${backedUp} successful, ${failed} failed.`); await loadGitHubBackupWorkspace(); haptics.success(); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to backup selected repositories'; setGitHubError(message); haptics.error(); } finally { setIsGitHubActionLoading(false); } }; const filteredFiles = () => { const term = searchTerm().toLowerCase(); const tags = selectedTags(); return files().filter(file => { const matchesSearch = file.name.toLowerCase().includes(term) || file.description?.toLowerCase().includes(term) || file.tags.some(tag => tag.toLowerCase().includes(term)); const matchesTags = tags.length === 0 || tags.every(tag => file.tags.includes(tag)); return matchesSearch && matchesTags; }); }; const allTags = () => { const tagSet = new Set(); files().forEach(file => file.tags.forEach(tag => tagSet.add(tag))); return Array.from(tagSet).sort(); }; const toggleTag = (tag: string) => { const currentTags = selectedTags(); if (currentTags.includes(tag)) { setSelectedTags([]); } else { setSelectedTags([tag]); } }; const handleFileUpload = async (uploadedFiles: any[]) => { try { const newFiles: FileItem[] = uploadedFiles.map(fileData => ({ id: Date.now() + Math.random(), name: fileData.name || 'Untitled', size: fileData.size || 0, type: fileData.type || 'application/octet-stream', uploadedAt: new Date().toISOString(), description: '', tags: [], url: fileData.url, isLink: Boolean(fileData.url), downloadUrl: fileData.url || `/files/download/${Date.now()}`, viewUrl: fileData.url || `/files/view/${Date.now()}`, shareUrl: `/files/share/${Date.now()}`, })); setFiles(prev => [...newFiles, ...prev]); setShowUploadModal(false); haptics.success(); } catch (error) { console.error('Failed to upload files:', error); haptics.error(); } }; const handlePreviewFile = (file: FileItem) => { setSelectedFile(file); setShowPreviewModal(true); }; const handleCopyLink = async (file: FileItem) => { try { const link = file.isLink ? file.url : file.shareUrl || '#'; if (link) { await navigator.clipboard.writeText(link); setCopiedLink(true); setTimeout(() => setCopiedLink(false), 2000); haptics.success(); } } catch (error) { console.error('Failed to copy link:', error); haptics.error(); } }; const handleShareFile = (file: FileItem) => { const shareUrl = file.shareUrl || '#'; if (navigator.share) { navigator.share({ title: file.name, text: file.description, url: shareUrl, }); } else { window.open(shareUrl, '_blank'); } }; const handleDownloadFile = (file: FileItem) => { if (file.isLink && file.url) { window.open(file.url, '_blank'); return; } if (file.downloadUrl) { const link = document.createElement('a'); link.href = file.downloadUrl; link.download = file.name; link.click(); } }; const deleteFile = async (fileId: number) => { try { setFiles(prev => prev.filter(file => file.id !== fileId)); } catch (error) { console.error('Failed to delete file:', error); } }; const isRepoSelected = (fullName: string) => selectedRepos().includes(fullName); const toggleRepoSelection = (fullName: string) => { setSelectedRepos(prev => ( prev.includes(fullName) ? prev.filter(repo => repo !== fullName) : [...prev, fullName] )); }; const selectAllRepos = () => setSelectedRepos(gitHubRepos().map(repo => repo.full_name)); const clearSelectedRepos = () => setSelectedRepos([]); const formatSourceLabel = (source: string) => source === 'github_app' ? 'GitHub App' : 'GitHub Sign-In'; return (

Files

Manage uploads and GitHub repository backups in one place.

<> setSearchTerm(value)} tagOptions={allTags()} selectedTag={selectedTags()[0] || ''} onTagChange={value => setSelectedTags(value ? [value] : [])} onReset={() => { setSearchTerm(''); setSelectedTags([]); }} />
Link copied!
{[...Array(6)].map(() => (
))}
<>
{(file) => { const fileTypeConfig = getFileTypeConfig(file.type, file.name); const IconComponent = fileTypeConfig.icon; return ( handlePreviewFile(file)} >
{fileTypeConfig.displayName} Link

{file.name}

{formatFileSize(file.size)}

{file.description}

{(tag) => ( )}
0}>

Linked to:

{(assoc) => ( {assoc.type}: {assoc.title} )}
{new Date(file.uploadedAt).toLocaleDateString()}
); }}

{searchTerm() || selectedTags().length > 0 ? 'No files found matching your search or filters.' : 'No files uploaded yet. Upload your first file!'}

Repository Storage

GitHub Backups

Select repositories and store mirrored backups locally for resilient archival.

Active account: @{gitHubUsername()}

GitHub Sign-In: {gitHubOAuthConnected() ? 'Connected' : 'Disconnected'} App Install: {gitHubAppStatus().installed ? 'Installed' : 'Not installed'} App Credentials: {gitHubAppStatus().credentials_configured ? 'Ready' : 'Missing'}

GitHub sign-in is not available from the unified Trackeep control service.

Sign in with GitHub first, then install the GitHub App to verify the installation against your account.

{gitHubMessage()}
{gitHubError()}
{[...Array(3)].map(() => (
))}
<>

Available Repositories

{gitHubRepos().length}

Selected for Backup

{selectedRepos().length}

Stored Backups

{gitHubBackups().length}

Repository Selection

Choose repositories to mirror into local backup storage.

0} fallback={

No repositories available. Connect GitHub sign-in or install/configure GitHub App access first.

}>
{(repo) => ( )}

Local Backup Inventory

{gitHubBackupRoot()}
0} fallback={

No repository backups yet. Select repositories above and run your first backup.

}>
{(backup) => (

{backup.repository_full_name}

Source: {formatSourceLabel(backup.source)} • Last run:{' '} {backup.last_backup_at ? new Date(backup.last_backup_at).toLocaleString() : 'N/A'}

{backup.last_backup_status}
Size: {formatFileSize(backup.last_backup_size || 0)} Path: {backup.local_path}

{backup.last_backup_error}

)}
setShowUploadModal(false)} onFilesChange={handleFileUpload} maxFileSize={50} acceptedTypes={['image/jpeg', 'image/png', 'application/pdf', 'video/mp4']} /> setShowPreviewModal(false)} file={selectedFile()} />
); };