@syncular/console 0.0.5-42 → 0.0.6-101

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 (66) hide show
  1. package/dist/App.d.ts +3 -7
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +3 -3
  4. package/dist/App.js.map +1 -1
  5. package/dist/hooks/ConnectionContext.d.ts +4 -1
  6. package/dist/hooks/ConnectionContext.d.ts.map +1 -1
  7. package/dist/hooks/ConnectionContext.js +111 -14
  8. package/dist/hooks/ConnectionContext.js.map +1 -1
  9. package/dist/hooks/useLiveEvents.d.ts.map +1 -1
  10. package/dist/hooks/useLiveEvents.js +22 -3
  11. package/dist/hooks/useLiveEvents.js.map +1 -1
  12. package/dist/layout.d.ts +4 -1
  13. package/dist/layout.d.ts.map +1 -1
  14. package/dist/layout.js +7 -7
  15. package/dist/layout.js.map +1 -1
  16. package/dist/mount.d.ts +1 -0
  17. package/dist/mount.d.ts.map +1 -1
  18. package/dist/mount.js +1 -1
  19. package/dist/mount.js.map +1 -1
  20. package/dist/pages/Config.js +22 -14
  21. package/dist/pages/Config.js.map +1 -1
  22. package/dist/pages/Ops.js.map +1 -1
  23. package/dist/pages/Stream.d.ts.map +1 -1
  24. package/dist/pages/Stream.js +2 -3
  25. package/dist/pages/Stream.js.map +1 -1
  26. package/dist/routeTree.d.ts +1 -1
  27. package/dist/routeTree.d.ts.map +1 -1
  28. package/dist/routes/__root.d.ts +1 -1
  29. package/dist/routes/__root.d.ts.map +1 -1
  30. package/dist/routes/config.d.ts +1 -1
  31. package/dist/routes/config.d.ts.map +1 -1
  32. package/dist/routes/fleet.d.ts +1 -1
  33. package/dist/routes/fleet.d.ts.map +1 -1
  34. package/dist/routes/index.d.ts +1 -1
  35. package/dist/routes/index.d.ts.map +1 -1
  36. package/dist/routes/investigate-commit.d.ts +1 -1
  37. package/dist/routes/investigate-commit.d.ts.map +1 -1
  38. package/dist/routes/investigate-event.d.ts +1 -1
  39. package/dist/routes/investigate-event.d.ts.map +1 -1
  40. package/dist/routes/ops.d.ts +1 -1
  41. package/dist/routes/ops.d.ts.map +1 -1
  42. package/dist/routes/storage.d.ts +1 -1
  43. package/dist/routes/storage.d.ts.map +1 -1
  44. package/dist/routes/stream.d.ts +1 -1
  45. package/dist/routes/stream.d.ts.map +1 -1
  46. package/dist/static-server.d.ts.map +1 -1
  47. package/dist/static-server.js +6 -1
  48. package/dist/static-server.js.map +1 -1
  49. package/dist/styles.css +1 -1
  50. package/package.json +8 -8
  51. package/src/App.tsx +12 -10
  52. package/src/__tests__/static-server.test.ts +193 -0
  53. package/src/hooks/ConnectionContext.tsx +130 -14
  54. package/src/hooks/useLiveEvents.ts +27 -3
  55. package/src/layout.tsx +27 -4
  56. package/src/mount.tsx +6 -1
  57. package/src/pages/Config.tsx +55 -48
  58. package/src/pages/Stream.tsx +6 -3
  59. package/src/static-server.ts +12 -1
  60. package/web-dist/assets/index-BuP84qca.js +86 -0
  61. package/web-dist/assets/index-D_fQabjS.css +1 -0
  62. package/web-dist/console.css +1 -1
  63. package/web-dist/index.html +2 -2
  64. package/web-dist/site.webmanifest +2 -2
  65. package/web-dist/assets/index-BhPtRvK0.css +0 -1
  66. package/web-dist/assets/index-Fyq7dTrO.js +0 -86
