Highly ambitious ATProtocol AppView service and sdks

fix log timestamps, clean up sync ui

Changed files
+133 -127
frontend
src
features
slices
sync-logs
shared
fragments
utils
+23 -52
frontend/src/features/slices/sync-logs/handlers.tsx
··· 1 1 import type { Route } from "@std/http/unstable-route"; 2 2 import { renderHTML } from "../../../utils/render.tsx"; 3 - import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 3 + import { withAuth } from "../../../routes/middleware.ts"; 4 4 import { getSliceClient } from "../../../utils/client.ts"; 5 5 import { 6 6 requireSliceAccess, ··· 8 8 } from "../../../routes/slice-middleware.ts"; 9 9 import { extractSliceParams } from "../../../utils/slice-params.ts"; 10 10 import { SyncJobLogsPage } from "./templates/SyncJobLogsPage.tsx"; 11 - import { SyncJobLogs } from "./templates/SyncJobLogs.tsx"; 11 + import type { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts"; 12 12 13 13 async function handleSyncJobLogsPage( 14 14 req: Request, 15 - params?: URLPatternResult, 15 + params?: URLPatternResult 16 16 ): Promise<Response> { 17 17 const authContext = await withAuth(req); 18 18 const sliceParams = extractSliceParams(params); ··· 25 25 const context = await withSliceAccess( 26 26 authContext, 27 27 sliceParams.handle, 28 - sliceParams.sliceId, 28 + sliceParams.sliceId 29 29 ); 30 30 const accessError = requireSliceAccess(context); 31 31 if (accessError) return accessError; 32 32 33 - return renderHTML( 34 - <SyncJobLogsPage 35 - slice={context.sliceContext!.slice!} 36 - sliceId={sliceParams.sliceId} 37 - jobId={jobId} 38 - currentUser={authContext.currentUser} 39 - />, 40 - ); 41 - } 42 - 43 - async function handleSyncJobLogs( 44 - req: Request, 45 - params?: URLPatternResult, 46 - ): Promise<Response> { 47 - const context = await withAuth(req); 48 - const authResponse = requireAuth(context); 49 - if (authResponse) return authResponse; 50 - 51 - const sliceId = params?.pathname.groups.id; 52 - const jobId = params?.pathname.groups.jobId; 53 - 54 - if (!sliceId || !jobId) { 55 - return renderHTML( 56 - <div className="p-8 text-center text-red-600"> 57 - Invalid slice ID or job ID 58 - </div>, 59 - { status: 400 }, 60 - ); 61 - } 33 + // Fetch sync job logs 34 + let logs: NetworkSlicesSliceGetJobLogsLogEntry[] = []; 35 + let error: string | null = null; 62 36 63 37 try { 64 - const sliceClient = getSliceClient(context, sliceId); 38 + const sliceClient = getSliceClient(authContext, sliceParams.sliceId); 65 39 const logsResponse = await sliceClient.network.slices.slice.getJobLogs({ 66 40 jobId, 67 41 }); 68 42 69 43 if (logsResponse.logs && Array.isArray(logsResponse.logs)) { 70 - return renderHTML(<SyncJobLogs logs={logsResponse.logs} />); 44 + logs = logsResponse.logs; 71 45 } 46 + } catch (err) { 47 + console.error("Failed to get sync job logs:", err); 48 + error = err instanceof Error ? err.message : String(err); 49 + } 72 50 73 - return renderHTML( 74 - <div className="p-8 text-center text-gray-600">No logs available</div>, 75 - ); 76 - } catch (error) { 77 - console.error("Failed to get sync job logs:", error); 78 - const errorMessage = error instanceof Error ? error.message : String(error); 79 - return renderHTML( 80 - <div className="p-8 text-center text-red-600"> 81 - Failed to load logs: {errorMessage} 82 - </div>, 83 - ); 84 - } 51 + return renderHTML( 52 + <SyncJobLogsPage 53 + slice={context.sliceContext!.slice!} 54 + sliceId={sliceParams.sliceId} 55 + jobId={jobId} 56 + currentUser={authContext.currentUser} 57 + logs={logs} 58 + error={error} 59 + /> 60 + ); 85 61 } 86 62 87 63 export const syncLogsRoutes: Route[] = [ ··· 91 67 pathname: "/profile/:handle/slice/:rkey/sync/:jobId", 92 68 }), 93 69 handler: handleSyncJobLogsPage, 94 - }, 95 - { 96 - method: "GET", 97 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync/:jobId" }), 98 - handler: handleSyncJobLogs, 99 70 }, 100 71 ];
+17 -13
frontend/src/features/slices/sync-logs/templates/SyncJobLogsPage.tsx
··· 1 1 import { SliceLogPage } from "../../shared/fragments/SliceLogPage.tsx"; 2 2 import type { AuthenticatedUser } from "../../../../routes/middleware.ts"; 3 - import type { NetworkSlicesSliceDefsSliceView } from "../../../../client.ts"; 3 + import type { 4 + NetworkSlicesSliceDefsSliceView, 5 + NetworkSlicesSliceGetJobLogsLogEntry, 6 + } from "../../../../client.ts"; 4 7 import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 8 + import { SyncJobLogs } from "./SyncJobLogs.tsx"; 5 9 6 10 interface SyncJobLogsPageProps { 7 11 slice: NetworkSlicesSliceDefsSliceView; 8 12 sliceId: string; 9 13 jobId: string; 10 14 currentUser?: AuthenticatedUser; 15 + logs: NetworkSlicesSliceGetJobLogsLogEntry[]; 16 + error?: string | null; 11 17 } 12 18 13 19 export function SyncJobLogsPage({ ··· 15 21 sliceId, 16 22 jobId, 17 23 currentUser, 24 + logs, 25 + error, 18 26 }: SyncJobLogsPageProps) { 19 27 return ( 20 28 <SliceLogPage ··· 25 33 breadcrumbItems={[ 26 34 { label: slice.name, href: buildSliceUrlFromView(slice, sliceId) }, 27 35 { label: "Sync", href: buildSliceUrlFromView(slice, sliceId, "sync") }, 28 - { label: jobId.split("-")[0] + "..." } 36 + { label: jobId.split("-")[0] + "..." }, 29 37 ]} 30 38 headerActions={ 31 - <div className="text-sm text-zinc-500 font-mono"> 32 - Job: {jobId} 33 - </div> 39 + <div className="text-sm text-zinc-500 font-mono">Job: {jobId}</div> 34 40 } 35 41 > 36 - <div 37 - hx-get={`/api/slices/${sliceId}/sync/${jobId}`} 38 - hx-trigger="load" 39 - hx-swap="innerHTML" 40 - > 41 - <div className="p-8 text-center text-zinc-500"> 42 - Loading logs... 42 + {error ? ( 43 + <div className="p-8 text-center text-red-600"> 44 + Failed to load logs: {error} 43 45 </div> 44 - </div> 46 + ) : ( 47 + <SyncJobLogs logs={logs} /> 48 + )} 45 49 </SliceLogPage> 46 50 ); 47 51 }
+87 -59
frontend/src/shared/fragments/LogViewer.tsx
··· 29 29 const infoCount = logs.filter((l) => l.level === "info").length; 30 30 31 31 return ( 32 - <Card> 33 - <div className="bg-zinc-50 dark:bg-zinc-800 px-6 py-3 border-b border-zinc-200 dark:border-zinc-700 rounded-t-sm"> 34 - <div className="flex items-center gap-4"> 35 - <Text as="span" size="sm"> 36 - Total: <strong>{logs.length}</strong> 37 - </Text> 38 - {errorCount > 0 && ( 39 - <Text as="span" size="sm" variant="error"> 40 - Errors: <strong>{errorCount}</strong> 32 + <> 33 + <Card> 34 + <div className="bg-zinc-50 dark:bg-zinc-800 px-6 py-3 border-b border-zinc-200 dark:border-zinc-700 rounded-t-sm"> 35 + <div className="flex items-center gap-4"> 36 + <Text as="span" size="sm"> 37 + Total: <strong>{logs.length}</strong> 41 38 </Text> 42 - )} 43 - {warnCount > 0 && ( 44 - <Text as="span" size="sm" variant="warning"> 45 - Warnings: <strong>{warnCount}</strong> 39 + {errorCount > 0 && ( 40 + <Text as="span" size="sm" variant="error"> 41 + Errors: <strong>{errorCount}</strong> 42 + </Text> 43 + )} 44 + {warnCount > 0 && ( 45 + <Text as="span" size="sm" variant="warning"> 46 + Warnings: <strong>{warnCount}</strong> 47 + </Text> 48 + )} 49 + <Text 50 + as="span" 51 + size="sm" 52 + className="text-blue-600 dark:text-blue-400" 53 + > 54 + Info: <strong>{infoCount}</strong> 46 55 </Text> 47 - )} 48 - <Text 49 - as="span" 50 - size="sm" 51 - className="text-blue-600 dark:text-blue-400" 52 - > 53 - Info: <strong>{infoCount}</strong> 54 - </Text> 56 + </div> 55 57 </div> 56 - </div> 57 58 58 - <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 59 - {logs.map((log) => ( 60 - <div 61 - key={log.id} 62 - className="p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 font-mono text-sm" 63 - > 64 - <div className="flex items-start gap-3"> 65 - <Text as="span" size="xs" variant="muted"> 66 - {formatTimestamp(log.createdAt)} 67 - </Text> 68 - <LogLevelBadge level={log.level} /> 69 - <div className="flex-1"> 70 - <Text as="div" size="sm"> 71 - {log.message} 72 - </Text> 73 - {log.metadata && Object.keys(log.metadata).length > 0 && ( 74 - <details className="mt-2"> 75 - <summary 76 - className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300" 77 - /* @ts-ignore - Hyperscript attribute */ 78 - _="on click toggle .hidden on next <pre/>" 79 - > 80 - <Text as="span" size="xs" variant="muted"> 81 - View metadata 82 - </Text> 83 - </summary> 84 - <pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-800 rounded text-xs overflow-x-auto break-words whitespace-pre-wrap hidden"> 85 - <Text as="span" size="xs"> 86 - {JSON.stringify(log.metadata, null, 2)} 87 - </Text> 88 - </pre> 89 - </details> 90 - )} 59 + <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 60 + {logs.map((log) => ( 61 + <div 62 + key={log.id} 63 + className="p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 font-mono text-sm" 64 + > 65 + <div className="flex items-start gap-3"> 66 + <span 67 + className="log-timestamp text-xs text-zinc-500 dark:text-zinc-400" 68 + data-timestamp={log.createdAt} 69 + > 70 + {log.createdAt} 71 + </span> 72 + <LogLevelBadge level={log.level} /> 73 + <div className="flex-1"> 74 + <Text as="div" size="sm"> 75 + {log.message} 76 + </Text> 77 + {log.metadata && Object.keys(log.metadata).length > 0 && ( 78 + <details className="mt-2"> 79 + <summary 80 + className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300" 81 + /* @ts-ignore - Hyperscript attribute */ 82 + _="on click toggle .hidden on next <pre/>" 83 + > 84 + <Text as="span" size="xs" variant="muted"> 85 + View metadata 86 + </Text> 87 + </summary> 88 + <pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-800 rounded text-xs overflow-x-auto break-words whitespace-pre-wrap hidden"> 89 + <Text as="span" size="xs"> 90 + {JSON.stringify(log.metadata, null, 2)} 91 + </Text> 92 + </pre> 93 + </details> 94 + )} 95 + </div> 91 96 </div> 92 97 </div> 93 - </div> 94 - ))} 95 - </Card.Content> 96 - </Card> 98 + ))} 99 + </Card.Content> 100 + </Card> 101 + <script 102 + dangerouslySetInnerHTML={{ 103 + __html: ` 104 + (function() { 105 + document.querySelectorAll('.log-timestamp').forEach(function(el) { 106 + var timestamp = el.getAttribute('data-timestamp'); 107 + if (timestamp) { 108 + var date = new Date(timestamp); 109 + el.textContent = date.toLocaleString([], { 110 + month: 'numeric', 111 + day: 'numeric', 112 + year: 'numeric', 113 + hour: 'numeric', 114 + minute: '2-digit', 115 + second: '2-digit', 116 + hour12: true 117 + }); 118 + } 119 + }); 120 + })(); 121 + `, 122 + }} 123 + /> 124 + </> 97 125 ); 98 126 }
+6 -3
frontend/src/utils/time.ts
··· 1 1 export function formatTimestamp(dateString: string): string { 2 2 const date = new Date(dateString); 3 - return date.toLocaleTimeString([], { 4 - hour: "2-digit", 3 + return date.toLocaleString([], { 4 + month: "numeric", 5 + day: "numeric", 6 + year: "numeric", 7 + hour: "numeric", 5 8 minute: "2-digit", 6 9 second: "2-digit", 7 - fractionalSecondDigits: 3, 10 + hour12: true, 8 11 }); 9 12 } 10 13