mission-control/hooks/useDocuments.ts
OpenClaw Bot 95060930b1 feat: Add machine token auth for Mission Control CLI
- Add mc_api_call_machine() function for MC_MACHINE_TOKEN auth
- Update mc_api_call() to use machine token when available
- Allows cron jobs to authenticate without cookie-based login
- No breaking changes - cookie auth still works for interactive use
- Also updates default API URL to production (was localhost)
2026-02-26 08:31:14 -06:00

249 lines
6.8 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { Document, Folder, DocumentType, DEFAULT_FOLDERS } from '@/types/documents';
const generateId = () => Math.random().toString(36).substring(2, 9);
// Transform Supabase row to Document type
const transformRowToDocument = (row: any): Document => ({
id: row.id,
title: row.title,
content: row.content,
type: row.type as DocumentType,
folder: row.folder || 'General',
tags: row.tags || [],
createdAt: row.created_at,
updatedAt: row.updated_at,
size: row.size || 0,
description: row.description,
});
const getInitialFolders = (): Folder[] =>
DEFAULT_FOLDERS.map((name) => ({
id: generateId(),
name,
count: 0,
}));
// Helper to fetch with timeout
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
export function useDocuments() {
const [documents, setDocuments] = useState<Document[]>([]);
const [folders, setFolders] = useState<Folder[]>([]);
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load from API on mount
useEffect(() => {
let isMounted = true;
async function loadDocuments() {
try {
console.log('[useDocuments] Starting fetch...');
const response = await fetchWithTimeout('/api/documents', {
headers: {
'Cache-Control': 'no-cache',
},
cache: 'no-store',
}, 10000);
console.log('[useDocuments] Response status:', response.status);
// Try to parse JSON even if response is not ok (to get error details)
let result;
try {
result = await response.json();
} catch (parseErr) {
throw new Error(`Failed to parse response: ${response.statusText}`);
}
console.log('[useDocuments] API response:', result);
if (!response.ok) {
throw new Error(result.error || `HTTP error! status: ${response.status}`);
}
if (!isMounted) return;
if (result.error) {
console.error('[useDocuments] API returned error:', result.error);
setError(result.error);
setDocuments([]);
} else if (result.documents && Array.isArray(result.documents)) {
console.log(`[useDocuments] Loaded ${result.documents.length} documents`);
setDocuments(result.documents.map(transformRowToDocument));
setError(null);
} else {
console.log('[useDocuments] No documents found or invalid format');
setDocuments([]);
setError(null);
}
} catch (err: any) {
console.error('[useDocuments] Exception:', err);
if (!isMounted) return;
if (err.name === 'AbortError') {
setError('Request timed out. Please check your connection and try again.');
} else {
setError(err.message || 'Failed to load documents');
}
setDocuments([]);
} finally {
if (isMounted) {
console.log('[useDocuments] Setting isLoaded to true');
setIsLoaded(true);
}
}
}
loadDocuments();
return () => {
isMounted = false;
};
}, []);
// Initialize folders (local only for now)
useEffect(() => {
setFolders(getInitialFolders());
}, []);
// Update folder counts whenever documents change
useEffect(() => {
setFolders((prev) =>
prev.map((folder) => ({
...folder,
count: documents.filter((d) => d.folder === folder.name).length,
}))
);
}, [documents]);
const createDocument = useCallback(
async (doc: Omit<Document, 'id' | 'createdAt' | 'updatedAt' | 'size'>): Promise<Document | null> => {
// TODO: Implement via API
console.log('Create document not yet implemented');
return null;
},
[]
);
const updateDocument = useCallback(
async (id: string, updates: Partial<Omit<Document, 'id' | 'createdAt'>>): Promise<Document | null> => {
// TODO: Implement via API
console.log('Update document not yet implemented');
return null;
},
[]
);
const deleteDocument = useCallback(async (id: string): Promise<boolean> => {
try {
const response = await fetchWithTimeout(`/api/documents?id=${encodeURIComponent(id)}`, {
method: 'DELETE',
}, 10000);
let result: any = null;
try {
result = await response.json();
} catch {
// Best effort parse; fall back to status text below.
}
if (!response.ok) {
throw new Error(result?.error || `HTTP error! status: ${response.status}`);
}
setDocuments((prev) => prev.filter((doc) => doc.id !== id));
setError(null);
return true;
} catch (err: any) {
console.error('[useDocuments] Delete failed:', err);
setError(err?.message || 'Failed to delete document');
return false;
}
}, []);
const createFolder = useCallback((name: string): Folder => {
const newFolder: Folder = {
id: generateId(),
name,
count: 0,
};
setFolders((prev) => [...prev, newFolder]);
return newFolder;
}, []);
const deleteFolder = useCallback(
async (folderId: string): Promise<boolean> => {
const folder = folders.find((f) => f.id === folderId);
if (!folder) return false;
setFolders((prev) => prev.filter((f) => f.id !== folderId));
return true;
},
[folders]
);
const getDocumentById = useCallback(
(id: string): Document | undefined => {
return documents.find((d) => d.id === id);
},
[documents]
);
const getRecentDocuments = useCallback(
(limit = 5): Document[] => {
return [...documents]
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
.slice(0, limit);
},
[documents]
);
const getAllTags = useCallback((): string[] => {
const tags = new Set<string>();
documents.forEach((doc) => doc.tags.forEach((tag) => tags.add(tag)));
return Array.from(tags).sort();
}, [documents]);
// Refresh documents from API
const refreshDocuments = useCallback(async () => {
setIsLoaded(false);
// Re-trigger the effect by toggling a state (simplified)
window.location.reload();
}, []);
return {
documents,
folders,
isLoaded,
error,
createDocument,
updateDocument,
deleteDocument,
createFolder,
deleteFolder,
getDocumentById,
getRecentDocuments,
getAllTags,
refreshDocuments,
};
}