@affectively/aeon-pages 1.3.0
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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OfflineDiagnostics Component
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive diagnostics panel for offline-first applications.
|
|
5
|
+
* Shows network status, service worker state, cache management, and queue stats.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Network status monitoring
|
|
9
|
+
* - Service worker status display
|
|
10
|
+
* - Cache management UI
|
|
11
|
+
* - Queue statistics display
|
|
12
|
+
* - Conflict resolution UI
|
|
13
|
+
* - Composable panels
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use client';
|
|
17
|
+
|
|
18
|
+
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
19
|
+
import {
|
|
20
|
+
useNetworkState,
|
|
21
|
+
type NetworkState,
|
|
22
|
+
type BandwidthProfile,
|
|
23
|
+
} from '../hooks/useNetworkState';
|
|
24
|
+
import {
|
|
25
|
+
useConflicts,
|
|
26
|
+
type Conflict,
|
|
27
|
+
type ConflictStats,
|
|
28
|
+
} from '../hooks/useConflicts';
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Types
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export interface ServiceWorkerState {
|
|
35
|
+
isSupported: boolean;
|
|
36
|
+
registration: 'none' | 'installing' | 'waiting' | 'active';
|
|
37
|
+
updateAvailable: boolean;
|
|
38
|
+
controller: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CacheInfo {
|
|
42
|
+
name: string;
|
|
43
|
+
itemCount: number;
|
|
44
|
+
sampleUrls: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface QueueStats {
|
|
48
|
+
pending: number;
|
|
49
|
+
syncing: number;
|
|
50
|
+
synced: number;
|
|
51
|
+
failed: number;
|
|
52
|
+
totalBytes: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OfflineDiagnosticsProps {
|
|
56
|
+
/** Show network status panel */
|
|
57
|
+
showNetworkStatus?: boolean;
|
|
58
|
+
/** Show service worker panel */
|
|
59
|
+
showServiceWorker?: boolean;
|
|
60
|
+
/** Show cache management panel */
|
|
61
|
+
showCacheManagement?: boolean;
|
|
62
|
+
/** Show queue statistics panel */
|
|
63
|
+
showQueueStats?: boolean;
|
|
64
|
+
/** Show conflict resolution panel */
|
|
65
|
+
showConflicts?: boolean;
|
|
66
|
+
/** Callback when cache is cleared */
|
|
67
|
+
onClearCache?: (cacheName?: string) => Promise<void>;
|
|
68
|
+
/** CSS class for container */
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Panel Components
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Network Status Panel
|
|
78
|
+
*/
|
|
79
|
+
export function NetworkStatusPanel(): ReactNode {
|
|
80
|
+
const { state, isOnline, isPoor, bandwidth, timeSinceChange, refresh } =
|
|
81
|
+
useNetworkState();
|
|
82
|
+
|
|
83
|
+
const stateColor = {
|
|
84
|
+
online: '#10b981',
|
|
85
|
+
offline: '#ef4444',
|
|
86
|
+
poor: '#f59e0b',
|
|
87
|
+
unknown: '#6b7280',
|
|
88
|
+
}[state];
|
|
89
|
+
|
|
90
|
+
const formatTime = (ms: number) => {
|
|
91
|
+
if (ms < 1000) return `${ms}ms`;
|
|
92
|
+
if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
|
|
93
|
+
return `${Math.floor(ms / 60000)}m`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
98
|
+
<h4
|
|
99
|
+
style={{
|
|
100
|
+
fontSize: '1rem',
|
|
101
|
+
fontWeight: 600,
|
|
102
|
+
marginBottom: '0.75rem',
|
|
103
|
+
display: 'flex',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
gap: '0.5rem',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<span
|
|
109
|
+
style={{
|
|
110
|
+
width: '0.75rem',
|
|
111
|
+
height: '0.75rem',
|
|
112
|
+
borderRadius: '50%',
|
|
113
|
+
backgroundColor: stateColor,
|
|
114
|
+
display: 'inline-block',
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
Network Status
|
|
118
|
+
</h4>
|
|
119
|
+
<div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
|
|
120
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
121
|
+
<span style={{ color: '#6b7280' }}>Status:</span>
|
|
122
|
+
<span style={{ color: stateColor, fontWeight: 500 }}>
|
|
123
|
+
{state.charAt(0).toUpperCase() + state.slice(1)}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
127
|
+
<span style={{ color: '#6b7280' }}>Connection Type:</span>
|
|
128
|
+
<span>{bandwidth.effectiveType || 'Unknown'}</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
131
|
+
<span style={{ color: '#6b7280' }}>Speed:</span>
|
|
132
|
+
<span>
|
|
133
|
+
{bandwidth.speedKbps >= 1024
|
|
134
|
+
? `${(bandwidth.speedKbps / 1024).toFixed(1)} Mbps`
|
|
135
|
+
: `${bandwidth.speedKbps} Kbps`}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
139
|
+
<span style={{ color: '#6b7280' }}>Latency:</span>
|
|
140
|
+
<span>{bandwidth.latencyMs}ms</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
143
|
+
<span style={{ color: '#6b7280' }}>Last Change:</span>
|
|
144
|
+
<span>{formatTime(timeSinceChange)} ago</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<button
|
|
148
|
+
onClick={refresh}
|
|
149
|
+
style={{
|
|
150
|
+
marginTop: '0.75rem',
|
|
151
|
+
padding: '0.375rem 0.75rem',
|
|
152
|
+
backgroundColor: '#e5e7eb',
|
|
153
|
+
border: 'none',
|
|
154
|
+
borderRadius: '0.375rem',
|
|
155
|
+
cursor: 'pointer',
|
|
156
|
+
fontSize: '0.75rem',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
Refresh
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Service Worker Panel
|
|
167
|
+
*/
|
|
168
|
+
export function ServiceWorkerPanel(): ReactNode {
|
|
169
|
+
const [swState, setSwState] = useState<ServiceWorkerState>({
|
|
170
|
+
isSupported: false,
|
|
171
|
+
registration: 'none',
|
|
172
|
+
updateAvailable: false,
|
|
173
|
+
controller: false,
|
|
174
|
+
});
|
|
175
|
+
const [isChecking, setIsChecking] = useState(false);
|
|
176
|
+
|
|
177
|
+
const checkServiceWorker = useCallback(async () => {
|
|
178
|
+
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setSwState((prev) => ({ ...prev, isSupported: true }));
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
186
|
+
|
|
187
|
+
if (!registration) {
|
|
188
|
+
setSwState((prev) => ({ ...prev, registration: 'none' }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let regState: ServiceWorkerState['registration'] = 'none';
|
|
193
|
+
if (registration.active) regState = 'active';
|
|
194
|
+
else if (registration.waiting) regState = 'waiting';
|
|
195
|
+
else if (registration.installing) regState = 'installing';
|
|
196
|
+
|
|
197
|
+
setSwState({
|
|
198
|
+
isSupported: true,
|
|
199
|
+
registration: regState,
|
|
200
|
+
updateAvailable: !!registration.waiting,
|
|
201
|
+
controller: !!navigator.serviceWorker.controller,
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Error checking service worker:', error);
|
|
205
|
+
}
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
checkServiceWorker();
|
|
210
|
+
}, [checkServiceWorker]);
|
|
211
|
+
|
|
212
|
+
const handleCheckUpdate = async () => {
|
|
213
|
+
setIsChecking(true);
|
|
214
|
+
try {
|
|
215
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
216
|
+
if (registration) {
|
|
217
|
+
await registration.update();
|
|
218
|
+
await checkServiceWorker();
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error('Error checking for updates:', error);
|
|
222
|
+
} finally {
|
|
223
|
+
setIsChecking(false);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleUnregister = async () => {
|
|
228
|
+
try {
|
|
229
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
230
|
+
if (registration) {
|
|
231
|
+
await registration.unregister();
|
|
232
|
+
await checkServiceWorker();
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Error unregistering service worker:', error);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const regColor = {
|
|
240
|
+
none: '#6b7280',
|
|
241
|
+
installing: '#f59e0b',
|
|
242
|
+
waiting: '#f59e0b',
|
|
243
|
+
active: '#10b981',
|
|
244
|
+
}[swState.registration];
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
248
|
+
<h4
|
|
249
|
+
style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
|
|
250
|
+
>
|
|
251
|
+
Service Worker
|
|
252
|
+
</h4>
|
|
253
|
+
{!swState.isSupported ? (
|
|
254
|
+
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
|
255
|
+
Service workers are not supported in this browser.
|
|
256
|
+
</p>
|
|
257
|
+
) : (
|
|
258
|
+
<>
|
|
259
|
+
<div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
|
|
260
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
261
|
+
<span style={{ color: '#6b7280' }}>Status:</span>
|
|
262
|
+
<span style={{ color: regColor, fontWeight: 500 }}>
|
|
263
|
+
{swState.registration.charAt(0).toUpperCase() +
|
|
264
|
+
swState.registration.slice(1)}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
268
|
+
<span style={{ color: '#6b7280' }}>Controller:</span>
|
|
269
|
+
<span>{swState.controller ? 'Yes' : 'No'}</span>
|
|
270
|
+
</div>
|
|
271
|
+
{swState.updateAvailable && (
|
|
272
|
+
<div style={{ color: '#f59e0b', fontWeight: 500 }}>
|
|
273
|
+
⚠ Update available
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem' }}>
|
|
278
|
+
<button
|
|
279
|
+
onClick={handleCheckUpdate}
|
|
280
|
+
disabled={isChecking}
|
|
281
|
+
style={{
|
|
282
|
+
padding: '0.375rem 0.75rem',
|
|
283
|
+
backgroundColor: '#e5e7eb',
|
|
284
|
+
border: 'none',
|
|
285
|
+
borderRadius: '0.375rem',
|
|
286
|
+
cursor: isChecking ? 'not-allowed' : 'pointer',
|
|
287
|
+
opacity: isChecking ? 0.5 : 1,
|
|
288
|
+
fontSize: '0.75rem',
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{isChecking ? 'Checking...' : 'Check for Updates'}
|
|
292
|
+
</button>
|
|
293
|
+
<button
|
|
294
|
+
onClick={handleUnregister}
|
|
295
|
+
style={{
|
|
296
|
+
padding: '0.375rem 0.75rem',
|
|
297
|
+
backgroundColor: '#fef2f2',
|
|
298
|
+
color: '#ef4444',
|
|
299
|
+
border: 'none',
|
|
300
|
+
borderRadius: '0.375rem',
|
|
301
|
+
cursor: 'pointer',
|
|
302
|
+
fontSize: '0.75rem',
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
Unregister
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Cache Management Panel
|
|
316
|
+
*/
|
|
317
|
+
export function CacheManagementPanel({
|
|
318
|
+
onClearCache,
|
|
319
|
+
}: {
|
|
320
|
+
onClearCache?: (cacheName?: string) => Promise<void>;
|
|
321
|
+
}): ReactNode {
|
|
322
|
+
const [caches, setCaches] = useState<CacheInfo[]>([]);
|
|
323
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
324
|
+
const [isClearing, setIsClearing] = useState<string | null>(null);
|
|
325
|
+
|
|
326
|
+
const loadCaches = useCallback(async () => {
|
|
327
|
+
if (typeof window === 'undefined' || !('caches' in window)) {
|
|
328
|
+
setIsLoading(false);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const cacheNames = await window.caches.keys();
|
|
334
|
+
const cacheInfos: CacheInfo[] = [];
|
|
335
|
+
|
|
336
|
+
for (const name of cacheNames) {
|
|
337
|
+
const cache = await window.caches.open(name);
|
|
338
|
+
const keys = await cache.keys();
|
|
339
|
+
cacheInfos.push({
|
|
340
|
+
name,
|
|
341
|
+
itemCount: keys.length,
|
|
342
|
+
sampleUrls: keys.slice(0, 5).map((k) => k.url),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
setCaches(cacheInfos);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('Error loading caches:', error);
|
|
349
|
+
} finally {
|
|
350
|
+
setIsLoading(false);
|
|
351
|
+
}
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
loadCaches();
|
|
356
|
+
}, [loadCaches]);
|
|
357
|
+
|
|
358
|
+
const handleClearCache = async (cacheName?: string) => {
|
|
359
|
+
setIsClearing(cacheName || 'all');
|
|
360
|
+
try {
|
|
361
|
+
if (onClearCache) {
|
|
362
|
+
await onClearCache(cacheName);
|
|
363
|
+
} else if (cacheName) {
|
|
364
|
+
await window.caches.delete(cacheName);
|
|
365
|
+
} else {
|
|
366
|
+
const names = await window.caches.keys();
|
|
367
|
+
await Promise.all(names.map((name) => window.caches.delete(name)));
|
|
368
|
+
}
|
|
369
|
+
await loadCaches();
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error('Error clearing cache:', error);
|
|
372
|
+
} finally {
|
|
373
|
+
setIsClearing(null);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
if (!('caches' in window)) {
|
|
378
|
+
return (
|
|
379
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
380
|
+
<h4
|
|
381
|
+
style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
|
|
382
|
+
>
|
|
383
|
+
Cache Storage
|
|
384
|
+
</h4>
|
|
385
|
+
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
|
386
|
+
Cache API is not supported in this browser.
|
|
387
|
+
</p>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
394
|
+
<h4
|
|
395
|
+
style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
|
|
396
|
+
>
|
|
397
|
+
Cache Storage
|
|
398
|
+
</h4>
|
|
399
|
+
{isLoading ? (
|
|
400
|
+
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>Loading...</p>
|
|
401
|
+
) : caches.length === 0 ? (
|
|
402
|
+
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
|
403
|
+
No caches found.
|
|
404
|
+
</p>
|
|
405
|
+
) : (
|
|
406
|
+
<>
|
|
407
|
+
<div style={{ display: 'grid', gap: '0.75rem' }}>
|
|
408
|
+
{caches.map((cache) => (
|
|
409
|
+
<div
|
|
410
|
+
key={cache.name}
|
|
411
|
+
style={{
|
|
412
|
+
padding: '0.75rem',
|
|
413
|
+
backgroundColor: '#f3f4f6',
|
|
414
|
+
borderRadius: '0.375rem',
|
|
415
|
+
fontSize: '0.875rem',
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<div
|
|
419
|
+
style={{
|
|
420
|
+
display: 'flex',
|
|
421
|
+
justifyContent: 'space-between',
|
|
422
|
+
alignItems: 'center',
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
<span style={{ fontWeight: 500 }}>{cache.name}</span>
|
|
426
|
+
<span style={{ color: '#6b7280' }}>
|
|
427
|
+
{cache.itemCount} items
|
|
428
|
+
</span>
|
|
429
|
+
</div>
|
|
430
|
+
<button
|
|
431
|
+
onClick={() => handleClearCache(cache.name)}
|
|
432
|
+
disabled={isClearing === cache.name}
|
|
433
|
+
style={{
|
|
434
|
+
marginTop: '0.5rem',
|
|
435
|
+
padding: '0.25rem 0.5rem',
|
|
436
|
+
backgroundColor: '#fef2f2',
|
|
437
|
+
color: '#ef4444',
|
|
438
|
+
border: 'none',
|
|
439
|
+
borderRadius: '0.25rem',
|
|
440
|
+
cursor:
|
|
441
|
+
isClearing === cache.name ? 'not-allowed' : 'pointer',
|
|
442
|
+
opacity: isClearing === cache.name ? 0.5 : 1,
|
|
443
|
+
fontSize: '0.75rem',
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
{isClearing === cache.name ? 'Clearing...' : 'Clear'}
|
|
447
|
+
</button>
|
|
448
|
+
</div>
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
<button
|
|
452
|
+
onClick={() => handleClearCache()}
|
|
453
|
+
disabled={isClearing === 'all'}
|
|
454
|
+
style={{
|
|
455
|
+
marginTop: '0.75rem',
|
|
456
|
+
padding: '0.375rem 0.75rem',
|
|
457
|
+
backgroundColor: '#fef2f2',
|
|
458
|
+
color: '#ef4444',
|
|
459
|
+
border: 'none',
|
|
460
|
+
borderRadius: '0.375rem',
|
|
461
|
+
cursor: isClearing === 'all' ? 'not-allowed' : 'pointer',
|
|
462
|
+
opacity: isClearing === 'all' ? 0.5 : 1,
|
|
463
|
+
fontSize: '0.75rem',
|
|
464
|
+
}}
|
|
465
|
+
>
|
|
466
|
+
{isClearing === 'all' ? 'Clearing...' : 'Clear All Caches'}
|
|
467
|
+
</button>
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
470
|
+
</div>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Queue Stats Panel
|
|
476
|
+
*/
|
|
477
|
+
export function QueueStatsPanel({ stats }: { stats?: QueueStats }): ReactNode {
|
|
478
|
+
const defaultStats: QueueStats = stats || {
|
|
479
|
+
pending: 0,
|
|
480
|
+
syncing: 0,
|
|
481
|
+
synced: 0,
|
|
482
|
+
failed: 0,
|
|
483
|
+
totalBytes: 0,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const formatBytes = (bytes: number) => {
|
|
487
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
488
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
489
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
494
|
+
<h4
|
|
495
|
+
style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
|
|
496
|
+
>
|
|
497
|
+
Offline Queue
|
|
498
|
+
</h4>
|
|
499
|
+
<div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
|
|
500
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
501
|
+
<span style={{ color: '#6b7280' }}>Pending:</span>
|
|
502
|
+
<span
|
|
503
|
+
style={{ color: defaultStats.pending > 0 ? '#f59e0b' : '#10b981' }}
|
|
504
|
+
>
|
|
505
|
+
{defaultStats.pending}
|
|
506
|
+
</span>
|
|
507
|
+
</div>
|
|
508
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
509
|
+
<span style={{ color: '#6b7280' }}>Syncing:</span>
|
|
510
|
+
<span>{defaultStats.syncing}</span>
|
|
511
|
+
</div>
|
|
512
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
513
|
+
<span style={{ color: '#6b7280' }}>Synced:</span>
|
|
514
|
+
<span style={{ color: '#10b981' }}>{defaultStats.synced}</span>
|
|
515
|
+
</div>
|
|
516
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
517
|
+
<span style={{ color: '#6b7280' }}>Failed:</span>
|
|
518
|
+
<span
|
|
519
|
+
style={{ color: defaultStats.failed > 0 ? '#ef4444' : '#6b7280' }}
|
|
520
|
+
>
|
|
521
|
+
{defaultStats.failed}
|
|
522
|
+
</span>
|
|
523
|
+
</div>
|
|
524
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
525
|
+
<span style={{ color: '#6b7280' }}>Total Size:</span>
|
|
526
|
+
<span>{formatBytes(defaultStats.totalBytes)}</span>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Conflicts Panel
|
|
535
|
+
*/
|
|
536
|
+
export function ConflictsPanel(): ReactNode {
|
|
537
|
+
const { unresolvedConflicts, stats, resolveConflict } = useConflicts();
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
541
|
+
<h4
|
|
542
|
+
style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
|
|
543
|
+
>
|
|
544
|
+
Conflicts
|
|
545
|
+
</h4>
|
|
546
|
+
<div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
|
|
547
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
548
|
+
<span style={{ color: '#6b7280' }}>Total:</span>
|
|
549
|
+
<span>{stats.total}</span>
|
|
550
|
+
</div>
|
|
551
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
552
|
+
<span style={{ color: '#6b7280' }}>Unresolved:</span>
|
|
553
|
+
<span style={{ color: stats.unresolved > 0 ? '#f59e0b' : '#10b981' }}>
|
|
554
|
+
{stats.unresolved}
|
|
555
|
+
</span>
|
|
556
|
+
</div>
|
|
557
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
558
|
+
<span style={{ color: '#6b7280' }}>High Severity:</span>
|
|
559
|
+
<span
|
|
560
|
+
style={{ color: stats.highSeverity > 0 ? '#ef4444' : '#6b7280' }}
|
|
561
|
+
>
|
|
562
|
+
{stats.highSeverity}
|
|
563
|
+
</span>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
{unresolvedConflicts.length > 0 && (
|
|
567
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
568
|
+
<p
|
|
569
|
+
style={{
|
|
570
|
+
fontSize: '0.75rem',
|
|
571
|
+
color: '#6b7280',
|
|
572
|
+
marginBottom: '0.5rem',
|
|
573
|
+
}}
|
|
574
|
+
>
|
|
575
|
+
Unresolved conflicts:
|
|
576
|
+
</p>
|
|
577
|
+
{unresolvedConflicts.slice(0, 3).map((conflict) => (
|
|
578
|
+
<div
|
|
579
|
+
key={conflict.id}
|
|
580
|
+
style={{
|
|
581
|
+
padding: '0.5rem',
|
|
582
|
+
backgroundColor: '#fef3c7',
|
|
583
|
+
borderRadius: '0.25rem',
|
|
584
|
+
marginBottom: '0.5rem',
|
|
585
|
+
fontSize: '0.75rem',
|
|
586
|
+
}}
|
|
587
|
+
>
|
|
588
|
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
589
|
+
<span>{conflict.type}</span>
|
|
590
|
+
<span
|
|
591
|
+
style={{
|
|
592
|
+
color: conflict.severity === 'high' ? '#ef4444' : '#f59e0b',
|
|
593
|
+
}}
|
|
594
|
+
>
|
|
595
|
+
{conflict.severity}
|
|
596
|
+
</span>
|
|
597
|
+
</div>
|
|
598
|
+
<div
|
|
599
|
+
style={{
|
|
600
|
+
display: 'flex',
|
|
601
|
+
gap: '0.25rem',
|
|
602
|
+
marginTop: '0.25rem',
|
|
603
|
+
}}
|
|
604
|
+
>
|
|
605
|
+
<button
|
|
606
|
+
onClick={() => resolveConflict(conflict.id, 'local-wins')}
|
|
607
|
+
style={{
|
|
608
|
+
padding: '0.125rem 0.375rem',
|
|
609
|
+
backgroundColor: '#dbeafe',
|
|
610
|
+
color: '#1d4ed8',
|
|
611
|
+
border: 'none',
|
|
612
|
+
borderRadius: '0.125rem',
|
|
613
|
+
cursor: 'pointer',
|
|
614
|
+
fontSize: '0.625rem',
|
|
615
|
+
}}
|
|
616
|
+
>
|
|
617
|
+
Keep Local
|
|
618
|
+
</button>
|
|
619
|
+
<button
|
|
620
|
+
onClick={() => resolveConflict(conflict.id, 'remote-wins')}
|
|
621
|
+
style={{
|
|
622
|
+
padding: '0.125rem 0.375rem',
|
|
623
|
+
backgroundColor: '#dcfce7',
|
|
624
|
+
color: '#15803d',
|
|
625
|
+
border: 'none',
|
|
626
|
+
borderRadius: '0.125rem',
|
|
627
|
+
cursor: 'pointer',
|
|
628
|
+
fontSize: '0.625rem',
|
|
629
|
+
}}
|
|
630
|
+
>
|
|
631
|
+
Use Remote
|
|
632
|
+
</button>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Main Component
|
|
644
|
+
// ============================================================================
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Comprehensive offline diagnostics panel
|
|
648
|
+
*/
|
|
649
|
+
export function OfflineDiagnostics({
|
|
650
|
+
showNetworkStatus = true,
|
|
651
|
+
showServiceWorker = true,
|
|
652
|
+
showCacheManagement = true,
|
|
653
|
+
showQueueStats = true,
|
|
654
|
+
showConflicts = true,
|
|
655
|
+
onClearCache,
|
|
656
|
+
className,
|
|
657
|
+
}: OfflineDiagnosticsProps): ReactNode {
|
|
658
|
+
return (
|
|
659
|
+
<div className={className} role="region" aria-label="Offline diagnostics">
|
|
660
|
+
<h3
|
|
661
|
+
style={{ fontSize: '1.25rem', fontWeight: 600, marginBottom: '1rem' }}
|
|
662
|
+
>
|
|
663
|
+
Offline Diagnostics
|
|
664
|
+
</h3>
|
|
665
|
+
|
|
666
|
+
{showNetworkStatus && <NetworkStatusPanel />}
|
|
667
|
+
{showServiceWorker && <ServiceWorkerPanel />}
|
|
668
|
+
{showCacheManagement && (
|
|
669
|
+
<CacheManagementPanel onClearCache={onClearCache} />
|
|
670
|
+
)}
|
|
671
|
+
{showQueueStats && <QueueStatsPanel />}
|
|
672
|
+
{showConflicts && <ConflictsPanel />}
|
|
673
|
+
</div>
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export default OfflineDiagnostics;
|