refactor: remove Daily.co, fix IndexedDB sync for stale cache
Daily.co Cleanup: - Remove @daily-co/daily-js and @daily-co/daily-react packages - Remove DailyProvider wrapper from App.tsx - Remove ~380 lines of Daily API endpoints from worker.ts - Remove DAILY_DOMAIN from wrangler configs - Remove Daily env vars from .env.example - Video chat now uses self-hosted Jitsi (meet.jeffemmett.com) Sync Logic Fix: - Fix stale IndexedDB cache preventing server data from loading - Changed threshold from "10x more shapes" to "more shapes" - Server data now properly updates local cache on initial load - Keeps local-only records for offline work Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
fb3edce5a9
commit
2030ae447d
|
|
@ -1,7 +1,6 @@
|
|||
# Frontend (VITE) Public Variables
|
||||
VITE_GOOGLE_CLIENT_ID='your_google_client_id'
|
||||
VITE_GOOGLE_MAPS_API_KEY='your_google_maps_api_key'
|
||||
VITE_DAILY_DOMAIN='your_daily_domain'
|
||||
VITE_TLDRAW_WORKER_URL='your_worker_url'
|
||||
|
||||
# AI Configuration
|
||||
|
|
@ -25,5 +24,4 @@ CLOUDFLARE_API_TOKEN='your_cloudflare_token'
|
|||
CLOUDFLARE_ACCOUNT_ID='your_account_id'
|
||||
CLOUDFLARE_ZONE_ID='your_zone_id'
|
||||
R2_BUCKET_NAME='your_bucket_name'
|
||||
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
||||
DAILY_API_KEY=your_daily_api_key_here
|
||||
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
||||
|
|
@ -18,8 +18,6 @@
|
|||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||
"@automerge/automerge-repo-storage-indexeddb": "^2.5.0",
|
||||
"@chengsokdara/use-whisper": "^0.2.0",
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@fal-ai/client": "^1.7.2",
|
||||
"@mdxeditor/editor": "^3.51.0",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
|
|
@ -3411,40 +3409,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@daily-co/daily-js": {
|
||||
"version": "0.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@daily-co/daily-js/-/daily-js-0.60.0.tgz",
|
||||
"integrity": "sha512-4GkmOKbxZfen4DI6N1HVqj5CiWrg7r8xALgcbwb5V+Ij1h7LHODDDd78XqhzEBBQp4yNjg6U2wz+l/cVznqc4A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@sentry/browser": "^7.60.1",
|
||||
"bowser": "^2.8.1",
|
||||
"dequal": "^2.0.3",
|
||||
"events": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@daily-co/daily-react": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@daily-co/daily-react/-/daily-react-0.20.0.tgz",
|
||||
"integrity": "sha512-Agcp5+nvMtZfej2jzPyl8ExmXG1Kk4ULk6BHz24RvNHYVsri8eJ3NSWZgJBmqPtCuEjLjM3wdo7/e1Aew0sfoA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash.throttle": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@daily-co/daily-js": ">=0.68.0 <1",
|
||||
"react": ">=16.13.1",
|
||||
"recoil": "^0.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
|
|
@ -8025,132 +7989,6 @@
|
|||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.120.4.tgz",
|
||||
"integrity": "sha512-eSwgvTdrh03zYYaI6UVOjI9p4VmKg6+c2+CBQfRZX++6wwnCVsNv7XF7WUIpVGBAkJ0N2oapjQmCzJKGKBRWQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.4",
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.120.4.tgz",
|
||||
"integrity": "sha512-2+W4CgUL1VzrPjArbTid4WhKh7HH21vREVilZdvffQPVwOEpgNTPAb69loQuTlhJVveh9hWTj2nE5UXLbLP+AA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.4",
|
||||
"@sentry/replay": "7.120.4",
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/tracing": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.4.tgz",
|
||||
"integrity": "sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.4",
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.120.4.tgz",
|
||||
"integrity": "sha512-ymlNtIPG6HAKzM/JXpWVGCzCNufZNADfy+O/olZuVJW5Be1DtOFyRnBvz0LeKbmxJbXb2lX/XMhuen6PXPdoQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/feedback": "7.120.4",
|
||||
"@sentry-internal/replay-canvas": "7.120.4",
|
||||
"@sentry-internal/tracing": "7.120.4",
|
||||
"@sentry/core": "7.120.4",
|
||||
"@sentry/integrations": "7.120.4",
|
||||
"@sentry/replay": "7.120.4",
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.4.tgz",
|
||||
"integrity": "sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/integrations": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.4.tgz",
|
||||
"integrity": "sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "7.120.4",
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4",
|
||||
"localforage": "^1.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/replay": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.120.4.tgz",
|
||||
"integrity": "sha512-FW8sPenNFfnO/K7sncsSTX4rIVak9j7VUiLIagJrcqZIC7d1dInFNjy8CdVJUlyz3Y3TOgIl3L3+ZpjfyMnaZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/tracing": "7.120.4",
|
||||
"@sentry/core": "7.120.4",
|
||||
"@sentry/types": "7.120.4",
|
||||
"@sentry/utils": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.4.tgz",
|
||||
"integrity": "sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "7.120.4",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.4.tgz",
|
||||
"integrity": "sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/types": "7.120.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz",
|
||||
|
|
@ -11917,12 +11755,6 @@
|
|||
"base-x": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
|
||||
"integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
|
|
@ -17241,15 +17073,6 @@
|
|||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@
|
|||
"@automerge/automerge-repo-react-hooks": "^2.2.0",
|
||||
"@automerge/automerge-repo-storage-indexeddb": "^2.5.0",
|
||||
"@chengsokdara/use-whisper": "^0.2.0",
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@fal-ai/client": "^1.7.2",
|
||||
"@mdxeditor/editor": "^3.51.0",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
|
|
|
|||
30
src/App.tsx
30
src/App.tsx
|
|
@ -34,11 +34,6 @@ import { Web3Provider } from './providers/Web3Provider';
|
|||
// Import Google Data test component
|
||||
import { GoogleDataTest } from './components/GoogleDataTest';
|
||||
|
||||
// Lazy load Daily.co provider - only needed for video chat
|
||||
const DailyProvider = lazy(() =>
|
||||
import('@daily-co/daily-react').then(m => ({ default: m.DailyProvider }))
|
||||
);
|
||||
|
||||
// Loading skeleton for lazy-loaded routes
|
||||
const LoadingSpinner = () => (
|
||||
<div style={{
|
||||
|
|
@ -69,25 +64,6 @@ const LoadingSpinner = () => (
|
|||
</div>
|
||||
);
|
||||
|
||||
// Daily.co call object - initialized lazily when needed
|
||||
let dailyCallObject: any = null;
|
||||
const getDailyCallObject = async () => {
|
||||
if (dailyCallObject) return dailyCallObject;
|
||||
|
||||
try {
|
||||
// Only create call object if we're in a secure context and mediaDevices is available
|
||||
if (typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
navigator.mediaDevices) {
|
||||
const Daily = (await import('@daily-co/daily-js')).default;
|
||||
dailyCallObject = Daily.createCallObject();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Daily.co call object initialization failed:', error);
|
||||
}
|
||||
return dailyCallObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional Auth Route component
|
||||
* Allows guests to browse, but provides login option
|
||||
|
|
@ -151,8 +127,7 @@ const AppWithProviders = () => {
|
|||
<FileSystemProvider>
|
||||
<NotificationProvider>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<DailyProvider callObject={null}>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter>
|
||||
{/* Display notifications */}
|
||||
<NotificationsDisplay />
|
||||
|
||||
|
|
@ -227,8 +202,7 @@ const AppWithProviders = () => {
|
|||
} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</DailyProvider>
|
||||
</BrowserRouter>
|
||||
</Suspense>
|
||||
</NotificationProvider>
|
||||
</FileSystemProvider>
|
||||
|
|
|
|||
|
|
@ -481,11 +481,11 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
|
|||
const localRecordCount = localDoc?.store ? Object.keys(localDoc.store).length : 0
|
||||
|
||||
// Merge server data with local data
|
||||
// Strategy:
|
||||
// 1. If local has NO SHAPES (only ephemeral records), use server data
|
||||
// 2. If server has SIGNIFICANTLY MORE shapes (10x), prefer server (stale local cache)
|
||||
// 3. Otherwise, only add server records that don't exist locally
|
||||
// (preserve offline changes, let Automerge CRDT sync handle conflicts)
|
||||
// Strategy (IMPROVED):
|
||||
// 1. Server is the source of truth for initial page load
|
||||
// 2. Always update local with server data for shape records
|
||||
// 3. Keep local-only records (potential offline additions not yet synced)
|
||||
// 4. This ensures stale IndexedDB cache doesn't override server data
|
||||
if (serverDoc.store && serverRecordCount > 0) {
|
||||
// Track if we merged any data (needed outside the change callback)
|
||||
let totalMerged = 0
|
||||
|
|
@ -500,46 +500,39 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
|
|||
const localShapeCount = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length
|
||||
const localIsEmpty = Object.keys(doc.store).length === 0
|
||||
|
||||
// Server has significantly more shapes - local is likely stale cache
|
||||
// Use 10x threshold or server has shapes but local has none
|
||||
const serverHasSignificantlyMore = (
|
||||
localShapeCount === 0 && serverShapeCount > 0
|
||||
) || (
|
||||
serverShapeCount > 0 && localShapeCount > 0 && serverShapeCount >= localShapeCount * 10
|
||||
)
|
||||
|
||||
// If local has no shapes but server does, or server has 10x more,
|
||||
// replace local with server data (but keep local ephemeral records)
|
||||
const shouldPreferServer = localIsEmpty || localShapeCount === 0 || serverHasSignificantlyMore
|
||||
// IMPROVED: Server is source of truth on initial load
|
||||
// Prefer server if:
|
||||
// - Local is empty (first load or cleared cache)
|
||||
// - Server has more shapes (local is likely stale/incomplete)
|
||||
// - Local has shapes but server has different/more content
|
||||
const serverHasMoreContent = serverShapeCount > localShapeCount
|
||||
const shouldPreferServer = localIsEmpty || localShapeCount === 0 || serverHasMoreContent
|
||||
|
||||
let addedFromServer = 0
|
||||
let skippedExisting = 0
|
||||
let replacedFromServer = 0
|
||||
let updatedFromServer = 0
|
||||
let keptLocal = 0
|
||||
|
||||
Object.entries(serverDoc.store).forEach(([id, record]) => {
|
||||
if (shouldPreferServer) {
|
||||
// Prefer server data - bootstrap or replace stale local
|
||||
if (doc.store[id]) {
|
||||
replacedFromServer++
|
||||
} else {
|
||||
addedFromServer++
|
||||
}
|
||||
doc.store[id] = record
|
||||
} else if (!doc.store[id]) {
|
||||
// Local has data but missing this record - add from server
|
||||
// This handles: shapes created on another device and synced to R2
|
||||
const existsLocally = !!doc.store[id]
|
||||
|
||||
if (!existsLocally) {
|
||||
// Record doesn't exist locally - add from server
|
||||
doc.store[id] = record
|
||||
addedFromServer++
|
||||
} else if (shouldPreferServer) {
|
||||
// Record exists locally but server has more content - update with server version
|
||||
// This handles stale IndexedDB cache scenarios
|
||||
doc.store[id] = record
|
||||
updatedFromServer++
|
||||
} else {
|
||||
// Record exists locally - preserve local version
|
||||
// The Automerge binary sync will handle merging conflicts via CRDT
|
||||
// This preserves offline edits to existing shapes
|
||||
skippedExisting++
|
||||
// Local has equal or more content - keep local version
|
||||
// Local changes will sync to server via normal CRDT mechanism
|
||||
keptLocal++
|
||||
}
|
||||
})
|
||||
|
||||
totalMerged = addedFromServer + replacedFromServer
|
||||
console.log(`🔄 Server sync: added=${addedFromServer}, replaced=${replacedFromServer}, skipped=${skippedExisting}, shouldPreferServer=${shouldPreferServer}`)
|
||||
totalMerged = addedFromServer + updatedFromServer
|
||||
console.log(`🔄 Server sync: added=${addedFromServer}, updated=${updatedFromServer}, keptLocal=${keptLocal}, serverShapes=${serverShapeCount}, localShapes=${localShapeCount}, preferServer=${shouldPreferServer}`)
|
||||
})
|
||||
|
||||
const finalDoc = handle.doc()
|
||||
|
|
|
|||
382
worker/worker.ts
382
worker/worker.ts
|
|
@ -322,388 +322,6 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
|||
})
|
||||
})
|
||||
|
||||
.post("/daily/rooms", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the request body from the client
|
||||
const body = await req.json()
|
||||
|
||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Get room info by name
|
||||
.get("/daily/rooms/:roomName", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
const { roomName } = req.params
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.post("/daily/tokens", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json() as { room_name: string; properties: any };
|
||||
const response = await fetch('https://api.daily.co/v1/meeting-tokens', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
room_name: body.room_name,
|
||||
properties: body.properties
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add new transcription endpoints
|
||||
.post("/daily/rooms/:roomName/start-transcription", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
const { roomName } = req.params
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}/transcription/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.post("/daily/rooms/:roomName/stop-transcription", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
const { roomName } = req.params
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.daily.co/v1/rooms/${roomName}/transcription/stop`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add endpoint to get transcript access link
|
||||
.get("/daily/transcript/:transcriptId/access-link", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
const { transcriptId } = req.params
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.daily.co/v1/transcript/${transcriptId}/access-link`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add endpoint to get transcript text
|
||||
.get("/daily/transcript/:transcriptId", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
const { transcriptId } = req.params
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.daily.co/v1/transcripts/${transcriptId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Recording endpoints
|
||||
.post("/daily/recordings/start", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json() as any;
|
||||
const response = await fetch('https://api.daily.co/v1/recordings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.post("/daily/recordings/:recordingId/stop", async (req, env) => {
|
||||
// Use server-side API key - never expose to client
|
||||
const apiKey = env.DAILY_API_KEY
|
||||
const { recordingId } = req.params
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response(JSON.stringify({ error: 'Daily.co API key not configured on server' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.daily.co/v1/recordings/${recordingId}/stop`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: (error as Error).message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Fathom API endpoints (api.fathom.ai)
|
||||
.get("/fathom/meetings", async (req) => {
|
||||
console.log('Fathom meetings endpoint called')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ account_id = "0e7b3338d5278ed1b148e6456b940913"
|
|||
|
||||
[vars]
|
||||
# Development environment variables
|
||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
||||
|
||||
[dev]
|
||||
port = 5172
|
||||
|
|
@ -52,6 +51,7 @@ crons = ["0 0 * * *"] # Run at midnight UTC every day
|
|||
|
||||
# Secrets should be set using `wrangler secret put` command for dev environment
|
||||
# DO NOT put these directly in wrangler.toml:
|
||||
# - DAILY_API_KEY
|
||||
# - CLOUDFLARE_API_TOKEN
|
||||
# - FAL_API_KEY
|
||||
# - RUNPOD_API_KEY
|
||||
# etc.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ account_id = "0e7b3338d5278ed1b148e6456b940913"
|
|||
[vars]
|
||||
# Environment variables are managed in Cloudflare Dashboard
|
||||
# Workers & Pages → jeffemmett-canvas → Settings → Variables
|
||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
||||
|
||||
# RunPod AI Service Configuration (defaults hardcoded in src/lib/clientConfig.ts)
|
||||
# These are documented here for reference - actual values are in the client code
|
||||
|
|
@ -77,7 +76,7 @@ name = "jeffemmett-canvas-automerge-dev"
|
|||
compatibility_date = "2024-07-01"
|
||||
|
||||
[env.dev.vars]
|
||||
DAILY_DOMAIN = "mycopunks.daily.co"
|
||||
# Dev environment variables
|
||||
|
||||
[env.dev.durable_objects]
|
||||
bindings = [
|
||||
|
|
@ -106,7 +105,6 @@ crons = ["0 0 * * *"] # Run at midnight UTC every day
|
|||
|
||||
# Secrets should be set using `wrangler secret put` command
|
||||
# DO NOT put these directly in wrangler.toml:
|
||||
# - DAILY_API_KEY
|
||||
# - CLOUDFLARE_API_TOKEN
|
||||
# - FAL_API_KEY # For fal.ai image/video generation proxy
|
||||
# - RUNPOD_API_KEY # For RunPod AI endpoints proxy
|
||||
|
|
|
|||
Loading…
Reference in New Issue