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:
Jeff Emmett 2026-01-24 13:21:20 +01:00
parent fb3edce5a9
commit 2030ae447d
8 changed files with 34 additions and 632 deletions

View File

@ -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'

177
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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>

View File

@ -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()

View File

@ -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')

View File

@ -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.

View File

@ -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