tangled
alpha
login
or
join now
slices.network
/
slices
137
fork
atom
Highly ambitious ATProtocol AppView service and sdks
137
fork
atom
overview
issues
10
pulls
3
pipelines
fix log timestamps, clean up sync ui
chadtmiller.com
5 months ago
b9b28021
e3c4bfef
+133
-127
4 changed files
expand all
collapse all
unified
split
frontend
src
features
slices
sync-logs
handlers.tsx
templates
SyncJobLogsPage.tsx
shared
fragments
LogViewer.tsx
utils
time.ts
+23
-52
frontend/src/features/slices/sync-logs/handlers.tsx
···
1
import type { Route } from "@std/http/unstable-route";
2
import { renderHTML } from "../../../utils/render.tsx";
3
-
import { requireAuth, withAuth } from "../../../routes/middleware.ts";
4
import { getSliceClient } from "../../../utils/client.ts";
5
import {
6
requireSliceAccess,
···
8
} from "../../../routes/slice-middleware.ts";
9
import { extractSliceParams } from "../../../utils/slice-params.ts";
10
import { SyncJobLogsPage } from "./templates/SyncJobLogsPage.tsx";
11
-
import { SyncJobLogs } from "./templates/SyncJobLogs.tsx";
12
13
async function handleSyncJobLogsPage(
14
req: Request,
15
-
params?: URLPatternResult,
16
): Promise<Response> {
17
const authContext = await withAuth(req);
18
const sliceParams = extractSliceParams(params);
···
25
const context = await withSliceAccess(
26
authContext,
27
sliceParams.handle,
28
-
sliceParams.sliceId,
29
);
30
const accessError = requireSliceAccess(context);
31
if (accessError) return accessError;
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
-
}
62
63
try {
64
-
const sliceClient = getSliceClient(context, sliceId);
65
const logsResponse = await sliceClient.network.slices.slice.getJobLogs({
66
jobId,
67
});
68
69
if (logsResponse.logs && Array.isArray(logsResponse.logs)) {
70
-
return renderHTML(<SyncJobLogs logs={logsResponse.logs} />);
71
}
0
0
0
0
72
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
-
}
85
}
86
87
export const syncLogsRoutes: Route[] = [
···
91
pathname: "/profile/:handle/slice/:rkey/sync/:jobId",
92
}),
93
handler: handleSyncJobLogsPage,
94
-
},
95
-
{
96
-
method: "GET",
97
-
pattern: new URLPattern({ pathname: "/api/slices/:id/sync/:jobId" }),
98
-
handler: handleSyncJobLogs,
99
},
100
];
···
1
import type { Route } from "@std/http/unstable-route";
2
import { renderHTML } from "../../../utils/render.tsx";
3
+
import { withAuth } from "../../../routes/middleware.ts";
4
import { getSliceClient } from "../../../utils/client.ts";
5
import {
6
requireSliceAccess,
···
8
} from "../../../routes/slice-middleware.ts";
9
import { extractSliceParams } from "../../../utils/slice-params.ts";
10
import { SyncJobLogsPage } from "./templates/SyncJobLogsPage.tsx";
11
+
import type { NetworkSlicesSliceGetJobLogsLogEntry } from "../../../client.ts";
12
13
async function handleSyncJobLogsPage(
14
req: Request,
15
+
params?: URLPatternResult
16
): Promise<Response> {
17
const authContext = await withAuth(req);
18
const sliceParams = extractSliceParams(params);
···
25
const context = await withSliceAccess(
26
authContext,
27
sliceParams.handle,
28
+
sliceParams.sliceId
29
);
30
const accessError = requireSliceAccess(context);
31
if (accessError) return accessError;
32
33
+
// Fetch sync job logs
34
+
let logs: NetworkSlicesSliceGetJobLogsLogEntry[] = [];
35
+
let error: string | null = null;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
36
37
try {
38
+
const sliceClient = getSliceClient(authContext, sliceParams.sliceId);
39
const logsResponse = await sliceClient.network.slices.slice.getJobLogs({
40
jobId,
41
});
42
43
if (logsResponse.logs && Array.isArray(logsResponse.logs)) {
44
+
logs = logsResponse.logs;
45
}
46
+
} catch (err) {
47
+
console.error("Failed to get sync job logs:", err);
48
+
error = err instanceof Error ? err.message : String(err);
49
+
}
50
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
+
);
0
0
61
}
62
63
export const syncLogsRoutes: Route[] = [
···
67
pathname: "/profile/:handle/slice/:rkey/sync/:jobId",
68
}),
69
handler: handleSyncJobLogsPage,
0
0
0
0
0
70
},
71
];
+17
-13
frontend/src/features/slices/sync-logs/templates/SyncJobLogsPage.tsx
···
1
import { SliceLogPage } from "../../shared/fragments/SliceLogPage.tsx";
2
import type { AuthenticatedUser } from "../../../../routes/middleware.ts";
3
-
import type { NetworkSlicesSliceDefsSliceView } from "../../../../client.ts";
0
0
0
4
import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts";
0
5
6
interface SyncJobLogsPageProps {
7
slice: NetworkSlicesSliceDefsSliceView;
8
sliceId: string;
9
jobId: string;
10
currentUser?: AuthenticatedUser;
0
0
11
}
12
13
export function SyncJobLogsPage({
···
15
sliceId,
16
jobId,
17
currentUser,
0
0
18
}: SyncJobLogsPageProps) {
19
return (
20
<SliceLogPage
···
25
breadcrumbItems={[
26
{ label: slice.name, href: buildSliceUrlFromView(slice, sliceId) },
27
{ label: "Sync", href: buildSliceUrlFromView(slice, sliceId, "sync") },
28
-
{ label: jobId.split("-")[0] + "..." }
29
]}
30
headerActions={
31
-
<div className="text-sm text-zinc-500 font-mono">
32
-
Job: {jobId}
33
-
</div>
34
}
35
>
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...
43
</div>
44
-
</div>
0
0
45
</SliceLogPage>
46
);
47
}
···
1
import { SliceLogPage } from "../../shared/fragments/SliceLogPage.tsx";
2
import type { AuthenticatedUser } from "../../../../routes/middleware.ts";
3
+
import type {
4
+
NetworkSlicesSliceDefsSliceView,
5
+
NetworkSlicesSliceGetJobLogsLogEntry,
6
+
} from "../../../../client.ts";
7
import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts";
8
+
import { SyncJobLogs } from "./SyncJobLogs.tsx";
9
10
interface SyncJobLogsPageProps {
11
slice: NetworkSlicesSliceDefsSliceView;
12
sliceId: string;
13
jobId: string;
14
currentUser?: AuthenticatedUser;
15
+
logs: NetworkSlicesSliceGetJobLogsLogEntry[];
16
+
error?: string | null;
17
}
18
19
export function SyncJobLogsPage({
···
21
sliceId,
22
jobId,
23
currentUser,
24
+
logs,
25
+
error,
26
}: SyncJobLogsPageProps) {
27
return (
28
<SliceLogPage
···
33
breadcrumbItems={[
34
{ label: slice.name, href: buildSliceUrlFromView(slice, sliceId) },
35
{ label: "Sync", href: buildSliceUrlFromView(slice, sliceId, "sync") },
36
+
{ label: jobId.split("-")[0] + "..." },
37
]}
38
headerActions={
39
+
<div className="text-sm text-zinc-500 font-mono">Job: {jobId}</div>
0
0
40
}
41
>
42
+
{error ? (
43
+
<div className="p-8 text-center text-red-600">
44
+
Failed to load logs: {error}
0
0
0
0
45
</div>
46
+
) : (
47
+
<SyncJobLogs logs={logs} />
48
+
)}
49
</SliceLogPage>
50
);
51
}
+87
-59
frontend/src/shared/fragments/LogViewer.tsx
···
29
const infoCount = logs.filter((l) => l.level === "info").length;
30
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>
41
</Text>
42
-
)}
43
-
{warnCount > 0 && (
44
-
<Text as="span" size="sm" variant="warning">
45
-
Warnings: <strong>{warnCount}</strong>
0
0
0
0
0
0
0
0
0
0
0
0
46
</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>
55
</div>
56
-
</div>
57
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
-
)}
0
0
0
0
91
</div>
92
</div>
93
-
</div>
94
-
))}
95
-
</Card.Content>
96
-
</Card>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
97
);
98
}
···
29
const infoCount = logs.filter((l) => l.level === "info").length;
30
31
return (
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>
0
0
0
38
</Text>
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>
55
</Text>
56
+
</div>
0
0
0
0
0
0
0
57
</div>
0
58
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>
96
</div>
97
</div>
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
+
</>
125
);
126
}
+6
-3
frontend/src/utils/time.ts
···
1
export function formatTimestamp(dateString: string): string {
2
const date = new Date(dateString);
3
-
return date.toLocaleTimeString([], {
4
-
hour: "2-digit",
0
0
0
5
minute: "2-digit",
6
second: "2-digit",
7
-
fractionalSecondDigits: 3,
8
});
9
}
10
···
1
export function formatTimestamp(dateString: string): string {
2
const date = new Date(dateString);
3
+
return date.toLocaleString([], {
4
+
month: "numeric",
5
+
day: "numeric",
6
+
year: "numeric",
7
+
hour: "numeric",
8
minute: "2-digit",
9
second: "2-digit",
10
+
hour12: true,
11
});
12
}
13