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:
Till JS 2026-03-31 20:16:54 +02:00
parent 6e0dd0c065
commit 6d2509c258
9 changed files with 359 additions and 789 deletions

View 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"]

View file

@ -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}`)
);
});

View file

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

View file

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

View file

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

View file

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