+23
-52
frontend/src/features/slices/sync-logs/handlers.tsx
+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
+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
}
+6
-3
frontend/src/utils/time.ts
+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