@syncular/console 0.0.4-25 → 0.0.5-42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/App.d.ts +1 -1
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +2 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/browser-main.js +1 -1
  6. package/dist/browser-main.js.map +1 -1
  7. package/dist/hooks/ConnectionContext.d.ts.map +1 -1
  8. package/dist/hooks/ConnectionContext.js +5 -14
  9. package/dist/hooks/ConnectionContext.js.map +1 -1
  10. package/dist/hooks/useConsoleApi.d.ts +11 -1
  11. package/dist/hooks/useConsoleApi.d.ts.map +1 -1
  12. package/dist/hooks/useConsoleApi.js +78 -0
  13. package/dist/hooks/useConsoleApi.js.map +1 -1
  14. package/dist/hooks/useLiveEvents.d.ts.map +1 -1
  15. package/dist/hooks/useLiveEvents.js +94 -0
  16. package/dist/hooks/useLiveEvents.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/layout.d.ts.map +1 -1
  22. package/dist/layout.js +3 -1
  23. package/dist/layout.js.map +1 -1
  24. package/dist/lib/api.d.ts +1 -1
  25. package/dist/lib/api.d.ts.map +1 -1
  26. package/dist/lib/api.js +36 -4
  27. package/dist/lib/api.js.map +1 -1
  28. package/dist/lib/types.d.ts +13 -0
  29. package/dist/lib/types.d.ts.map +1 -1
  30. package/dist/mount.d.ts.map +1 -1
  31. package/dist/mount.js +4 -1
  32. package/dist/mount.js.map +1 -1
  33. package/dist/pages/Config.d.ts +3 -1
  34. package/dist/pages/Config.d.ts.map +1 -1
  35. package/dist/pages/Config.js +2 -3
  36. package/dist/pages/Config.js.map +1 -1
  37. package/dist/pages/Fleet.d.ts +3 -1
  38. package/dist/pages/Fleet.d.ts.map +1 -1
  39. package/dist/pages/Fleet.js +6 -3
  40. package/dist/pages/Fleet.js.map +1 -1
  41. package/dist/pages/Ops.js +2 -2
  42. package/dist/pages/Ops.js.map +1 -1
  43. package/dist/pages/Storage.d.ts +2 -0
  44. package/dist/pages/Storage.d.ts.map +1 -0
  45. package/dist/pages/Storage.js +103 -0
  46. package/dist/pages/Storage.js.map +1 -0
  47. package/dist/pages/index.d.ts +1 -0
  48. package/dist/pages/index.d.ts.map +1 -1
  49. package/dist/pages/index.js +1 -0
  50. package/dist/pages/index.js.map +1 -1
  51. package/dist/routeTree.d.ts +1 -1
  52. package/dist/routeTree.d.ts.map +1 -1
  53. package/dist/routeTree.js +2 -0
  54. package/dist/routeTree.js.map +1 -1
  55. package/dist/routes/storage.d.ts +2 -0
  56. package/dist/routes/storage.d.ts.map +1 -0
  57. package/dist/routes/storage.js +9 -0
  58. package/dist/routes/storage.js.map +1 -0
  59. package/dist/server.d.ts +3 -0
  60. package/dist/server.d.ts.map +1 -0
  61. package/dist/server.js +3 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/static-server.d.ts +18 -0
  64. package/dist/static-server.d.ts.map +1 -0
  65. package/dist/static-server.js +137 -0
  66. package/dist/static-server.js.map +1 -0
  67. package/dist/styles.css +1 -0
  68. package/dist/theme-scope.d.ts +3 -0
  69. package/dist/theme-scope.d.ts.map +1 -0
  70. package/dist/theme-scope.js +5 -0
  71. package/dist/theme-scope.js.map +1 -0
  72. package/package.json +20 -9
  73. package/src/App.tsx +6 -1
  74. package/src/browser-main.tsx +1 -1
  75. package/src/hooks/ConnectionContext.tsx +5 -15
  76. package/src/hooks/useConsoleApi.ts +103 -0
  77. package/src/hooks/useLiveEvents.ts +115 -1
  78. package/src/index.ts +1 -0
  79. package/src/layout.tsx +12 -2
  80. package/src/lib/api.ts +38 -5
  81. package/src/lib/types.ts +17 -0
  82. package/src/mount.tsx +5 -1
  83. package/src/pages/Config.tsx +2 -1
  84. package/src/pages/Fleet.tsx +19 -17
  85. package/src/pages/Ops.tsx +4 -2
  86. package/src/pages/Storage.tsx +277 -0
  87. package/src/pages/index.ts +1 -0
  88. package/src/routeTree.ts +2 -0
  89. package/src/routes/storage.tsx +9 -0
  90. package/src/server.ts +2 -0
  91. package/src/static-server.ts +219 -0
  92. package/src/styles/globals.css +8 -1
  93. package/src/theme-scope.ts +5 -0
  94. package/web-dist/assets/index-BhPtRvK0.css +1 -0
  95. package/web-dist/assets/index-Fyq7dTrO.js +86 -0
  96. package/web-dist/console.css +1 -0
  97. package/web-dist/index.html +4 -4
  98. package/web-dist/chunk-7ayekhzx.css +0 -1
  99. package/web-dist/chunk-myppbvt5.js +0 -90