@@ -10,6 +10,7 @@ import {
10
10
  useContext,
11
11
  useEffect,
12
12
  useMemo,
13
+ useRef,
13
14
  useState,
14
15
  } from 'react';
15
16
  import {
@@ -17,7 +18,58 @@ import {
17
18
  createConsoleClient,
18
19
  testConnection,
19
20
  } from '../lib/api';
20
- import { useLocalStorage } from './useLocalStorage';
21
+
22
+ export type ConnectionStorageMode = 'memory' | 'session' | 'local';
23
+
24
+ const CONNECTION_STORAGE_KEY = 'sync-console-connection';
25
+
26
+ function normalizeConfig(
27
+ config: ConnectionConfig | null | undefined
28
+ ): ConnectionConfig | null {
29
+ if (!config) return null;
30
+ const serverUrl = config.serverUrl?.trim() ?? '';
31
+ const token = config.token?.trim() ?? '';
32
+ if (!serverUrl || !token) return null;
33
+ return { serverUrl, token };
34
+ }
35
+
36
+ function getStorageForMode(mode: ConnectionStorageMode): Storage | null {
37
+ if (typeof window === 'undefined') return null;
38
+ if (mode === 'local') return window.localStorage;
39
+ if (mode === 'session') return window.sessionStorage;
40
+ return null;
41
+ }
42
+
43
+ function readStoredConfig(
44
+ mode: ConnectionStorageMode
45
+ ): ConnectionConfig | null {
46
+ const storage = getStorageForMode(mode);
47
+ if (!storage) return null;
48
+ try {
49
+ const raw = storage.getItem(CONNECTION_STORAGE_KEY);
50
+ if (!raw) return null;
51
+ return normalizeConfig(JSON.parse(raw) as ConnectionConfig);
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function writeStoredConfig(
58
+ mode: ConnectionStorageMode,
59
+ config: ConnectionConfig | null
60
+ ): void {
61
+ const storage = getStorageForMode(mode);
62
+ if (!storage) return;
63
+ try {
64
+ if (!config) {
65
+ storage.removeItem(CONNECTION_STORAGE_KEY);
66
+ return;
67
+ }
68
+ storage.setItem(CONNECTION_STORAGE_KEY, JSON.stringify(config));
69
+ } catch {
70
+ // Ignore storage write errors.
71
+ }
72
+ }
21
73
 
22
74
  interface ConnectionState {
23
75
  isConnected: boolean;
@@ -44,6 +96,8 @@ interface ConnectionContextValue {
44
96
  interface ConnectionProviderProps {
45
97
  children: ReactNode;
46
98
  defaultConfig?: ConnectionConfig | null;
99
+ autoConnect?: boolean;
100
+ storageMode?: ConnectionStorageMode;
47
101
  }
48
102
 
49
103
  const ConnectionContext = createContext<ConnectionContextValue | null>(null);
@@ -51,10 +105,35 @@ const ConnectionContext = createContext<ConnectionContextValue | null>(null);
51
105
  export function ConnectionProvider({
52
106
  children,
53
107
  defaultConfig = null,
108
+ autoConnect = false,
109
+ storageMode = 'session',
54
110
  }: ConnectionProviderProps) {
55
- const [config, setConfigStorage] = useLocalStorage<ConnectionConfig | null>(
56
- 'sync-console-connection',
57
- null
111
+ const [config, setConfigState] = useState<ConnectionConfig | null>(() =>
112
+ readStoredConfig(storageMode)
113
+ );
114
+
115
+ useEffect(() => {
116
+ const storedConfig = readStoredConfig(storageMode);
117
+ setConfigState(storedConfig);
118
+ }, [storageMode]);
119
+
120
+ useEffect(() => {
121
+ if (typeof window === 'undefined') return;
122
+ if (storageMode !== 'local') {
123
+ window.localStorage.removeItem(CONNECTION_STORAGE_KEY);
124
+ }
125
+ if (storageMode === 'memory') {
126
+ window.sessionStorage.removeItem(CONNECTION_STORAGE_KEY);
127
+ }
128
+ }, [storageMode]);
129
+
130
+ const setConfigStorage = useCallback(
131
+ (nextConfig: ConnectionConfig | null) => {
132
+ const normalized = normalizeConfig(nextConfig);
133
+ setConfigState(normalized);
134
+ writeStoredConfig(storageMode, normalized);
135
+ },
136
+ [storageMode]
58
137
  );
59
138
 
60
139
  const [state, setState] = useState<ConnectionState>({
@@ -63,6 +142,7 @@ export function ConnectionProvider({
63
142
  error: null,
64
143
  client: null,
65
144
  });
145
+ const lastAutoConnectConfigKeyRef = useRef<string | null>(null);
66
146
 
67
147
  // Resolve initial config: saved config -> provided defaults
68
148
  useEffect(() => {
@@ -90,18 +170,16 @@ export function ConnectionProvider({
90
170
  return false;
91
171
  }
92
172
 
93
- const normalizedConfig: ConnectionConfig = {
94
- serverUrl: effectiveConfig.serverUrl?.trim() ?? '',
95
- token: effectiveConfig.token?.trim() ?? '',
96
- };
173
+ const normalizedConfig = normalizeConfig(effectiveConfig);
97
174
 
98
175
  // Validate config has required fields
99
- if (!normalizedConfig.serverUrl) {
100
- setState((s) => ({ ...s, error: 'Server URL is required' }));
101
- return false;
102
- }
103
- if (!normalizedConfig.token) {
104
- setState((s) => ({ ...s, error: 'Token is required' }));
176
+ if (!normalizedConfig) {
177
+ const hasServerUrl =
178
+ (effectiveConfig.serverUrl?.trim() ?? '').length > 0;
179
+ setState((s) => ({
180
+ ...s,
181
+ error: hasServerUrl ? 'Token is required' : 'Server URL is required',
182
+ }));
105
183
  return false;
106
184
  }
107
185
 
@@ -182,6 +260,32 @@ export function ConnectionProvider({
182
260
  [config, setConfig, state, connect, disconnect, clearError]
183
261
  );
184
262
 
263
+ useEffect(() => {
264
+ if (!autoConnect || state.isConnected || state.isConnecting) {
265
+ return;
266
+ }
267
+
268
+ const candidate = config ?? defaultConfig;
269
+ const key = normalizeConfigKey(candidate);
270
+ if (!candidate || !key) {
271
+ return;
272
+ }
273
+
274
+ if (lastAutoConnectConfigKeyRef.current === key) {
275
+ return;
276
+ }
277
+
278
+ lastAutoConnectConfigKeyRef.current = key;
279
+ void connect(candidate, { persistOverride: true });
280
+ }, [
281
+ autoConnect,
282
+ config,
283
+ defaultConfig,
284
+ state.isConnected,
285
+ state.isConnecting,
286
+ connect,
287
+ ]);
288
+
185
289
  return (
186
290
  <ConnectionContext.Provider value={value}>
187
291
  {children}
@@ -189,6 +293,18 @@ export function ConnectionProvider({
189
293
  );
190
294
  }
191
295
 
296
+ function normalizeConfigKey(config: ConnectionConfig | null): string | null {
297
+ if (!config) {
298
+ return null;
299
+ }
300
+ const serverUrl = config.serverUrl?.trim() ?? '';
301
+ const token = config.token?.trim() ?? '';
302
+ if (!serverUrl || !token) {
303
+ return null;
304
+ }
305
+ return `${serverUrl}\u0000${token}`;
306
+ }
307
+
192
308
  export function useConnection(): ConnectionContextValue {
193
309
  const context = useContext(ConnectionContext);
194
310
  if (!context) {
@@ -243,7 +243,6 @@ export function useLiveEvents(
243
243
  : baseUrl.pathname;
244
244
  baseUrl.pathname = `${normalizedPath}/console/events/live`;
245
245
  baseUrl.search = '';
246
- baseUrl.searchParams.set('token', config.token);
247
246
  if (lastEventTimestampRef.current) {
248
247
  baseUrl.searchParams.set('since', lastEventTimestampRef.current);
249
248
  }
@@ -266,8 +265,21 @@ export function useLiveEvents(
266
265
  return;
267
266
  }
268
267
  reconnectAttemptsRef.current = 0;
269
- markActivity();
270
268
  setError(null);
269
+ setConnectionState('connecting');
270
+ setIsConnected(false);
271
+
272
+ try {
273
+ ws.send(
274
+ JSON.stringify({
275
+ type: 'auth',
276
+ token: config.token,
277
+ })
278
+ );
279
+ } catch {
280
+ ws.close();
281
+ return;
282
+ }
271
283
 
272
284
  clearStaleInterval();
273
285
  staleCheckIntervalRef.current = setInterval(() => {
@@ -304,7 +316,19 @@ export function useLiveEvents(
304
316
  markActivity();
305
317
 
306
318
  // Skip control events
307
- if (eventType === 'connected' || eventType === 'heartbeat') {
319
+ if (
320
+ eventType === 'connected' ||
321
+ eventType === 'heartbeat' ||
322
+ eventType === 'auth_required'
323
+ ) {
324
+ return;
325
+ }
326
+ if (eventType === 'error') {
327
+ const message =
328
+ typeof data.message === 'string'
329
+ ? data.message
330
+ : 'Live events authentication failed';
331
+ setError(new Error(message));
308
332
  return;
309
333
  }
310
334
 
package/src/layout.tsx CHANGED
@@ -1,15 +1,18 @@
1
1
  import {
2
+ Badge,
2
3
  BottomBar,
3
4
  Button,
4
5
  ConnectionStatusBadge,
5
6
  Input,
6
7
  NavPill,
7
8
  NavPillGroup,
9
+ navActionLinkClassName,
8
10
  SyncularBrand,
9
11
  TopNavigation,
10
12
  } from '@syncular/ui';
11
13
  import { Link, Outlet, useRouterState } from '@tanstack/react-router';
12
- import { Settings } from 'lucide-react';
14
+ import { ArrowLeft, Settings } from 'lucide-react';
15
+ import type { ReactNode } from 'react';
13
16
  import { useMemo } from 'react';
14
17
  import { useConnection } from './hooks/ConnectionContext';
15
18
  import { useStats } from './hooks/useConsoleApi';
@@ -20,6 +23,8 @@ import { SYNCULAR_CONSOLE_ROOT_CLASS } from './theme-scope';
20
23
 
21
24
  interface ConsoleLayoutProps {
22
25
  basePath?: string;
26
+ appHref?: string;
27
+ modeBadge?: ReactNode;
23
28
  }
24
29
 
25
30
  type ConsoleNavSuffix =
@@ -56,7 +61,11 @@ function resolvePath(basePath: string, suffix: ConsoleNavSuffix): string {
56
61
  return suffix ? `${basePath}${suffix}` : basePath;
57
62
  }
58
63
 
59
- export function ConsoleLayout({ basePath }: ConsoleLayoutProps) {
64
+ export function ConsoleLayout({
65
+ basePath,
66
+ appHref,
67
+ modeBadge,
68
+ }: ConsoleLayoutProps) {
60
69
  const { connect, config, isConnected, isConnecting } = useConnection();
61
70
  const { preferences } = usePreferences();
62
71
  const { instanceId, rawInstanceId, setInstanceId, clearInstanceId } =
@@ -134,6 +143,14 @@ export function ConsoleLayout({ basePath }: ConsoleLayoutProps) {
134
143
  }
135
144
  right={
136
145
  <div className="flex items-center gap-2">
146
+ {modeBadge ? (
147
+ <Badge
148
+ variant="flow"
149
+ className="hidden md:inline-flex px-2 py-1 text-[10px]"
150
+ >
151
+ {modeBadge}
152
+ </Badge>
153
+ ) : null}
137
154
  <div className="flex items-center gap-1">
138
155
  <span className="font-mono text-[9px] text-neutral-500 uppercase tracking-wide">
139
156
  Instance
@@ -186,14 +203,20 @@ export function ConsoleLayout({ basePath }: ConsoleLayoutProps) {
186
203
  variant={pathname === configPath ? 'secondary' : 'ghost'}
187
204
  size="icon"
188
205
  >
189
- <Settings />
206
+ <Settings className="h-3 w-3" />
190
207
  </Button>
191
208
  </Link>
209
+ {appHref ? (
210
+ <a href={appHref} className={navActionLinkClassName}>
211
+ <ArrowLeft className="h-3 w-3" />
212
+ Go to app
213
+ </a>
214
+ ) : null}
192
215
  </div>
193
216
  }
194
217
  />
195
218
 
196
- <main className="flex-1 overflow-auto pt-[42px] pb-[32px]">
219
+ <main className="flex-1 overflow-auto pb-[32px]">
197
220
  <div className="min-h-full">
198
221
  {isConnected || pathname === configPath ? (
199
222
  <div key={pathname} style={{ animation: 'pageIn 0.3s ease-out' }}>
package/src/mount.tsx CHANGED
@@ -7,6 +7,7 @@ interface MountSyncularConsoleOptions {
7
7
  strictMode?: boolean;
8
8
  basePath?: SyncularConsoleProps['basePath'];
9
9
  defaultConfig?: SyncularConsoleProps['defaultConfig'];
10
+ autoConnect?: SyncularConsoleProps['autoConnect'];
10
11
  }
11
12
 
12
13
  function resolveContainer(containerOrSelector: Element | string): Element {
@@ -33,7 +34,11 @@ export function mountSyncularConsoleApp(
33
34
 
34
35
  const root = createRoot(container);
35
36
  const app = (
36
- <App basePath={options.basePath} defaultConfig={options.defaultConfig} />
37
+ <App
38
+ basePath={options.basePath}
39
+ defaultConfig={options.defaultConfig}
40
+ autoConnect={options.autoConnect}
41
+ />
37
42
  );
38
43
 
39
44
  if (options.strictMode === false) {
@@ -80,15 +80,24 @@ function ConnectionTab() {
80
80
 
81
81
  useEffect(() => {
82
82
  const params = new URLSearchParams(window.location.search);
83
- const urlToken = params.get('token');
84
83
  const urlServer = params.get('server');
84
+ let shouldReplaceUrl = false;
85
85
 
86
- if (urlToken) {
87
- setToken(urlToken);
88
- window.history.replaceState({}, '', window.location.pathname);
89
- }
90
86
  if (urlServer) {
91
87
  setServerUrl(urlServer);
88
+ params.delete('server');
89
+ shouldReplaceUrl = true;
90
+ }
91
+ if (params.has('token')) {
92
+ params.delete('token');
93
+ shouldReplaceUrl = true;
94
+ }
95
+ if (shouldReplaceUrl) {
96
+ const nextQuery = params.toString();
97
+ const nextUrl = nextQuery
98
+ ? `${window.location.pathname}?${nextQuery}`
99
+ : window.location.pathname;
100
+ window.history.replaceState({}, '', nextUrl);
92
101
  }
93
102
  }, []);
94
103
 
@@ -555,7 +564,7 @@ function ApiKeysTab() {
555
564
  <Table>
556
565
  <TableHeader>
557
566
  <TableRow>
558
- <TableHead>
567
+ <TableHead className="w-[28px]">
559
568
  <Checkbox
560
569
  checked={allSelectableChecked}
561
570
  indeterminate={
@@ -573,16 +582,15 @@ function ApiKeysTab() {
573
582
  aria-label="Select all active keys"
574
583
  />
575
584
  </TableHead>
576
- <TableHead>NAME</TableHead>
577
- <TableHead>TYPE</TableHead>
578
- <TableHead>KEY PREFIX</TableHead>
579
- <TableHead>ACTOR</TableHead>
580
- <TableHead>SCOPES</TableHead>
581
- <TableHead>CREATED</TableHead>
582
- <TableHead>LAST USED</TableHead>
583
- <TableHead>EXPIRES</TableHead>
584
- <TableHead>STATUS</TableHead>
585
- <TableHead>ACTIONS</TableHead>
585
+ <TableHead className="w-[100px]">NAME</TableHead>
586
+ <TableHead className="w-[55px]">TYPE</TableHead>
587
+ <TableHead className="w-[90px]">KEY PREFIX</TableHead>
588
+ <TableHead className="w-[80px]">ACTOR</TableHead>
589
+ <TableHead className="w-[100px]">SCOPES</TableHead>
590
+ <TableHead className="w-[120px]">CREATED</TableHead>
591
+ <TableHead className="w-[120px]">LAST USED</TableHead>
592
+ <TableHead className="w-[120px]">EXPIRES</TableHead>
593
+ <TableHead className="flex-1">STATUS</TableHead>
586
594
  </TableRow>
587
595
  </TableHeader>
588
596
  <TableBody>
@@ -593,8 +601,8 @@ function ApiKeysTab() {
593
601
  );
594
602
 
595
603
  return (
596
- <TableRow key={apiKey.keyId}>
597
- <TableCell>
604
+ <TableRow key={apiKey.keyId} className="group relative">
605
+ <TableCell className="w-[28px]">
598
606
  <Checkbox
599
607
  checked={selectedKeyIds.includes(apiKey.keyId)}
600
608
  onCheckedChange={(checked) => {
@@ -610,8 +618,10 @@ function ApiKeysTab() {
610
618
  disabled={apiKey.revokedAt !== null}
611
619
  />
612
620
  </TableCell>
613
- <TableCell className="font-medium">{apiKey.name}</TableCell>
614
- <TableCell>
621
+ <TableCell className="w-[100px] font-medium">
622
+ {apiKey.name}
623
+ </TableCell>
624
+ <TableCell className="w-[55px]">
615
625
  <Badge
616
626
  variant={
617
627
  apiKey.keyType === 'admin'
@@ -624,63 +634,60 @@ function ApiKeysTab() {
624
634
  {apiKey.keyType}
625
635
  </Badge>
626
636
  </TableCell>
627
- <TableCell>
637
+ <TableCell className="w-[90px]">
628
638
  <code className="font-mono text-[11px]">
629
639
  {apiKey.keyPrefix}...
630
640
  </code>
631
641
  </TableCell>
632
- <TableCell className="text-neutral-500">
642
+ <TableCell className="w-[80px] text-neutral-500">
633
643
  {apiKey.actorId ?? '-'}
634
644
  </TableCell>
635
- <TableCell className="max-w-[220px] text-neutral-500">
645
+ <TableCell className="w-[100px] text-neutral-500">
636
646
  <code className="font-mono text-[10px]">
637
647
  {summarizeScopeKeys(apiKey.scopeKeys)}
638
648
  </code>
639
649
  </TableCell>
640
- <TableCell className="text-neutral-500">
650
+ <TableCell className="w-[120px] text-neutral-500">
641
651
  {formatOptionalDateTime(apiKey.createdAt)}
642
652
  </TableCell>
643
- <TableCell className="text-neutral-500">
653
+ <TableCell className="w-[120px] text-neutral-500">
644
654
  {formatOptionalDateTime(apiKey.lastUsedAt)}
645
655
  </TableCell>
646
- <TableCell className="text-neutral-500">
656
+ <TableCell className="w-[120px] text-neutral-500">
647
657
  {formatOptionalDateTime(apiKey.expiresAt)}
648
658
  </TableCell>
649
- <TableCell>
659
+ <TableCell className="flex-1">
650
660
  <Badge
651
661
  variant={getApiKeyStatusBadgeVariant(lifecycleStatus)}
652
662
  >
653
663
  {lifecycleStatus}
654
664
  </Badge>
655
665
  </TableCell>
656
- <TableCell>
657
- <div className="flex items-center gap-1">
658
- <Button
659
- variant="default"
660
- size="sm"
666
+ {apiKey.revokedAt === null && (
667
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
668
+ <button
669
+ type="button"
661
670
  onClick={() => setStagingRotateKey(apiKey)}
662
- disabled={apiKey.revokedAt !== null}
671
+ className="px-1.5 py-0.5 rounded text-[9px] font-mono text-neutral-600 hover:text-white hover:bg-white/[0.05] cursor-pointer transition-colors"
663
672
  >
664
- Stage
665
- </Button>
666
- <Button
667
- variant="default"
668
- size="sm"
673
+ stage
674
+ </button>
675
+ <button
676
+ type="button"
669
677
  onClick={() => setRotatingKeyId(apiKey.keyId)}
670
- disabled={apiKey.revokedAt !== null}
678
+ className="px-1.5 py-0.5 rounded text-[9px] font-mono text-neutral-600 hover:text-white hover:bg-white/[0.05] cursor-pointer transition-colors"
671
679
  >
672
- Rotate
673
- </Button>
674
- <Button
675
- variant="destructive"
676
- size="sm"
680
+ rotate
681
+ </button>
682
+ <button
683
+ type="button"
677
684
  onClick={() => setRevokingKeyId(apiKey.keyId)}
678
- disabled={apiKey.revokedAt !== null}
685
+ className="px-1.5 py-0.5 rounded text-[9px] font-mono text-neutral-600 hover:text-offline hover:bg-offline/10 cursor-pointer transition-colors"
679
686
  >
680
- Revoke
681
- </Button>
687
+ revoke
688
+ </button>
682
689
  </div>
683
- </TableCell>
690
+ )}
684
691
  </TableRow>
685
692
  );
686
693
  })}
@@ -164,8 +164,11 @@ export function Stream({ initialSelectedEntryId }: StreamProps = {}) {
164
164
  const { range, setRange } = useTimeRangeState();
165
165
  const pageSize = preferences.pageSize;
166
166
  const refreshIntervalMs = preferences.refreshInterval * 1000;
167
- const traceUrlTemplate: string | undefined = import.meta.env
168
- ?.VITE_CONSOLE_TRACE_URL_TEMPLATE;
167
+ const traceUrlTemplate: string | undefined = (
168
+ import.meta as ImportMeta & {
169
+ env?: { VITE_CONSOLE_TRACE_URL_TEMPLATE?: string };
170
+ }
171
+ ).env?.VITE_CONSOLE_TRACE_URL_TEMPLATE;
169
172
 
170
173
  const [viewMode, setViewMode] = useState<ViewMode>(() => {
171
174
  if (initialSelectedEntryId?.startsWith('#')) return 'commits';
@@ -268,7 +271,7 @@ export function Stream({ initialSelectedEntryId }: StreamProps = {}) {
268
271
  selectedEvent?.traceId ?? null,
269
272
  selectedEvent?.spanId ?? null
270
273
  ),
271
- [selectedEvent?.spanId, selectedEvent?.traceId]
274
+ [selectedEvent?.spanId, selectedEvent?.traceId, traceUrlTemplate]
272
275
  );
273
276
 
274
277
  useEffect(() => {
@@ -125,7 +125,7 @@ function renderIndexHtml(args: {
125
125
  const resolvedServerUrl = args.prefill?.serverUrl ?? '';
126
126
  const resolvedToken = args.prefill?.token ?? '';
127
127
 
128
- return withMetaTag(
128
+ const withMeta = withMetaTag(
129
129
  withMetaTag(
130
130
  withMetaTag(args.template, CONSOLE_BASEPATH_META, resolvedBasePath),
131
131
  CONSOLE_SERVER_URL_META,
@@ -134,6 +134,17 @@ function renderIndexHtml(args: {
134
134
  CONSOLE_TOKEN_META,
135
135
  resolvedToken
136
136
  );
137
+
138
+ if (resolvedBasePath === '/') {
139
+ return withMeta;
140
+ }
141
+
142
+ const mountPrefix = resolvedBasePath.replace(/\/+$/g, '');
143
+ return withMeta.replace(
144
+ /(src|href)=("|')\/assets\/([^"']+)("|')/g,
145
+ (_match, attribute, openQuote, assetPath, closeQuote) =>
146
+ `${attribute}=${openQuote}${mountPrefix}/assets/${assetPath}${closeQuote}`
147
+ );
137
148
  }
138
149
 
139
150
  export function createConsoleStaticResponder(