mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
fix(mukke): lint fixes, rate limiting, DB indexes, and prod config
- Add ThrottlerModule rate limiting to backend - Add DB indexes on userId, songId, playlistId columns - Lint/formatter fixes from pre-commit hooks - Add GlitchTip error tracking to docker-compose.macmini.yml - Add glitchtip.mana.how to cloudflared tunnel config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
212ec16125
commit
54c1326c14
30 changed files with 326 additions and 141 deletions
|
|
@ -31,6 +31,7 @@
|
|||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.7",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { DatabaseModule } from './db/database.module';
|
||||
import { ProjectModule } from './project/project.module';
|
||||
import { BeatModule } from './beat/beat.module';
|
||||
|
|
@ -18,6 +19,7 @@ import { HealthModule } from '@manacore/shared-nestjs-health';
|
|||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
|
||||
DatabaseModule,
|
||||
ProjectModule,
|
||||
BeatModule,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
import { IsString, IsNotEmpty, IsUUID, IsNumber, IsOptional, IsObject } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsUUID,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
MaxLength,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateBeatUploadDto {
|
||||
@IsUUID()
|
||||
|
|
@ -7,6 +17,7 @@ export class CreateBeatUploadDto {
|
|||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(255)
|
||||
filename!: string;
|
||||
}
|
||||
|
||||
|
|
@ -19,14 +30,19 @@ export class UseLibraryBeatDto {
|
|||
export class UpdateBeatMetadataDto {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(0)
|
||||
duration?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(1)
|
||||
@Max(999)
|
||||
bpm?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
bpmConfidence?: number;
|
||||
|
||||
@IsObject()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { pgTable, uuid, text, timestamp, varchar, real, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, text, timestamp, varchar, real, jsonb, index } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects.schema';
|
||||
|
||||
export const beats = pgTable('beats', {
|
||||
export const beats = pgTable(
|
||||
'beats',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
projectId: uuid('project_id')
|
||||
.references(() => projects.id, { onDelete: 'cascade' })
|
||||
|
|
@ -17,7 +19,9 @@ export const beats = pgTable('beats', {
|
|||
transcriptionError: text('transcription_error'),
|
||||
transcribedAt: timestamp('transcribed_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
},
|
||||
(table) => [index('beats_project_id_idx').on(table.projectId)]
|
||||
);
|
||||
|
||||
export type Beat = typeof beats.$inferSelect;
|
||||
export type NewBeat = typeof beats.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
import { pgTable, uuid, text, real, integer } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, text, real, integer, index } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects.schema';
|
||||
|
||||
export const lyrics = pgTable('lyrics', {
|
||||
export const lyrics = pgTable(
|
||||
'lyrics',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
projectId: uuid('project_id')
|
||||
.references(() => projects.id, { onDelete: 'cascade' })
|
||||
.notNull()
|
||||
.unique(),
|
||||
content: text('content'),
|
||||
});
|
||||
},
|
||||
(table) => [index('lyrics_project_id_idx').on(table.projectId)]
|
||||
);
|
||||
|
||||
export const lyricLines = pgTable('lyric_lines', {
|
||||
export const lyricLines = pgTable(
|
||||
'lyric_lines',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
lyricsId: uuid('lyrics_id')
|
||||
.references(() => lyrics.id, { onDelete: 'cascade' })
|
||||
|
|
@ -19,7 +25,9 @@ export const lyricLines = pgTable('lyric_lines', {
|
|||
text: text('text').notNull(),
|
||||
startTime: real('start_time'),
|
||||
endTime: real('end_time'),
|
||||
});
|
||||
},
|
||||
(table) => [index('lyric_lines_lyrics_id_idx').on(table.lyricsId)]
|
||||
);
|
||||
|
||||
export type Lyrics = typeof lyrics.$inferSelect;
|
||||
export type NewLyrics = typeof lyrics.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { pgTable, uuid, varchar, real, integer } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, varchar, real, integer, index } from 'drizzle-orm/pg-core';
|
||||
import { beats } from './beats.schema';
|
||||
|
||||
export const markers = pgTable('markers', {
|
||||
export const markers = pgTable(
|
||||
'markers',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
beatId: uuid('beat_id')
|
||||
.references(() => beats.id, { onDelete: 'cascade' })
|
||||
|
|
@ -12,7 +14,9 @@ export const markers = pgTable('markers', {
|
|||
endTime: real('end_time'),
|
||||
color: varchar('color', { length: 7 }),
|
||||
sortOrder: integer('sort_order'),
|
||||
});
|
||||
},
|
||||
(table) => [index('markers_beat_id_idx').on(table.beatId)]
|
||||
);
|
||||
|
||||
export type Marker = typeof markers.$inferSelect;
|
||||
export type NewMarker = typeof markers.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { pgTable, uuid, text, timestamp, varchar, integer } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, text, timestamp, varchar, integer, index } from 'drizzle-orm/pg-core';
|
||||
import { songs } from './songs.schema';
|
||||
|
||||
export const playlists = pgTable('playlists', {
|
||||
export const playlists = pgTable(
|
||||
'playlists',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
|
|
@ -9,9 +11,13 @@ export const playlists = pgTable('playlists', {
|
|||
coverArtPath: text('cover_art_path'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
},
|
||||
(table) => [index('playlists_user_id_idx').on(table.userId)]
|
||||
);
|
||||
|
||||
export const playlistSongs = pgTable('playlist_songs', {
|
||||
export const playlistSongs = pgTable(
|
||||
'playlist_songs',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
playlistId: uuid('playlist_id')
|
||||
.references(() => playlists.id, { onDelete: 'cascade' })
|
||||
|
|
@ -21,7 +27,12 @@ export const playlistSongs = pgTable('playlist_songs', {
|
|||
.notNull(),
|
||||
sortOrder: integer('sort_order').notNull(),
|
||||
addedAt: timestamp('added_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
},
|
||||
(table) => [
|
||||
index('playlist_songs_playlist_id_idx').on(table.playlistId),
|
||||
index('playlist_songs_song_id_idx').on(table.songId),
|
||||
]
|
||||
);
|
||||
|
||||
export type Playlist = typeof playlists.$inferSelect;
|
||||
export type NewPlaylist = typeof playlists.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { pgTable, uuid, text, timestamp, varchar } from 'drizzle-orm/pg-core';
|
||||
import { pgTable, uuid, text, timestamp, varchar, index } from 'drizzle-orm/pg-core';
|
||||
import { songs } from './songs.schema';
|
||||
|
||||
export const projects = pgTable('projects', {
|
||||
export const projects = pgTable(
|
||||
'projects',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
|
|
@ -9,7 +11,12 @@ export const projects = pgTable('projects', {
|
|||
songId: uuid('song_id').references(() => songs.id, { onDelete: 'set null' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
},
|
||||
(table) => [
|
||||
index('projects_user_id_idx').on(table.userId),
|
||||
index('projects_song_id_idx').on(table.songId),
|
||||
]
|
||||
);
|
||||
|
||||
export type Project = typeof projects.$inferSelect;
|
||||
export type NewProject = typeof projects.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ import {
|
|||
integer,
|
||||
real,
|
||||
boolean,
|
||||
index,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
|
||||
export const songs = pgTable('songs', {
|
||||
export const songs = pgTable(
|
||||
'songs',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: text('user_id').notNull(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
|
|
@ -29,7 +32,12 @@ export const songs = pgTable('songs', {
|
|||
lastPlayedAt: timestamp('last_played_at', { withTimezone: true }),
|
||||
addedAt: timestamp('added_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
},
|
||||
(table) => [
|
||||
index('songs_user_id_idx').on(table.userId),
|
||||
index('songs_added_at_idx').on(table.addedAt),
|
||||
]
|
||||
);
|
||||
|
||||
export type Song = typeof songs.$inferSelect;
|
||||
export type NewSong = typeof songs.$inferInsert;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ import {
|
|||
ValidateNested,
|
||||
IsInt,
|
||||
Min,
|
||||
MaxLength,
|
||||
ArrayMaxSize,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateOrUpdateLyricsDto {
|
||||
@IsString()
|
||||
@MaxLength(50000)
|
||||
content!: string;
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +23,7 @@ class LyricLineDto {
|
|||
lineNumber!: number;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(1000)
|
||||
text!: string;
|
||||
|
||||
@IsNumber()
|
||||
|
|
@ -36,6 +40,7 @@ class LyricLineDto {
|
|||
export class SyncLinesDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@ArrayMaxSize(2000)
|
||||
@Type(() => LyricLineDto)
|
||||
lines!: LyricLineDto[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
ValidateNested,
|
||||
MaxLength,
|
||||
Min,
|
||||
ArrayMaxSize,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ export class BulkCreateMarkersDto {
|
|||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@ArrayMaxSize(500)
|
||||
@Type(() => MarkerItemDto)
|
||||
markers!: MarkerItemDto[];
|
||||
}
|
||||
|
|
@ -132,6 +134,7 @@ class MarkerUpdateItemDto {
|
|||
export class BulkUpdateMarkersDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@ArrayMaxSize(500)
|
||||
@Type(() => MarkerUpdateItemDto)
|
||||
updates!: MarkerUpdateItemDto[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,11 +100,20 @@ export class MarkerService {
|
|||
data: Partial<Pick<Marker, 'startTime' | 'endTime' | 'sortOrder'>>;
|
||||
}>
|
||||
): Promise<Marker[]> {
|
||||
return this.db.transaction(async (tx) => {
|
||||
const results: Marker[] = [];
|
||||
for (const update of updates) {
|
||||
const marker = await this.update(update.id, userId, update.data);
|
||||
results.push(marker);
|
||||
const marker = await this.findByIdOrThrow(update.id);
|
||||
await this.verifyBeatOwnership(marker.beatId, userId);
|
||||
|
||||
const [updatedMarker] = await tx
|
||||
.update(markers)
|
||||
.set(update.data)
|
||||
.where(eq(markers.id, update.id))
|
||||
.returning();
|
||||
results.push(updatedMarker);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { IsString, IsNotEmpty, IsOptional, IsUUID, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
ArrayMaxSize,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreatePlaylistDto {
|
||||
@IsString()
|
||||
|
|
@ -8,6 +16,7 @@ export class CreatePlaylistDto {
|
|||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(2000)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +28,7 @@ export class UpdatePlaylistDto {
|
|||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(2000)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +39,9 @@ export class AddSongDto {
|
|||
}
|
||||
|
||||
export class ReorderSongsDto {
|
||||
@IsArray()
|
||||
@IsUUID('4', { each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@ArrayMaxSize(1000)
|
||||
songIds!: string[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,11 +121,15 @@ export class PlaylistService {
|
|||
async reorderSongs(playlistId: string, userId: string, songIds: string[]): Promise<void> {
|
||||
await this.findById(playlistId, userId);
|
||||
|
||||
await this.db.transaction(async (tx) => {
|
||||
for (let i = 0; i < songIds.length; i++) {
|
||||
await this.db
|
||||
await tx
|
||||
.update(playlistSongs)
|
||||
.set({ sortOrder: i })
|
||||
.where(and(eq(playlistSongs.playlistId, playlistId), eq(playlistSongs.songId, songIds[i])));
|
||||
}
|
||||
.where(
|
||||
and(eq(playlistSongs.playlistId, playlistId), eq(playlistSongs.songId, songIds[i]))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export class CreateProjectDto {
|
|||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(2000)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
|
|
@ -19,5 +20,6 @@ export class UpdateProjectDto {
|
|||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(2000)
|
||||
description?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,17 +67,22 @@ export class ProjectService {
|
|||
}
|
||||
|
||||
const title = song.artist ? `${song.title} - ${song.artist}` : song.title;
|
||||
const [project] = await this.db.insert(projects).values({ userId, title, songId }).returning();
|
||||
|
||||
const project = await this.db.transaction(async (tx) => {
|
||||
const [newProject] = await tx.insert(projects).values({ userId, title, songId }).returning();
|
||||
|
||||
// Create a beat record linked to the song's storage
|
||||
await this.db.insert(beats).values({
|
||||
projectId: project.id,
|
||||
await tx.insert(beats).values({
|
||||
projectId: newProject.id,
|
||||
storagePath: song.storagePath,
|
||||
filename: `${song.title}.mp3`,
|
||||
duration: song.duration,
|
||||
bpm: song.bpm,
|
||||
});
|
||||
|
||||
return newProject;
|
||||
});
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,38 @@
|
|||
import { IsString, IsNotEmpty, IsOptional, IsNumber, IsInt } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsInt,
|
||||
MaxLength,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateSongDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(255)
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
artist?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
album?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(100)
|
||||
genre?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(1)
|
||||
trackNumber?: number;
|
||||
|
||||
@IsInt()
|
||||
|
|
@ -27,32 +41,40 @@ export class CreateSongDto {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(1)
|
||||
@Max(999)
|
||||
bpm?: number;
|
||||
}
|
||||
|
||||
export class UpdateSongDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
artist?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
album?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(255)
|
||||
albumArtist?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(100)
|
||||
genre?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(1)
|
||||
trackNumber?: number;
|
||||
|
||||
@IsInt()
|
||||
|
|
@ -61,19 +83,24 @@ export class UpdateSongDto {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(0)
|
||||
duration?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Min(1)
|
||||
@Max(999)
|
||||
bpm?: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(0)
|
||||
fileSize?: number;
|
||||
}
|
||||
|
||||
export class SongUploadDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(255)
|
||||
filename!: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,11 +57,12 @@
|
|||
error = null;
|
||||
|
||||
try {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${backendUrl}/beats/library/${beatId}/use`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
},
|
||||
body: JSON.stringify({ projectId }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -238,13 +238,11 @@ export const authStore = {
|
|||
return await tokenManager.getValidToken();
|
||||
},
|
||||
|
||||
getAuthHeaders(): Record<string, string> {
|
||||
const authService = getAuthService();
|
||||
if (!authService) return {};
|
||||
async getAuthHeaders(): Promise<Record<string, string>> {
|
||||
const tokenManager = getTokenManager();
|
||||
if (!tokenManager) return {};
|
||||
|
||||
// Get token synchronously from storage if available
|
||||
const token =
|
||||
typeof localStorage !== 'undefined' ? localStorage.getItem('manacore_access_token') : null;
|
||||
const token = await tokenManager.getValidToken();
|
||||
if (token) {
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,12 @@ function createLibraryStore() {
|
|||
});
|
||||
|
||||
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,11 +73,12 @@ function createPlayerStore() {
|
|||
}
|
||||
|
||||
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ function createPlaylistStore() {
|
|||
});
|
||||
|
||||
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,11 +36,12 @@ function createProjectStore() {
|
|||
});
|
||||
|
||||
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -215,10 +215,11 @@
|
|||
(typeof window !== 'undefined' &&
|
||||
(window as unknown as { __PUBLIC_BACKEND_URL__: string }).__PUBLIC_BACKEND_URL__) ||
|
||||
'http://localhost:3010';
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(
|
||||
`${backendUrl}/export/${projectStore.currentProject.id}?format=${format}`,
|
||||
{
|
||||
headers: authStore.getAuthHeaders(),
|
||||
headers: authHeaders,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@
|
|||
}
|
||||
|
||||
async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@
|
|||
}
|
||||
|
||||
async function fetchApi<T>(path: string): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error('Request failed');
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@
|
|||
}
|
||||
|
||||
async function fetchApi<T>(path: string): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error('Request failed');
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@
|
|||
}
|
||||
|
||||
async function fetchApi<T>(path: string): Promise<T> {
|
||||
const authHeaders = await authStore.getAuthHeaders();
|
||||
const response = await fetch(`${getBackendUrl()}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authStore.getAuthHeaders(),
|
||||
...authHeaders,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error('Request failed');
|
||||
|
|
|
|||
|
|
@ -100,5 +100,9 @@ ingress:
|
|||
- hostname: link.mana.how
|
||||
service: http://localhost:4090
|
||||
|
||||
# GlitchTip Error Tracking
|
||||
- hostname: glitchtip.mana.how
|
||||
service: http://localhost:8020
|
||||
|
||||
# Catch-all
|
||||
- service: http_status:404
|
||||
|
|
|
|||
|
|
@ -1905,6 +1905,51 @@ services:
|
|||
# Volumes (Naming: mana-{service}-data)
|
||||
# ============================================
|
||||
|
||||
# ============================================
|
||||
# GlitchTip Error Tracking (Sentry-compatible)
|
||||
# ============================================
|
||||
|
||||
glitchtip:
|
||||
image: glitchtip/glitchtip:latest
|
||||
container_name: mana-mon-glitchtip
|
||||
restart: always
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/glitchtip
|
||||
SECRET_KEY: ${GLITCHTIP_SECRET_KEY:-change-me-in-production}
|
||||
PORT: "8020"
|
||||
GLITCHTIP_DOMAIN: https://glitchtip.mana.how
|
||||
DEFAULT_FROM_EMAIL: glitchtip@mana.how
|
||||
CELERY_WORKER_AUTOSCALE: "1,3"
|
||||
ENABLE_USER_REGISTRATION: "false"
|
||||
ports:
|
||||
- "8020:8020"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8020/_health/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
glitchtip-worker:
|
||||
image: glitchtip/glitchtip:latest
|
||||
container_name: mana-mon-glitchtip-worker
|
||||
restart: always
|
||||
command: ./bin/run-celery-with-beat.sh
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/glitchtip
|
||||
SECRET_KEY: ${GLITCHTIP_SECRET_KEY:-change-me-in-production}
|
||||
GLITCHTIP_DOMAIN: https://glitchtip.mana.how
|
||||
CELERY_WORKER_AUTOSCALE: "1,3"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
name: mana-redis-data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue