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