@@ -0,0 +1,277 @@
1
+ import {
2
+ Badge,
3
+ Button,
4
+ Dialog,
5
+ DialogContent,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ EmptyState,
10
+ Input,
11
+ SectionCard,
12
+ Spinner,
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ } from '@syncular/ui';
20
+ import { useState } from 'react';
21
+ import {
22
+ useBlobDownload,
23
+ useBlobs,
24
+ useDeleteBlobMutation,
25
+ } from '../hooks/useConsoleApi';
26
+
27
+ function formatFileSize(bytes: number): string {
28
+ if (bytes === 0) return '0 B';
29
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
30
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
31
+ const value = bytes / 1024 ** i;
32
+ return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
33
+ }
34
+
35
+ function formatDateTime(iso: string): string {
36
+ const parsed = Date.parse(iso);
37
+ if (!Number.isFinite(parsed)) return iso;
38
+ return new Date(parsed).toLocaleString();
39
+ }
40
+
41
+ export function Storage() {
42
+ const [prefixInput, setPrefixInput] = useState('');
43
+ const [activePrefix, setActivePrefix] = useState<string | undefined>(
44
+ undefined
45
+ );
46
+ const [cursor, setCursor] = useState<string | undefined>(undefined);
47
+ const [cursorHistory, setCursorHistory] = useState<string[]>([]);
48
+ const [deletingKey, setDeletingKey] = useState<string | null>(null);
49
+
50
+ const { data, isLoading, error } = useBlobs({
51
+ prefix: activePrefix,
52
+ cursor,
53
+ limit: 100,
54
+ });
55
+ const deleteMutation = useDeleteBlobMutation();
56
+ const download = useBlobDownload();
57
+
58
+ function handleFilter() {
59
+ const trimmed = prefixInput.trim();
60
+ setActivePrefix(trimmed.length > 0 ? trimmed : undefined);
61
+ setCursor(undefined);
62
+ setCursorHistory([]);
63
+ }
64
+
65
+ function handleClearFilter() {
66
+ setPrefixInput('');
67
+ setActivePrefix(undefined);
68
+ setCursor(undefined);
69
+ setCursorHistory([]);
70
+ }
71
+
72
+ function handleNextPage() {
73
+ if (data?.cursor) {
74
+ setCursorHistory((prev) => [...prev, cursor ?? '']);
75
+ setCursor(data.cursor);
76
+ }
77
+ }
78
+
79
+ function handlePrevPage() {
80
+ setCursorHistory((prev) => {
81
+ const next = [...prev];
82
+ const prevCursor = next.pop();
83
+ setCursor(prevCursor && prevCursor.length > 0 ? prevCursor : undefined);
84
+ return next;
85
+ });
86
+ }
87
+
88
+ function handleDelete() {
89
+ if (!deletingKey) return;
90
+ deleteMutation.mutate(deletingKey, {
91
+ onSuccess: () => setDeletingKey(null),
92
+ });
93
+ }
94
+
95
+ if (isLoading) {
96
+ return (
97
+ <div className="flex flex-col gap-4 px-5 py-5">
98
+ <div className="flex items-center justify-center h-[200px]">
99
+ <Spinner size="lg" />
100
+ </div>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ if (error) {
106
+ return (
107
+ <div className="flex flex-col gap-4 px-5 py-5">
108
+ <div className="flex items-center justify-center h-[200px]">
109
+ <p className="text-danger font-mono text-[11px]">
110
+ Failed to load storage items: {error.message}
111
+ </p>
112
+ </div>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ const items = data?.items ?? [];
118
+
119
+ return (
120
+ <div className="flex flex-col gap-4 px-5 py-5">
121
+ <SectionCard
122
+ title="Storage"
123
+ actions={
124
+ <div className="flex items-center gap-2">
125
+ <Input
126
+ placeholder="Prefix filter..."
127
+ value={prefixInput}
128
+ onChange={(e) => setPrefixInput(e.target.value)}
129
+ onKeyDown={(e) => {
130
+ if (e.key === 'Enter') handleFilter();
131
+ }}
132
+ className="h-7 w-48 text-xs"
133
+ />
134
+ <Button variant="default" size="sm" onClick={handleFilter}>
135
+ Filter
136
+ </Button>
137
+ {activePrefix && (
138
+ <Button variant="ghost" size="sm" onClick={handleClearFilter}>
139
+ Clear
140
+ </Button>
141
+ )}
142
+ </div>
143
+ }
144
+ >
145
+ {items.length === 0 ? (
146
+ <EmptyState
147
+ message={
148
+ activePrefix
149
+ ? `No storage items matching prefix "${activePrefix}".`
150
+ : 'No storage items found.'
151
+ }
152
+ />
153
+ ) : (
154
+ <>
155
+ <div className="overflow-x-auto">
156
+ <Table>
157
+ <TableHeader>
158
+ <TableRow>
159
+ <TableHead>Key</TableHead>
160
+ <TableHead>Size</TableHead>
161
+ <TableHead>Type</TableHead>
162
+ <TableHead>Uploaded</TableHead>
163
+ <TableHead>Actions</TableHead>
164
+ </TableRow>
165
+ </TableHeader>
166
+ <TableBody>
167
+ {items.map((blob) => (
168
+ <TableRow key={blob.key}>
169
+ <TableCell
170
+ className="font-mono text-xs max-w-[320px]"
171
+ title={blob.key}
172
+ >
173
+ {blob.key}
174
+ </TableCell>
175
+ <TableCell className="font-mono text-xs text-neutral-400 whitespace-nowrap">
176
+ {formatFileSize(blob.size)}
177
+ </TableCell>
178
+ <TableCell>
179
+ {blob.httpMetadata?.contentType ? (
180
+ <Badge variant="ghost">
181
+ {blob.httpMetadata.contentType}
182
+ </Badge>
183
+ ) : (
184
+ <span className="text-neutral-500 text-xs">--</span>
185
+ )}
186
+ </TableCell>
187
+ <TableCell className="whitespace-nowrap text-xs text-neutral-400">
188
+ {formatDateTime(blob.uploaded)}
189
+ </TableCell>
190
+ <TableCell>
191
+ <div className="flex items-center gap-1">
192
+ <Button
193
+ variant="ghost"
194
+ size="sm"
195
+ onClick={() => void download(blob.key)}
196
+ >
197
+ Download
198
+ </Button>
199
+ <Button
200
+ variant="ghost"
201
+ size="sm"
202
+ onClick={() => setDeletingKey(blob.key)}
203
+ >
204
+ Delete
205
+ </Button>
206
+ </div>
207
+ </TableCell>
208
+ </TableRow>
209
+ ))}
210
+ </TableBody>
211
+ </Table>
212
+ </div>
213
+
214
+ {/* Pagination */}
215
+ <div className="flex items-center justify-between px-2 py-2">
216
+ <Button
217
+ variant="ghost"
218
+ size="sm"
219
+ disabled={cursorHistory.length === 0}
220
+ onClick={handlePrevPage}
221
+ >
222
+ Previous
223
+ </Button>
224
+ <Button
225
+ variant="ghost"
226
+ size="sm"
227
+ disabled={!data?.truncated}
228
+ onClick={handleNextPage}
229
+ >
230
+ Next
231
+ </Button>
232
+ </div>
233
+ </>
234
+ )}
235
+ </SectionCard>
236
+
237
+ {/* Delete confirmation dialog */}
238
+ <Dialog
239
+ open={deletingKey !== null}
240
+ onOpenChange={() => setDeletingKey(null)}
241
+ >
242
+ <DialogContent>
243
+ <DialogHeader>
244
+ <DialogTitle>Delete Storage Item</DialogTitle>
245
+ </DialogHeader>
246
+ <div className="px-5 py-4 flex flex-col gap-4">
247
+ <p className="font-mono text-[11px] text-neutral-300">
248
+ Are you sure you want to delete{' '}
249
+ <span className="font-mono text-white">{deletingKey}</span>?
250
+ </p>
251
+ <p className="font-mono text-[10px] text-neutral-500">
252
+ This action cannot be undone.
253
+ </p>
254
+ </div>
255
+ <DialogFooter>
256
+ <Button variant="default" onClick={() => setDeletingKey(null)}>
257
+ Cancel
258
+ </Button>
259
+ <Button
260
+ variant="destructive"
261
+ onClick={handleDelete}
262
+ disabled={deleteMutation.isPending}
263
+ >
264
+ {deleteMutation.isPending ? (
265
+ <>
266
+ <Spinner size="sm" /> Deleting...
267
+ </>
268
+ ) : (
269
+ 'Delete'
270
+ )}
271
+ </Button>
272
+ </DialogFooter>
273
+ </DialogContent>
274
+ </Dialog>
275
+ </div>
276
+ );
277
+ }
@@ -2,4 +2,5 @@ export * from './Command';
2
2
  export * from './Config';
3
3
  export * from './Fleet';
4
4
  export * from './Ops';
5
+ export * from './Storage';
5
6
  export * from './Stream';
package/src/routeTree.ts CHANGED
@@ -5,6 +5,7 @@ import { Route as indexRoute } from './routes/index';
5
5
  import { Route as investigateCommitRoute } from './routes/investigate-commit';
6
6
  import { Route as investigateEventRoute } from './routes/investigate-event';
7
7
  import { Route as opsRoute } from './routes/ops';
8
+ import { Route as storageRoute } from './routes/storage';
8
9
  import { Route as streamRoute } from './routes/stream';
9
10
 
10
11
  export const routeTree = rootRoute.addChildren([
@@ -14,5 +15,6 @@ export const routeTree = rootRoute.addChildren([
14
15
  investigateEventRoute,
15
16
  fleetRoute,
16
17
  opsRoute,
18
+ storageRoute,
17
19
  configRoute,
18
20
  ]);
@@ -0,0 +1,9 @@
1
+ import { createRoute } from '@tanstack/react-router';
2
+ import { Storage } from '../pages';
3
+ import { Route as rootRoute } from './__root';
4
+
5
+ export const Route = createRoute({
6
+ getParentRoute: () => rootRoute,
7
+ path: '/storage',
8
+ component: Storage,
9
+ });
package/src/server.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './runtime-config';
2
+ export * from './static-server';
@@ -0,0 +1,219 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ CONSOLE_BASEPATH_META,
7
+ CONSOLE_SERVER_URL_META,
8
+ CONSOLE_TOKEN_META,
9
+ normalizeBasePath,
10
+ } from './runtime-config';
11
+
12
+ const DEFAULT_MOUNT_PATH = '/console';
13
+ const DEFAULT_INDEX_CACHE_CONTROL = 'no-store';
14
+ const DEFAULT_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
15
+
16
+ const CONTENT_TYPES: Record<string, string> = {
17
+ '.css': 'text/css; charset=utf-8',
18
+ '.html': 'text/html; charset=utf-8',
19
+ '.ico': 'image/x-icon',
20
+ '.js': 'text/javascript; charset=utf-8',
21
+ '.json': 'application/json; charset=utf-8',
22
+ '.manifest': 'application/manifest+json; charset=utf-8',
23
+ '.png': 'image/png',
24
+ '.svg': 'image/svg+xml',
25
+ '.txt': 'text/plain; charset=utf-8',
26
+ '.webmanifest': 'application/manifest+json; charset=utf-8',
27
+ };
28
+
29
+ export interface ConsoleUiPrefill {
30
+ basePath?: string;
31
+ serverUrl?: string;
32
+ token?: string;
33
+ }
34
+
35
+ export interface CreateConsoleStaticResponderOptions {
36
+ mountPath?: string;
37
+ staticDir?: string;
38
+ defaultPrefill?: ConsoleUiPrefill;
39
+ indexCacheControl?: string;
40
+ assetCacheControl?: string;
41
+ }
42
+
43
+ export interface ServeConsoleStaticRequestOptions {
44
+ prefill?: ConsoleUiPrefill;
45
+ }
46
+
47
+ export type ConsoleStaticResponder = (
48
+ request: Request,
49
+ options?: ServeConsoleStaticRequestOptions
50
+ ) => Promise<Response | null>;
51
+
52
+ function normalizeMountPath(mountPath: string | undefined): string {
53
+ const value = mountPath?.trim() ?? '';
54
+ if (!value || value === '/') return '/';
55
+ const withLeadingSlash = value.startsWith('/') ? value : `/${value}`;
56
+ return withLeadingSlash.replace(/\/+$/g, '') || '/';
57
+ }
58
+
59
+ function normalizeRequestPath(pathname: string): string {
60
+ const decodedPathname = decodeURIComponent(pathname);
61
+ return decodedPathname === '' ? '/' : decodedPathname;
62
+ }
63
+
64
+ function matchesMountPath(pathname: string, mountPath: string): boolean {
65
+ if (mountPath === '/') return pathname.startsWith('/');
66
+ return pathname === mountPath || pathname.startsWith(`${mountPath}/`);
67
+ }
68
+
69
+ function relativePathForMount(pathname: string, mountPath: string): string {
70
+ if (mountPath === '/') return pathname;
71
+ const withoutPrefix = pathname.slice(mountPath.length);
72
+ return withoutPrefix === '' ? '/' : withoutPrefix;
73
+ }
74
+
75
+ function escapeHtmlAttribute(value: string): string {
76
+ return value
77
+ .replaceAll('&', '&amp;')
78
+ .replaceAll('"', '&quot;')
79
+ .replaceAll('<', '&lt;')
80
+ .replaceAll('>', '&gt;');
81
+ }
82
+
83
+ function withMetaTag(html: string, name: string, value: string): string {
84
+ const escapedValue = escapeHtmlAttribute(value);
85
+ const pattern = new RegExp(`<meta name="${name}" content="[^"]*"\\s*/?>`);
86
+ if (pattern.test(html)) {
87
+ return html.replace(
88
+ pattern,
89
+ `<meta name="${name}" content="${escapedValue}" />`
90
+ );
91
+ }
92
+
93
+ return html.replace(
94
+ '</head>',
95
+ ` <meta name="${name}" content="${escapedValue}" />\n </head>`
96
+ );
97
+ }
98
+
99
+ function isWithinDirectory(baseDir: string, targetPath: string): boolean {
100
+ return (
101
+ targetPath === baseDir || targetPath.startsWith(`${baseDir}${path.sep}`)
102
+ );
103
+ }
104
+
105
+ function contentTypeFor(pathname: string): string {
106
+ const extension = path.extname(pathname).toLowerCase();
107
+ return CONTENT_TYPES[extension] ?? 'application/octet-stream';
108
+ }
109
+
110
+ function resolveConsoleStaticDir(): string {
111
+ const staticDirFromModule = fileURLToPath(
112
+ new URL('../web-dist', import.meta.url)
113
+ );
114
+ return path.resolve(staticDirFromModule);
115
+ }
116
+
117
+ function renderIndexHtml(args: {
118
+ template: string;
119
+ mountPath: string;
120
+ prefill?: ConsoleUiPrefill;
121
+ }): string {
122
+ const resolvedBasePath = normalizeBasePath(
123
+ args.prefill?.basePath ?? args.mountPath
124
+ );
125
+ const resolvedServerUrl = args.prefill?.serverUrl ?? '';
126
+ const resolvedToken = args.prefill?.token ?? '';
127
+
128
+ return withMetaTag(
129
+ withMetaTag(
130
+ withMetaTag(args.template, CONSOLE_BASEPATH_META, resolvedBasePath),
131
+ CONSOLE_SERVER_URL_META,
132
+ resolvedServerUrl
133
+ ),
134
+ CONSOLE_TOKEN_META,
135
+ resolvedToken
136
+ );
137
+ }
138
+
139
+ export function createConsoleStaticResponder(
140
+ options: CreateConsoleStaticResponderOptions = {}
141
+ ): ConsoleStaticResponder {
142
+ const mountPath = normalizeMountPath(options.mountPath ?? DEFAULT_MOUNT_PATH);
143
+ const staticDir = path.resolve(
144
+ options.staticDir ?? resolveConsoleStaticDir()
145
+ );
146
+ const indexPath = path.join(staticDir, 'index.html');
147
+
148
+ if (!existsSync(indexPath)) {
149
+ throw new Error(
150
+ `Console distributable missing: ${indexPath}. Build @syncular/console before serving static assets.`
151
+ );
152
+ }
153
+
154
+ const indexTemplate = readFileSync(indexPath, 'utf8');
155
+ const indexCacheControl =
156
+ options.indexCacheControl ?? DEFAULT_INDEX_CACHE_CONTROL;
157
+ const assetCacheControl =
158
+ options.assetCacheControl ?? DEFAULT_ASSET_CACHE_CONTROL;
159
+ const defaultPrefill = options.defaultPrefill;
160
+
161
+ return async (request, requestOptions = {}) => {
162
+ const method = request.method.toUpperCase();
163
+ if (method !== 'GET' && method !== 'HEAD') {
164
+ return null;
165
+ }
166
+
167
+ const url = new URL(request.url);
168
+ const pathname = normalizeRequestPath(url.pathname);
169
+ if (!matchesMountPath(pathname, mountPath)) {
170
+ return null;
171
+ }
172
+
173
+ const relativePath = relativePathForMount(pathname, mountPath);
174
+ const effectivePrefill = {
175
+ ...defaultPrefill,
176
+ ...requestOptions.prefill,
177
+ };
178
+ const sendIndex = () => {
179
+ const html = renderIndexHtml({
180
+ template: indexTemplate,
181
+ mountPath,
182
+ prefill: effectivePrefill,
183
+ });
184
+ return new Response(method === 'HEAD' ? undefined : html, {
185
+ status: 200,
186
+ headers: {
187
+ 'Content-Type': 'text/html; charset=utf-8',
188
+ 'Cache-Control': indexCacheControl,
189
+ },
190
+ });
191
+ };
192
+
193
+ if (
194
+ relativePath === '/' ||
195
+ relativePath === '/index.html' ||
196
+ path.extname(relativePath).length === 0
197
+ ) {
198
+ return sendIndex();
199
+ }
200
+
201
+ const candidatePath = path.resolve(staticDir, `.${relativePath}`);
202
+ if (!isWithinDirectory(staticDir, candidatePath)) {
203
+ return new Response('Forbidden', { status: 403 });
204
+ }
205
+
206
+ if (!existsSync(candidatePath) || !statSync(candidatePath).isFile()) {
207
+ return new Response('Not Found', { status: 404 });
208
+ }
209
+
210
+ const body = method === 'HEAD' ? undefined : await readFile(candidatePath);
211
+ return new Response(body, {
212
+ status: 200,
213
+ headers: {
214
+ 'Content-Type': contentTypeFor(candidatePath),
215
+ 'Cache-Control': assetCacheControl,
216
+ },
217
+ });
218
+ };
219
+ }
@@ -1 +1,8 @@
1
- @import "@syncular/ui/styles.css";
1
+ @import '@syncular/ui/styles.source.css';
2
+
3
+ /* Scan console page source files for Tailwind class usage */
4
+ @source "..";
5
+
6
+ :where(.syncular-console-root) {
7
+ color-scheme: dark;
8
+ }
@@ -0,0 +1,5 @@
1
+ export const SYNCULAR_CONSOLE_ROOT_CLASS = 'syncular-console-root';
2
+
3
+ export function applyConsoleThemeScope(container: Element): void {
4
+ container.classList.add(SYNCULAR_CONSOLE_ROOT_CLASS);
5
+ }