mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(memoro): add deployment infrastructure and migrate web services to new Hono server
- Dockerfile for audio-server (Bun + ffmpeg) - docker-compose.macmini.yml entries for memoro-server (3015) and memoro-audio-server (3016) - Dev commands: dev:memoro:server, dev:memoro:audio-server, dev:memoro:app, dev:memoro:full - MEMORO_* env vars in .env.development - web: add PUBLIC_MEMORO_SERVER_URL env var to env.ts and .env.example - web: rewrite transcriptionService → POST /api/v1/memos (new server path) - web: rewrite spaceService → /api/v1/spaces/* (aligned with actual Hono routes) - server: fix callAudioServer param name audioPath (was filePath) in memos.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6e0dd0c065
commit
6d2509c258
9 changed files with 359 additions and 789 deletions
19
apps/memoro/apps/audio-server/Dockerfile
Normal file
19
apps/memoro/apps/audio-server/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
FROM oven/bun:1 AS production
|
||||
|
||||
# Install ffmpeg
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock* ./
|
||||
RUN bun install --frozen-lockfile 2>/dev/null || bun install
|
||||
|
||||
COPY src ./src
|
||||
COPY tsconfig.json ./
|
||||
|
||||
EXPOSE 3016
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD bun -e "fetch('http://localhost:3016/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
|
|
@ -119,7 +119,7 @@ memoRoutes.post('/:id/append', async (c) => {
|
|||
callAudioServer({
|
||||
memoId,
|
||||
userId,
|
||||
filePath: body.filePath,
|
||||
audioPath: body.filePath,
|
||||
duration: body.duration,
|
||||
recordingIndex,
|
||||
recordingLanguages: body.recordingLanguages,
|
||||
|
|
@ -158,7 +158,7 @@ memoRoutes.post('/:id/retry-transcription', async (c) => {
|
|||
await updateMemoProcessingStatus(memoId, 'transcription', 'pending');
|
||||
|
||||
queueMicrotask(() => {
|
||||
callAudioServer({ memoId, userId, filePath, duration }).catch((err) =>
|
||||
callAudioServer({ memoId, userId, audioPath: filePath, duration }).catch((err) =>
|
||||
console.error(`[memos] Retry transcription failed: ${err}`)
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
PUBLIC_SUPABASE_URL=your-supabase-url
|
||||
PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
|
||||
|
||||
# Middleware Service URLs (required for transcription)
|
||||
# Memoro Server (new Hono/Bun backend — replaces middleware)
|
||||
PUBLIC_MEMORO_SERVER_URL=http://localhost:3015
|
||||
|
||||
# Middleware Service URLs (legacy — kept during transition)
|
||||
PUBLIC_MEMORO_MIDDLEWARE_URL=https://your-memoro-middleware-url
|
||||
PUBLIC_MANA_MIDDLEWARE_URL=https://your-mana-middleware-url
|
||||
PUBLIC_MIDDLEWARE_APP_ID=your-middleware-app-id
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
PUBLIC_SUPABASE_URL,
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_MEMORO_MIDDLEWARE_URL,
|
||||
PUBLIC_MEMORO_SERVER_URL,
|
||||
PUBLIC_MANA_MIDDLEWARE_URL,
|
||||
PUBLIC_MIDDLEWARE_APP_ID,
|
||||
PUBLIC_STORAGE_BUCKET,
|
||||
|
|
@ -25,7 +26,12 @@ export const env = {
|
|||
anonKey: PUBLIC_SUPABASE_ANON_KEY,
|
||||
},
|
||||
|
||||
// Middleware APIs
|
||||
// API servers
|
||||
server: {
|
||||
memoroUrl: PUBLIC_MEMORO_SERVER_URL,
|
||||
},
|
||||
|
||||
// Middleware APIs (legacy — kept for authService compatibility during migration)
|
||||
middleware: {
|
||||
memoroUrl: PUBLIC_MEMORO_MIDDLEWARE_URL,
|
||||
manaUrl: PUBLIC_MANA_MIDDLEWARE_URL,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
/**
|
||||
* Space Service for Memoro Web
|
||||
* Handles space management and collaboration features
|
||||
*
|
||||
* Pattern adapted from memoro_app/features/spaces/services/spaceService.ts
|
||||
* Handles space management and collaboration via memoro-server (Hono/Bun).
|
||||
*/
|
||||
|
||||
import { env } from '$lib/config/env';
|
||||
|
||||
const API = () => env.server.memoroUrl.replace(/\/$/, '');
|
||||
|
||||
// Types
|
||||
|
||||
export interface Memo {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -60,15 +61,11 @@ export interface Space {
|
|||
};
|
||||
};
|
||||
};
|
||||
apps?: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
apps?: { name: string; slug: string };
|
||||
}
|
||||
|
||||
export interface CreateSpaceRequest {
|
||||
name: string;
|
||||
appId?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
|
@ -79,702 +76,196 @@ export interface UpdateSpaceRequest {
|
|||
color?: string;
|
||||
}
|
||||
|
||||
class SpaceService {
|
||||
private apiUrl: string;
|
||||
private appId: string;
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
constructor() {
|
||||
this.apiUrl = env.middleware.memoroUrl.replace(/\/$/, '');
|
||||
this.appId = env.middleware.appId;
|
||||
}
|
||||
function headers(token: string) {
|
||||
return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all spaces for current user from the unified API
|
||||
*/
|
||||
async getSpaces(appToken: string): Promise<Space[]> {
|
||||
async function throwOnError(response: Response, fallback: string) {
|
||||
if (!response.ok) {
|
||||
let msg = fallback;
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `Error fetching spaces: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
// Transform the response to match our Space interface
|
||||
return data.spaces.map((space: any) => ({
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
description: space.description || '',
|
||||
memoCount: space.memo_count || 0,
|
||||
isDefault: space.is_default || false,
|
||||
color: space.color || '#4CAF50',
|
||||
created_at: space.created_at,
|
||||
updated_at: space.updated_at,
|
||||
owner_id: space.owner_id,
|
||||
app_id: space.app_id,
|
||||
credits: space.credits || 0,
|
||||
roles: space.roles,
|
||||
isOwner: space.isOwner || false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch spaces:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific space by ID
|
||||
*/
|
||||
async getSpace(spaceId: string, appToken: string): Promise<Space> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `Error fetching space: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Transform the response to match our Space interface
|
||||
return {
|
||||
id: data.space.id,
|
||||
name: data.space.name,
|
||||
description: data.space.description || '',
|
||||
memoCount: data.space.memo_count || 0,
|
||||
isDefault: data.space.is_default || false,
|
||||
color: data.space.color || '#4CAF50',
|
||||
created_at: data.space.created_at,
|
||||
updated_at: data.space.updated_at,
|
||||
owner_id: data.space.owner_id,
|
||||
app_id: data.space.app_id,
|
||||
credits: data.space.credits || 0,
|
||||
roles: data.space.roles,
|
||||
apps: data.space.apps,
|
||||
isOwner: data.space.isOwner || false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new space
|
||||
*/
|
||||
async createSpace(spaceData: CreateSpaceRequest, appToken: string): Promise<Space> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
// Ensure appId is set
|
||||
if (!spaceData.appId) {
|
||||
spaceData.appId = this.appId;
|
||||
}
|
||||
|
||||
// Prepare request data according to API docs
|
||||
const requestData = {
|
||||
name: spaceData.name,
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `Error creating space: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Create space response:', JSON.stringify(data, null, 2));
|
||||
|
||||
// Handle both formats: {success, spaceId} or {space: {success, spaceId}}
|
||||
const success = data.success || (data.space && data.space.success);
|
||||
const spaceId = data.spaceId || (data.space && data.space.spaceId);
|
||||
|
||||
if (!success) {
|
||||
throw new Error(data.error || (data.space && data.space.error) || 'Failed to create space');
|
||||
}
|
||||
|
||||
if (!spaceId) {
|
||||
throw new Error('No space ID returned from server');
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get the new space details
|
||||
return await this.getSpace(spaceId, appToken);
|
||||
} catch (fetchError) {
|
||||
console.log('Could not fetch newly created space details. Creating fallback space object.');
|
||||
|
||||
// Create a fallback space object with minimal information
|
||||
return {
|
||||
id: spaceId,
|
||||
name: spaceData.name,
|
||||
description: '',
|
||||
memoCount: 0,
|
||||
isDefault: false,
|
||||
color: spaceData.color || '#4CAF50',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create space:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing space
|
||||
*/
|
||||
async updateSpace(
|
||||
spaceId: string,
|
||||
spaceData: UpdateSpaceRequest,
|
||||
appToken: string
|
||||
): Promise<Space> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(spaceData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `Error updating space: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to update space');
|
||||
}
|
||||
|
||||
// Get the updated space details
|
||||
return this.getSpace(spaceId, appToken);
|
||||
} catch (error) {
|
||||
console.error(`Failed to update space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a space
|
||||
*/
|
||||
async deleteSpace(spaceId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.log(`Deleting space at: ${this.apiUrl}/memoro/spaces/${spaceId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error response status: ${response.status}`);
|
||||
let errorMessage = `Error deleting space: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to delete space');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave a space (for non-owners)
|
||||
*/
|
||||
async leaveSpace(spaceId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.log(`Leaving space at: ${this.apiUrl}/memoro/spaces/${spaceId}/leave`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/leave`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error response status: ${response.status}`);
|
||||
let errorMessage = `Error leaving space: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to leave space');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to leave space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all memos for a specific space
|
||||
*/
|
||||
async getSpaceMemos(spaceId: string, appToken: string): Promise<Memo[]> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`Fetching memos for space ${spaceId} from ${this.apiUrl}/memoro/spaces/${spaceId}/memos`
|
||||
);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/memos`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Error fetching space memos: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If response is not JSON, use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.memos || [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch memos for space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a memo to a space
|
||||
*/
|
||||
async linkMemoToSpace(memoId: string, spaceId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.log(`Linking memo ${memoId} to space ${spaceId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/link-memo`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ memoId, spaceId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error response status: ${response.status}`);
|
||||
let errorMessage = `Error linking memo to space: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.success;
|
||||
} catch (error) {
|
||||
console.error(`Failed to link memo ${memoId} to space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a memo from a space
|
||||
*/
|
||||
async unlinkMemoFromSpace(memoId: string, spaceId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.log(`Unlinking memo ${memoId} from space ${spaceId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/unlink-memo`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ memoId, spaceId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error response status: ${response.status}`);
|
||||
let errorMessage = `Error unlinking memo from space: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.success;
|
||||
} catch (error) {
|
||||
console.error(`Failed to unlink memo ${memoId} from space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending invites for a space
|
||||
*/
|
||||
async getSpaceInvites(spaceId: string, appToken: string): Promise<SpaceInvite[]> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(`Fetching invites for space ${spaceId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/invites`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to get space invites: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.invites || [];
|
||||
} catch (error) {
|
||||
console.error(`Failed to get invites for space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to a space by email
|
||||
*/
|
||||
async inviteUserToSpace(
|
||||
spaceId: string,
|
||||
email: string,
|
||||
role: string,
|
||||
appToken: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(`Inviting user ${email} to space ${spaceId} with role ${role}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/${spaceId}/invite`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, role }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to invite user to space: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.inviteId;
|
||||
} catch (error) {
|
||||
console.error(`Failed to invite user to space ${spaceId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend a space invitation
|
||||
*/
|
||||
async resendInvite(inviteId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(`Resending invite ${inviteId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/${inviteId}/resend`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to resend invitation: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to resend invite ${inviteId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a space invitation
|
||||
*/
|
||||
async acceptInvite(inviteId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(`Accepting invite ${inviteId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/accept`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ inviteId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to accept invitation: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to accept invite ${inviteId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decline a space invitation
|
||||
*/
|
||||
async declineInvite(inviteId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(`Declining invite ${inviteId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/decline`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ inviteId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to decline invitation: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to decline invite ${inviteId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a space invitation (for space owners/admins)
|
||||
*/
|
||||
async cancelInvite(inviteId: string, appToken: string): Promise<boolean> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug(`Canceling invite ${inviteId}`);
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/spaces/invites/cancel`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ inviteId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to cancel invitation: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to cancel invite ${inviteId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all invitations for the current user
|
||||
*/
|
||||
async getUserInvites(appToken: string): Promise<SpaceInvite[]> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
console.debug('Getting user invites');
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/memoro/invites/pending`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to get user invitations: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (jsonError) {
|
||||
// If we can't parse JSON, just use the status text
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('User invites:', data);
|
||||
return data.invites || [];
|
||||
} catch (error) {
|
||||
console.error('Failed to get user invites:', error);
|
||||
throw error;
|
||||
}
|
||||
const d = await response.json();
|
||||
msg = d.error || d.message || msg;
|
||||
} catch {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function mapSpace(s: any): Space {
|
||||
return {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
description: s.description || '',
|
||||
memoCount: s.memo_count || 0,
|
||||
isDefault: s.is_default || false,
|
||||
color: s.color || '#4CAF50',
|
||||
created_at: s.created_at,
|
||||
updated_at: s.updated_at,
|
||||
owner_id: s.owner_id,
|
||||
app_id: s.app_id,
|
||||
credits: s.credits || 0,
|
||||
roles: s.roles,
|
||||
apps: s.apps,
|
||||
isOwner: s.isOwner || false,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Service ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class SpaceService {
|
||||
async getSpaces(token: string): Promise<Space[]> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces`, { headers: headers(token) });
|
||||
await throwOnError(r, 'Error fetching spaces');
|
||||
const d = await r.json();
|
||||
return (d.spaces ?? []).map(mapSpace);
|
||||
}
|
||||
|
||||
async getSpace(spaceId: string, token: string): Promise<Space> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, { headers: headers(token) });
|
||||
await throwOnError(r, 'Error fetching space');
|
||||
const d = await r.json();
|
||||
return mapSpace(d.space);
|
||||
}
|
||||
|
||||
async createSpace(spaceData: CreateSpaceRequest, token: string): Promise<Space> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify({ name: spaceData.name, description: spaceData.description }),
|
||||
});
|
||||
await throwOnError(r, 'Error creating space');
|
||||
const d = await r.json();
|
||||
return mapSpace(d.space);
|
||||
}
|
||||
|
||||
async updateSpace(spaceId: string, spaceData: UpdateSpaceRequest, token: string): Promise<Space> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, {
|
||||
method: 'PUT',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify(spaceData),
|
||||
});
|
||||
await throwOnError(r, 'Error updating space');
|
||||
return this.getSpace(spaceId, token);
|
||||
}
|
||||
|
||||
async deleteSpace(spaceId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: headers(token),
|
||||
});
|
||||
await throwOnError(r, 'Error deleting space');
|
||||
return true;
|
||||
}
|
||||
|
||||
async leaveSpace(spaceId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/leave`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
});
|
||||
await throwOnError(r, 'Error leaving space');
|
||||
return true;
|
||||
}
|
||||
|
||||
async getSpaceMemos(spaceId: string, token: string): Promise<Memo[]> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos`, {
|
||||
headers: headers(token),
|
||||
});
|
||||
await throwOnError(r, 'Error fetching space memos');
|
||||
const d = await r.json();
|
||||
return d.memos ?? [];
|
||||
}
|
||||
|
||||
async linkMemoToSpace(memoId: string, spaceId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos/link`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify({ memoId }),
|
||||
});
|
||||
await throwOnError(r, 'Error linking memo to space');
|
||||
return true;
|
||||
}
|
||||
|
||||
async unlinkMemoFromSpace(memoId: string, spaceId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/memos/unlink`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify({ memoId }),
|
||||
});
|
||||
await throwOnError(r, 'Error unlinking memo from space');
|
||||
return true;
|
||||
}
|
||||
|
||||
async getSpaceInvites(spaceId: string, token: string): Promise<SpaceInvite[]> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/invites`, {
|
||||
headers: headers(token),
|
||||
});
|
||||
await throwOnError(r, 'Error fetching space invites');
|
||||
const d = await r.json();
|
||||
return d.invites ?? [];
|
||||
}
|
||||
|
||||
async inviteUserToSpace(
|
||||
spaceId: string,
|
||||
email: string,
|
||||
_role: string,
|
||||
token: string
|
||||
): Promise<string> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/${spaceId}/invite`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
await throwOnError(r, 'Error inviting user to space');
|
||||
const d = await r.json();
|
||||
return d.invite?.id ?? d.inviteId;
|
||||
}
|
||||
|
||||
async resendInvite(inviteId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/invites/${inviteId}/resend`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
});
|
||||
await throwOnError(r, 'Error resending invite');
|
||||
return true;
|
||||
}
|
||||
|
||||
async cancelInvite(inviteId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/spaces/invites/${inviteId}`, {
|
||||
method: 'DELETE',
|
||||
headers: headers(token),
|
||||
});
|
||||
await throwOnError(r, 'Error canceling invite');
|
||||
return true;
|
||||
}
|
||||
|
||||
async acceptInvite(inviteId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/invites/accept`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify({ inviteId }),
|
||||
});
|
||||
await throwOnError(r, 'Error accepting invite');
|
||||
return true;
|
||||
}
|
||||
|
||||
async declineInvite(inviteId: string, token: string): Promise<boolean> {
|
||||
const r = await fetch(`${API()}/api/v1/invites/decline`, {
|
||||
method: 'POST',
|
||||
headers: headers(token),
|
||||
body: JSON.stringify({ inviteId }),
|
||||
});
|
||||
await throwOnError(r, 'Error declining invite');
|
||||
return true;
|
||||
}
|
||||
|
||||
async getUserInvites(token: string): Promise<SpaceInvite[]> {
|
||||
const r = await fetch(`${API()}/api/v1/invites/pending`, { headers: headers(token) });
|
||||
await throwOnError(r, 'Error fetching user invites');
|
||||
const d = await r.json();
|
||||
return d.invites ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const spaceService = new SpaceService();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
/**
|
||||
* Transcription Service for Memoro Web
|
||||
* Handles audio transcription via memoro-service middleware
|
||||
*
|
||||
* Pattern adapted from memoro_app/features/storage/transcriptionUtils.ts
|
||||
* Triggers transcription via the new Hono/Bun memoro-server.
|
||||
*/
|
||||
|
||||
import { env } from '$lib/config/env';
|
||||
|
||||
const MEMORO_SERVICE_URL = env.middleware.memoroUrl.replace(/\/$/, '');
|
||||
const SERVER_URL = env.server.memoroUrl.replace(/\/$/, '');
|
||||
|
||||
/**
|
||||
* Enhanced transcription result with network error information
|
||||
*/
|
||||
export interface TranscriptionRequestResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
|
@ -32,44 +27,6 @@ export interface TranscriptionParams {
|
|||
mediaType?: 'audio' | 'video';
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcribes via Memoro Service (which handles intelligent routing)
|
||||
*/
|
||||
async function transcribeViaMemoryService(
|
||||
audioPath: string,
|
||||
appToken: string,
|
||||
duration: number,
|
||||
memoId?: string,
|
||||
spaceId?: string,
|
||||
recordingLanguages?: string[],
|
||||
title?: string,
|
||||
blueprintId?: string,
|
||||
mediaType?: 'audio' | 'video'
|
||||
): Promise<Response> {
|
||||
console.debug('🎯 Using Memoro Service for intelligent transcription routing');
|
||||
|
||||
return fetch(`${MEMORO_SERVICE_URL}/memoro/process-uploaded-audio`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath: audioPath,
|
||||
duration,
|
||||
memoId,
|
||||
spaceId,
|
||||
recordingLanguages,
|
||||
title,
|
||||
blueprintId,
|
||||
mediaType,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers transcription via memoro-service (which handles intelligent routing)
|
||||
*/
|
||||
export async function triggerTranscription({
|
||||
userId,
|
||||
fileName,
|
||||
|
|
@ -79,63 +36,47 @@ export async function triggerTranscription({
|
|||
recordingLanguages,
|
||||
title,
|
||||
blueprintId,
|
||||
appToken,
|
||||
accessToken,
|
||||
mediaType,
|
||||
}: TranscriptionParams & { appToken: string }): Promise<TranscriptionRequestResult> {
|
||||
}: TranscriptionParams & { accessToken: string }): Promise<TranscriptionRequestResult> {
|
||||
try {
|
||||
if (!appToken) {
|
||||
const errorMsg = 'No authenticated token found';
|
||||
return { success: false, error: errorMsg };
|
||||
if (!accessToken) {
|
||||
return { success: false, error: 'No authenticated token found' };
|
||||
}
|
||||
|
||||
const audioPath = `${userId}/${fileName}`;
|
||||
const filePath = `${userId}/${fileName}`;
|
||||
|
||||
console.debug('🎯 Triggering transcription with:', {
|
||||
audioPath,
|
||||
duration,
|
||||
memoId,
|
||||
spaceId,
|
||||
title,
|
||||
blueprintId,
|
||||
recordingLanguages,
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/memos`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filePath,
|
||||
duration,
|
||||
memoId,
|
||||
spaceId,
|
||||
recordingLanguages,
|
||||
title,
|
||||
blueprintId,
|
||||
mediaType,
|
||||
}),
|
||||
});
|
||||
|
||||
// Let memoro-service handle the intelligent routing
|
||||
const transcribeResponse = await transcribeViaMemoryService(
|
||||
audioPath,
|
||||
appToken,
|
||||
duration,
|
||||
memoId,
|
||||
spaceId,
|
||||
recordingLanguages,
|
||||
title,
|
||||
blueprintId,
|
||||
mediaType
|
||||
);
|
||||
|
||||
// Handle response
|
||||
if (!transcribeResponse.ok) {
|
||||
const errorText = await transcribeResponse.text();
|
||||
console.debug('Error calling transcription via Memoro Service:', {
|
||||
status: transcribeResponse.status,
|
||||
statusText: transcribeResponse.statusText,
|
||||
errorText,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return {
|
||||
success: false,
|
||||
error: errorText,
|
||||
isNetworkError: transcribeResponse.status >= 500,
|
||||
isNetworkError: response.status >= 500,
|
||||
userMessage: 'Transcription could not be started',
|
||||
technicalMessage: errorText,
|
||||
};
|
||||
}
|
||||
|
||||
console.debug('✅ Transcription started successfully via Memoro Service');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.debug('Error triggering transcription:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: String(error),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue