diff --git a/Dockerfile b/Dockerfile index 10f49e9..970d54f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,6 @@ RUN yarn build FROM node:20.15.1-alpine -# Install FFmpeg for video processing (reversed video generation) -RUN apk add --no-cache ffmpeg WORKDIR /app COPY backend/package.json backend/yarn.lock ./ RUN yarn install --pure-lockfile diff --git a/Dockerfile.dev b/Dockerfile.dev index e1526f2..a8353d5 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -11,12 +11,16 @@ WORKDIR /app/backend COPY backend/package.json backend/yarn.lock ./ RUN yarn install --pure-lockfile +FROM node:20.15.1-alpine AS app-shell-deps +RUN apk add --no-cache git +WORKDIR /app/app-shell +COPY app-shell/package.json app-shell/yarn.lock ./ +RUN yarn install --pure-lockfile + # Nginx setup and application build FROM node:20.15.1-alpine AS build RUN apk add --no-cache git nginx curl RUN apk add --no-cache lsof procps -# Install FFmpeg for video processing (reversed video generation) -RUN apk add --no-cache ffmpeg RUN yarn global add concurrently RUN apk add --no-cache \ @@ -39,9 +43,11 @@ ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:$PATH WORKDIR /app COPY --from=frontend-deps /app/frontend /app/frontend COPY --from=backend-deps /app/backend /app/backend +COPY --from=app-shell-deps /app/app-shell /app/app-shell COPY frontend /app/frontend COPY backend /app/backend +COPY app-shell /app/app-shell COPY docker /app/docker # Copy all files from root to /app @@ -62,6 +68,8 @@ EXPOSE 8080 ENV NODE_ENV=dev_stage ENV FRONT_PORT=3001 ENV BACKEND_PORT=3000 +ENV APP_SHELL_PORT=4000 + CMD ["sh", "-c", "\ yarn --cwd /app/frontend dev & echo $! > /app/pids/frontend.pid && \ @@ -72,5 +80,6 @@ CMD ["sh", "-c", "\ while ! nc -z localhost ${BACKEND_PORT}; do \ sleep 2; \ done && \ - echo 'Backend and frontend are up.' && \ - wait $NGINX_PID"] + echo 'Backend is up. Starting app_shell for Git check...' && \ + yarn --cwd /app/app-shell start && \ + wait $NGINX_PID"] \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index be94ae1..581cb98 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,6 @@ FROM node:20.15.1-alpine -# Install bash and FFmpeg for video processing (reversed video generation) -RUN apk update && apk add --no-cache bash ffmpeg - +RUN apk update && apk add bash # Create app directory WORKDIR /usr/src/app diff --git a/backend/package.json b/backend/package.json index ef0915c..00b4988 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,7 +27,6 @@ "dotenv": "^16.4.0", "express": "4.18.2", "express-validator": "^7.0.0", - "fluent-ffmpeg": "^2.1.3", "formidable": "1.2.2", "helmet": "^8.0.0", "joi": "^17.13.0", diff --git a/backend/src/db/api/asset_variants.js b/backend/src/db/api/asset_variants.js index 9159220..5cbffdc 100644 --- a/backend/src/db/api/asset_variants.js +++ b/backend/src/db/api/asset_variants.js @@ -72,7 +72,6 @@ class Asset_variantsDBApi extends GenericDBApi { id: data.id || undefined, variant_type: data.variant_type || null, cdn_url: data.cdn_url || null, - storage_key: data.storage_key || null, width_px: data.width_px || null, height_px: data.height_px || null, size_mb: data.size_mb || null, diff --git a/backend/src/db/api/base.api.js b/backend/src/db/api/base.api.js index 9602f28..5e29331 100644 --- a/backend/src/db/api/base.api.js +++ b/backend/src/db/api/base.api.js @@ -213,41 +213,6 @@ class GenericDBApi { return record; } - /** - * Partial update - only updates fields explicitly passed in data. - * Unlike update(), this doesn't go through getFieldMapping which - * converts missing fields to null. - * - * Use this when you need to update specific fields without affecting others. - * - * @param {string} id - Record ID - * @param {Object} data - Fields to update (only these will be modified) - * @param {Object} options - Options with currentUser and transaction - */ - static async partialUpdate(id, data, options = {}) { - const currentUser = options.currentUser || { id: null }; - const transaction = options.transaction; - - const record = await this.MODEL.findByPk(id, { transaction }); - - if (!record) { - throw { status: 404, message: `${this.TABLE_NAME} not found` }; - } - - const updatePayload = { updatedById: currentUser.id }; - - // Only include fields that are explicitly in the data object - for (const [key, value] of Object.entries(data)) { - if (value !== undefined) { - updatePayload[key] = value; - } - } - - await record.update(updatePayload, { transaction }); - - return record; - } - static async deleteByIds(ids, options = {}) { const currentUser = options.currentUser || { id: null }; const transaction = options.transaction; diff --git a/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js b/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js deleted file mode 100644 index 067d5a8..0000000 --- a/backend/src/db/migrations/20260413091125-add-reversed-variant-type.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -/** - * Migration: Add 'reversed' variant type to asset_variants - * - * This enables storing pre-reversed videos for back navigation transitions. - * Also adds storage_key column to track the S3/local storage path. - */ - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { - // Add 'reversed' to the enum_asset_variants_variant_type enum - await queryInterface.sequelize.query(` - ALTER TYPE "enum_asset_variants_variant_type" - ADD VALUE IF NOT EXISTS 'reversed'; - `); - - // Add storage_key column if it doesn't exist - const tableInfo = await queryInterface.describeTable('asset_variants'); - if (!tableInfo.storage_key) { - await queryInterface.addColumn('asset_variants', 'storage_key', { - type: Sequelize.TEXT, - allowNull: true, - }); - } - }, - - async down() { - // PostgreSQL doesn't support removing enum values - // storage_key column is safe to leave (no data loss) - }, -}; diff --git a/backend/src/db/models/asset_variants.js b/backend/src/db/models/asset_variants.js index b665bf6..f7354f0 100644 --- a/backend/src/db/models/asset_variants.js +++ b/backend/src/db/models/asset_variants.js @@ -23,16 +23,9 @@ module.exports = function (sequelize, DataTypes) { 'mp4_high', 'original', - - 'reversed', ], }, - storage_key: { - type: DataTypes.TEXT, - allowNull: true, - }, - cdn_url: { type: DataTypes.TEXT, validate: { diff --git a/backend/src/services/assets.js b/backend/src/services/assets.js index 205bf51..fc02a5c 100644 --- a/backend/src/services/assets.js +++ b/backend/src/services/assets.js @@ -1,10 +1,6 @@ const AssetsDBApi = require('../db/api/assets'); -const Asset_variantsDBApi = require('../db/api/asset_variants'); const { createEntityService } = require('../factories/service.factory'); const ValidationError = require('./notifications/errors/validation'); -const { downloadToBuffer, uploadBuffer } = require('./file'); -const videoProcessing = require('./videoProcessing'); -const { logger } = require('../utils/logger'); /** * Valid MIME type patterns for each asset type @@ -72,11 +68,11 @@ const BaseService = createEntityService(AssetsDBApi, { }); /** - * Assets Service with validation and video pre-processing + * Assets Service with validation */ class AssetsService extends BaseService { /** - * Create asset with MIME type validation and video pre-processing + * Create asset with MIME type validation */ static async create(data, currentUser) { // Validate asset_type and mime_type match @@ -89,99 +85,7 @@ class AssetsService extends BaseService { } // Call parent create - const asset = await super.create(data, currentUser); - - // Pre-generate reversed video for video assets (async, doesn't block response) - if (assetType === 'video' && data.storage_key) { - AssetsService.preGenerateReversedVideo(asset, currentUser).catch((err) => { - logger.error( - { err, assetId: asset.id }, - 'Failed to pre-generate reversed video (non-blocking)', - ); - }); - } - - return asset; - } - - /** - * Pre-generate reversed video variant for a video asset. - * Runs asynchronously after asset creation - doesn't block the upload response. - * This ensures reversed videos are ready for instant use in transitions. - * - * @param {Object} asset - Created asset record - * @param {Object} currentUser - Current user context - */ - static async preGenerateReversedVideo(asset, currentUser) { - const log = logger.child({ - assetId: asset.id, - operation: 'preGenerateReversed', - }); - - log.info('Starting pre-generation of reversed video'); - - try { - // Check if FFmpeg is available - const ffmpegAvailable = await videoProcessing.isFFmpegAvailable(); - if (!ffmpegAvailable) { - log.warn('FFmpeg not available, skipping pre-generation'); - return null; - } - - // Check if reversed variant already exists (shouldn't happen on create, but safety check) - const existingAsset = await AssetsDBApi.findBy({ id: asset.id }); - const variants = existingAsset?.asset_variants_asset || []; - const existingReversed = variants.find((v) => v.variant_type === 'reversed'); - - if (existingReversed) { - log.debug('Reversed variant already exists'); - return existingReversed.storage_key; - } - - // Download original video to buffer - const storageKey = asset.storage_key; - log.info({ storageKey }, 'Downloading original video'); - - const originalBuffer = await downloadToBuffer(storageKey); - - // Generate reversed video - log.info('Generating reversed video with FFmpeg'); - const reversedBuffer = await videoProcessing.reverseVideo( - originalBuffer, - asset.original_file_name || 'video.mp4', - ); - - // Upload reversed video to storage - const reversedKey = `assets/${asset.id}/reversed.mp4`; - log.info({ reversedKey }, 'Uploading reversed video'); - - const result = await uploadBuffer(reversedKey, reversedBuffer, { - contentType: 'video/mp4', - }); - - // Create variant record - await Asset_variantsDBApi.create( - { - assetId: asset.id, - variant_type: 'reversed', - cdn_url: result.url, - storage_key: reversedKey, - size_mb: reversedBuffer.length / (1024 * 1024), - }, - { currentUser }, - ); - - log.info( - { reversedKey, sizeMb: (reversedBuffer.length / (1024 * 1024)).toFixed(2) }, - 'Pre-generated reversed video successfully', - ); - - return reversedKey; - } catch (err) { - log.error({ err }, 'Failed to pre-generate reversed video'); - // Don't throw - this is a background operation - return null; - } + return super.create(data, currentUser); } /** diff --git a/backend/src/services/file.js b/backend/src/services/file.js index c50d19b..374c620 100644 --- a/backend/src/services/file.js +++ b/backend/src/services/file.js @@ -371,78 +371,6 @@ const deleteFile = async (privateUrl, options = {}) => { } }; -// ============================================================================ -// Download to Buffer (for processing) -// ============================================================================ - -/** - * Download a file to buffer (for processing) - * @param {string} privateUrl - Storage key/path - * @returns {Promise} - */ -const downloadToBuffer = async (privateUrl) => { - const provider = getFileStorageProvider(); - - if (provider === 's3') { - const s3 = getS3Provider(); - const result = await s3.download(privateUrl); - - // Convert stream to buffer - if (typeof result.body.transformToByteArray === 'function') { - const bytes = await result.body.transformToByteArray(); - return Buffer.from(bytes); - } - // Handle readable stream - const chunks = []; - for await (const chunk of result.body) { - chunks.push(chunk); - } - return Buffer.concat(chunks); - } else if (provider === 'gcloud') { - const { bucket, hash } = getGCloudBucket(); - const file = bucket.file(`${hash}/${privateUrl}`); - const [data] = await file.download(); - return data; - } else { - // Local provider - read directly from filesystem - return fs.readFileSync(path.join(config.uploadDir, privateUrl)); - } -}; - -/** - * Upload buffer to storage - * @param {string} privateUrl - Storage key/path - * @param {Buffer} buffer - File buffer to upload - * @param {Object} options - Upload options - * @param {string} [options.contentType] - MIME type - * @returns {Promise<{ url: string }>} - */ -const uploadBuffer = async (privateUrl, buffer, options = {}) => { - const provider = getFileStorageProvider(); - const { contentType = 'application/octet-stream' } = options; - - if (provider === 's3') { - const s3 = getS3Provider(); - const result = await s3.upload(privateUrl, buffer, { contentType }); - return { url: result.url }; - } else if (provider === 'gcloud') { - const { bucket, hash } = getGCloudBucket(); - const filePath = `${hash}/${privateUrl}`; - const blob = bucket.file(filePath); - await new Promise((resolve, reject) => { - const blobStream = blob.createWriteStream({ resumable: false }); - blobStream.on('error', reject); - blobStream.on('finish', resolve); - blobStream.end(buffer); - }); - return { url: `https://storage.googleapis.com/${bucket.name}/${blob.name}` }; - } else { - const local = getLocalProvider(); - await local.upload(privateUrl, buffer); - return { url: `/api/file/download?privateUrl=${encodeURIComponent(privateUrl)}` }; - } -}; - // ============================================================================ // Chunked Upload Session Management // ============================================================================ @@ -664,16 +592,10 @@ const generatePresignedUrls = async (urls) => { module.exports = { // Provider detection getFileStorageProvider, - getS3Provider, - getLocalProvider, - getGCloudBucket, // Unified interface uploadFile, downloadFile, deleteFile, - // Buffer operations - downloadToBuffer, - uploadBuffer, // Session-based chunked uploads initUploadSession, getUploadSession, diff --git a/backend/src/services/tour_pages.js b/backend/src/services/tour_pages.js index 9a5f95a..f35914e 100644 --- a/backend/src/services/tour_pages.js +++ b/backend/src/services/tour_pages.js @@ -1,542 +1,6 @@ -/** - * Tour Pages Service - * - * Extends the factory service with reversed video generation for back navigation transitions. - * - * Supports two back navigation modes: - * 1. target_page: Back button has its own transitionVideoUrl - reversed video generated for it - * 2. history: Back button uses forward element from previous page - reversed video generated - * for forward elements when project has any history-mode back buttons - */ - const Tour_pagesDBApi = require('../db/api/tour_pages'); -const AssetsDBApi = require('../db/api/assets'); -const Asset_variantsDBApi = require('../db/api/asset_variants'); const { createEntityService } = require('../factories/service.factory'); -const { downloadToBuffer, uploadBuffer } = require('./file'); -const videoProcessing = require('./videoProcessing'); -const { logger } = require('../utils/logger'); -// Cache for project history-mode status (cleared per request cycle) -const projectHistoryModeCache = new Map(); - -// Create base service from factory -const BaseService = createEntityService(Tour_pagesDBApi, { +module.exports = createEntityService(Tour_pagesDBApi, { entityName: 'tour_pages', }); - -/** - * Tour Pages Service with reversed video generation - */ -class TourPagesService extends BaseService { - /** - * Create tour page - generate reversed videos if needed - */ - static async create(data, currentUser) { - // Process reversed videos and get updated ui_schema_json - const updatedData = await TourPagesService.processReversedVideosAndUpdateSchema( - data, - currentUser, - ); - - return super.create(updatedData, currentUser); - } - - /** - * Update tour page - generate reversed videos if needed - */ - static async update(data, id, currentUser) { - // Fetch existing page to get projectId (not included in update request body) - const existingPage = await Tour_pagesDBApi.findBy({ id }); - const projectId = existingPage?.projectId || data.projectId || data.project_id; - - // Process reversed videos and get updated ui_schema_json - const updatedData = await TourPagesService.processReversedVideosAndUpdateSchema( - { ...data, projectId, id }, - currentUser, - ); - - return super.update(updatedData, id, currentUser); - } - - /** - * Check if element is a back navigation button - * @private - */ - static isBackElement(element) { - return ( - element.type === 'navigation_prev' || - (element.type?.startsWith?.('navigation') && element.navType === 'back') - ); - } - - /** - * Check if element is a forward navigation button with a target and transition - * @private - */ - static isForwardElementWithTarget(element) { - const isForward = - element.type === 'navigation_next' || - (element.type?.startsWith?.('navigation') && - element.navType !== 'back' && - element.type !== 'navigation_prev'); - // Check for target (slug or legacy ID) and transition video - const hasTarget = element.targetPageSlug || element.targetPageId; - return isForward && hasTarget && element.transitionVideoUrl; - } - - /** - * Check if any page in the project has a history-mode back button. - * Uses per-request caching to avoid repeated queries. - * - * @param {string} projectId - Project ID - * @returns {Promise} - */ - static async projectHasHistoryModeBackButton(projectId) { - if (!projectId) return false; - - // Check cache first - if (projectHistoryModeCache.has(projectId)) { - return projectHistoryModeCache.get(projectId); - } - - const { rows: pages } = await Tour_pagesDBApi.findAll( - { projectId }, - { attributes: ['id', 'ui_schema_json'] }, - ); - - logger.info( - { projectId, pageCount: pages.length }, - 'Checking project for history-mode back buttons', - ); - - for (const page of pages) { - const uiSchema = - typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json || '{}') - : page.ui_schema_json || {}; - - if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) continue; - - for (const element of uiSchema.elements) { - const isBack = TourPagesService.isBackElement(element); - - logger.debug( - { - pageId: page.id, - elementType: element.type, - navType: element.navType, - navBackMode: element.navBackMode, - isBack, - }, - 'Checking element for history mode', - ); - - if (isBack && element.navBackMode === 'history') { - logger.info( - { pageId: page.id, elementType: element.type }, - 'Found history-mode back button', - ); - projectHistoryModeCache.set(projectId, true); - return true; - } - } - } - - logger.info({ projectId }, 'No history-mode back buttons found in project'); - projectHistoryModeCache.set(projectId, false); - return false; - } - - /** - * Clear project cache (call at end of request or after project changes) - * @param {string} projectId - Project ID to clear, or undefined to clear all - */ - static clearProjectCache(projectId) { - if (projectId) { - projectHistoryModeCache.delete(projectId); - } else { - projectHistoryModeCache.clear(); - } - } - - /** - * Process reversed videos and update ui_schema_json with reversed URLs. - * Returns data with updated ui_schema_json. - * - * Handles two cases: - * 1. Back navigation elements with transitionVideoUrl - always generate reversed - * 2. Forward navigation elements when project uses history-mode back navigation - * - * @param {Object} data - Page data with ui_schema_json - * @param {Object} currentUser - Current user for permissions - * @param {Object} options - Processing options - * @param {boolean} options._forceForwardReversed - Force processing forward elements - * @param {boolean} options._skipHistoryModeCheck - Skip initial history mode check - */ - static async processReversedVideosAndUpdateSchema( - data, - currentUser, - options = {}, - ) { - let uiSchema = data.ui_schema_json; - const wasString = typeof uiSchema === 'string'; - - // Parse if string - if (wasString) { - try { - uiSchema = JSON.parse(uiSchema); - } catch { - return data; // Return original data if parsing fails - } - } - - if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) { - logger.debug({ hasElements: false }, 'No elements in ui_schema_json'); - return data; - } - - // Get project ID - const projectId = data.projectId || data.project_id || data.project; - - // Check if this page being saved has a history-mode back button - // This is used to trigger regeneration of other pages' forward elements - let thisPageHasHistoryMode = false; - - for (const element of uiSchema.elements) { - if ( - TourPagesService.isBackElement(element) && - element.navBackMode === 'history' - ) { - thisPageHasHistoryMode = true; - break; - } - } - - // Check if project already has history-mode back buttons (from other pages) - // Skip this check if we're in regeneration mode to avoid recursion - let projectHasHistoryMode = options._forceForwardReversed || false; - - if (!projectHasHistoryMode && projectId && !options._skipHistoryModeCheck) { - // Invalidate cache since we're about to save changes - TourPagesService.clearProjectCache(projectId); - projectHasHistoryMode = - await TourPagesService.projectHasHistoryModeBackButton(projectId); - } - - // Combined check: process forward elements if project uses history mode OR this page enables it - const shouldProcessForward = - projectHasHistoryMode || thisPageHasHistoryMode; - - logger.info( - { - thisPageHasHistoryMode, - projectHasHistoryMode, - shouldProcessForward, - projectId, - }, - 'History mode detection result', - ); - - let wasModified = false; - - for (const element of uiSchema.elements) { - const isBack = TourPagesService.isBackElement(element); - const isForward = TourPagesService.isForwardElementWithTarget(element); - - // Determine if this element needs reversed video - const needsReversed = isBack || (shouldProcessForward && isForward); - - logger.debug( - { - elementType: element.type, - navType: element.navType, - isBack, - isForward, - needsReversed, - hasTransitionVideo: Boolean(element.transitionVideoUrl), - targetPageSlug: element.targetPageSlug, - targetPageId: element.targetPageId, - shouldProcessForward, - }, - 'Evaluating element for reversed video', - ); - - if (!needsReversed) continue; - if (!element.transitionVideoUrl) continue; - - // Skip if already has a manually set reverseVideoUrl (separate_video mode) - if ( - element.transitionReverseMode === 'separate_video' && - element.reverseVideoUrl - ) { - continue; - } - - const storageKey = element.transitionVideoUrl; - - try { - // Get or generate reversed video URL - const reversedUrl = await TourPagesService.getOrGenerateReversedVariant( - storageKey, - currentUser, - ); - - if (reversedUrl && reversedUrl !== element.reverseVideoUrl) { - element.reverseVideoUrl = reversedUrl; - wasModified = true; - logger.info( - { - elementType: element.type, - isBack, - isForward, - storageKey, - }, - 'Added reversed video URL to element', - ); - } - } catch (err) { - logger.error( - { err, storageKey }, - 'Failed to get/generate reversed variant', - ); - // Continue without reversed video - button will be disabled in frontend - } - } - - // If this page has a history-mode back button, trigger regeneration of - // other pages' forward elements that are missing reversed videos. - // This runs every save but only processes elements without reverseVideoUrl. - if ( - thisPageHasHistoryMode && - projectId && - !options._skipHistoryModeCheck - ) { - logger.info( - { projectId }, - 'History mode back button detected - regenerating forward elements', - ); - - try { - await TourPagesService.regenerateProjectReversedVideos( - projectId, - currentUser, - data.id, - ); - } catch (err) { - logger.error( - { err, projectId }, - 'Failed to regenerate project reversed videos', - ); - } - } - - if (wasModified) { - return { - ...data, - // Return same type as input: string if input was string, object otherwise - ui_schema_json: wasString ? JSON.stringify(uiSchema) : uiSchema, - }; - } - - return data; - } - - /** - * Get existing reversed variant URL or generate one - * @param {string} storageKey - Original video storage key - * @param {Object} currentUser - Current user for permissions - * @returns {Promise} Reversed video storage key or null - */ - static async getOrGenerateReversedVariant(storageKey, currentUser) { - // Find the asset by storage key - const asset = await AssetsDBApi.findBy({ storage_key: storageKey }); - - if (!asset) { - logger.warn({ storageKey }, 'Asset not found for transition'); - return null; - } - - // Check if reversed variant already exists - const variants = asset.asset_variants_asset || []; - const reversedVariant = variants.find((v) => v.variant_type === 'reversed'); - - if (reversedVariant) { - logger.debug({ assetId: asset.id }, 'Using existing reversed variant'); - return reversedVariant.storage_key; - } - - // Generate reversed video - return TourPagesService.generateReversedVariant(asset, currentUser); - } - - /** - * Generate reversed video variant for an asset - * @returns {Promise} Reversed video storage key or null - */ - static async generateReversedVariant(asset, currentUser) { - const log = logger.child({ assetId: asset.id }); - log.info('Generating reversed video variant'); - - try { - // Check if FFmpeg is available - const ffmpegAvailable = await videoProcessing.isFFmpegAvailable(); - if (!ffmpegAvailable) { - log.error('FFmpeg is not available on this server'); - return null; - } - - // Download original video to buffer - const originalBuffer = await downloadToBuffer(asset.storage_key); - - // Generate reversed video - const reversedBuffer = await videoProcessing.reverseVideo( - originalBuffer, - asset.original_file_name || 'video.mp4', - ); - - // Upload reversed video to storage - const reversedKey = `assets/${asset.id}/reversed.mp4`; - - const result = await uploadBuffer(reversedKey, reversedBuffer, { - contentType: 'video/mp4', - }); - - // Create variant record - await Asset_variantsDBApi.create( - { - assetId: asset.id, - variant_type: 'reversed', - cdn_url: result.url, - storage_key: reversedKey, - size_mb: reversedBuffer.length / (1024 * 1024), - }, - { currentUser }, - ); - - log.info( - { reversedKey, size: reversedBuffer.length }, - 'Reversed variant created', - ); - - return reversedKey; - } catch (err) { - log.error({ err }, 'Failed to generate reversed variant'); - return null; - } - } - - /** - * Regenerate reversed videos for all forward navigation elements in project. - * Called when history-mode back navigation is first enabled in a project. - * - * @param {string} projectId - Project ID - * @param {Object} currentUser - Current user for permissions - * @param {string} excludePageId - Optional page ID to exclude (the page being saved) - */ - static async regenerateProjectReversedVideos( - projectId, - currentUser, - excludePageId, - ) { - const log = logger.child({ projectId, operation: 'regenerateReversed' }); - log.info('Starting project-wide reversed video regeneration'); - - try { - // Only process dev environment pages (constructor works with dev) - const { rows: pages } = await Tour_pagesDBApi.findAll({ - projectId, - environment: 'dev', - }); - - let pagesUpdated = 0; - let elementsProcessed = 0; - - for (const page of pages) { - // Skip the page that triggered this regeneration (it's already being processed) - if (excludePageId && page.id === excludePageId) { - continue; - } - - const uiSchema = - typeof page.ui_schema_json === 'string' - ? JSON.parse(page.ui_schema_json || '{}') - : page.ui_schema_json || {}; - - if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) { - continue; - } - - let pageModified = false; - - for (const element of uiSchema.elements) { - // Process both forward elements AND back elements with their own transition - const isForward = TourPagesService.isForwardElementWithTarget(element); - const isBackWithTransition = - TourPagesService.isBackElement(element) && - element.transitionVideoUrl; - - log.debug({ - pageId: page.id, - elementType: element.type, - navType: element.navType, - isForward, - isBackWithTransition, - hasTransitionVideo: Boolean(element.transitionVideoUrl), - hasReverseVideo: Boolean(element.reverseVideoUrl), - targetPageSlug: element.targetPageSlug, - targetPageId: element.targetPageId, - }, 'Checking element in regeneration'); - - // Skip if neither forward nor back-with-transition - if (!isForward && !isBackWithTransition) { - continue; - } - - if (!element.transitionVideoUrl) continue; - if (element.reverseVideoUrl) continue; // Already has reversed - - try { - const reversedUrl = - await TourPagesService.getOrGenerateReversedVariant( - element.transitionVideoUrl, - currentUser, - ); - - if (reversedUrl) { - element.reverseVideoUrl = reversedUrl; - pageModified = true; - elementsProcessed++; - } - } catch (err) { - log.error( - { err, pageId: page.id, elementType: element.type }, - 'Failed to generate reversed video for element', - ); - } - } - - if (pageModified) { - // Use partialUpdate to only update ui_schema_json field - // This avoids the regular update's getFieldMapping which sets other fields to null - await Tour_pagesDBApi.partialUpdate( - page.id, - { ui_schema_json: JSON.stringify(uiSchema) }, - { currentUser }, - ); - pagesUpdated++; - } - } - - log.info( - { pagesUpdated, elementsProcessed }, - 'Completed project-wide reversed video regeneration', - ); - } catch (err) { - log.error({ err }, 'Failed to regenerate project reversed videos'); - throw err; - } - } -} - -module.exports = TourPagesService; diff --git a/backend/src/services/videoProcessing.js b/backend/src/services/videoProcessing.js deleted file mode 100644 index d57b390..0000000 --- a/backend/src/services/videoProcessing.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Video Processing Service - * - * Provides video manipulation operations using FFmpeg. - * Used for generating reversed videos for back navigation transitions. - */ - -const ffmpeg = require('fluent-ffmpeg'); -const fs = require('fs').promises; -const path = require('path'); -const os = require('os'); -const { logger } = require('../utils/logger'); - -/** - * Reverse a video using FFmpeg - * @param {Buffer} inputBuffer - Input video buffer - * @param {string} filename - Original filename (for extension) - * @returns {Promise} Reversed video buffer - */ -async function reverseVideo(inputBuffer, filename) { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'video-reverse-')); - const ext = path.extname(filename) || '.mp4'; - const inputPath = path.join(tempDir, `input${ext}`); - const outputPath = path.join(tempDir, `reversed${ext}`); - - try { - // Write input buffer to temp file - await fs.writeFile(inputPath, inputBuffer); - - logger.info({ inputPath, outputPath }, 'Starting video reversal'); - - // Reverse video with FFmpeg - await new Promise((resolve, reject) => { - ffmpeg(inputPath) - .outputOptions([ - '-vf', 'reverse', - '-af', 'areverse', - '-c:v', 'libx264', - '-preset', 'fast', - '-crf', '23', - '-c:a', 'aac', - '-movflags', '+faststart', - ]) - .output(outputPath) - .on('start', (cmd) => logger.debug({ cmd }, 'FFmpeg command')) - .on('progress', (progress) => { - if (progress.percent) { - logger.debug({ percent: progress.percent }, 'FFmpeg progress'); - } - }) - .on('end', () => { - logger.info({ outputPath }, 'Video reversal complete'); - resolve(); - }) - .on('error', (err, stdout, stderr) => { - logger.error({ err, stdout, stderr }, 'FFmpeg error'); - reject(err); - }) - .run(); - }); - - // Read output as buffer - return await fs.readFile(outputPath); - } finally { - // Cleanup temp files - try { - await fs.rm(tempDir, { recursive: true, force: true }); - } catch (cleanupErr) { - logger.warn({ err: cleanupErr, tempDir }, 'Failed to cleanup temp dir'); - } - } -} - -/** - * Check if FFmpeg is available - * @returns {Promise} - */ -async function isFFmpegAvailable() { - return new Promise((resolve) => { - ffmpeg.getAvailableFormats((err) => { - resolve(!err); - }); - }); -} - -module.exports = { - reverseVideo, - isFFmpegAvailable, -}; diff --git a/backend/yarn.lock b/backend/yarn.lock index 09eb297..3da3deb 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -77,7 +77,7 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": +"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== @@ -93,7 +93,7 @@ dependencies: tslib "^2.6.2" -"@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": +"@aws-crypto/util@^5.2.0", "@aws-crypto/util@5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz" integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== @@ -1542,10 +1542,13 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" - integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: + version "8.15.0" + +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== agent-base@6: version "6.0.2" @@ -1554,15 +1557,8 @@ agent-base@6: dependencies: debug "4" -agent-base@^7.1.0, agent-base@^7.1.2: - version "7.1.4" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz" - integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== - ajv@^6.12.4: - version "6.14.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" - integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== + version "6.12.6" dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1726,11 +1722,6 @@ async-retry@^1.3.3: dependencies: retry "0.13.1" -async@^0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -1846,7 +1837,14 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1, brace-expansion@^2.0.2: +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + +brace-expansion@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== @@ -1962,7 +1960,22 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.2, chokidar@^3.5.3: +chokidar@^3.5.2: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^3.5.3: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -2017,11 +2030,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" - integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== - commander@^10.0.0: version "10.0.1" resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" @@ -2032,6 +2040,11 @@ commander@^6.1.0: resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -2055,7 +2068,7 @@ config-chain@^1.1.13: ini "^1.3.4" proto-list "~1.2.1" -content-disposition@0.5.4, content-disposition@^0.5.3: +content-disposition@^0.5.3, content-disposition@0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== @@ -2165,20 +2178,6 @@ dateformat@^4.6.3: resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4, debug@^4.3.4, debug@^4.3.5: - version "4.3.5" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -2186,13 +2185,34 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.1, debug@^4.3.2: +debug@^4, debug@^4.3.4, debug@^4.3.5, debug@4: + version "4.3.5" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@^4.3.1: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" +debug@^4.3.2: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + decamelize@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz" @@ -2236,16 +2256,16 @@ denque@^1.4.1: resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - depd@^1.1.0: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" @@ -2256,13 +2276,6 @@ diff@^5.2.0: resolved "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz" integrity sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A== -doctrine@3.0.0, doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" @@ -2270,6 +2283,13 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" +doctrine@^3.0.0, doctrine@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + dotenv@^16.4.0: version "16.6.1" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" @@ -2304,7 +2324,7 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: +ecdsa-sig-formatter@^1.0.11, ecdsa-sig-formatter@1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -2595,7 +2615,7 @@ eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.57.0: +"eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.57.0: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -2695,7 +2715,7 @@ express-validator@^7.0.0: lodash "^4.17.21" validator "~13.15.23" -express@4.18.2: +"express@>=4.0.0 || >=5.0.0-beta", express@4.18.2: version "4.18.2" resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -2769,7 +2789,7 @@ fast-xml-builder@^1.1.4: dependencies: path-expression-matcher "^1.1.3" -fast-xml-parser@5.5.8, fast-xml-parser@^5.3.4: +fast-xml-parser@^5.3.4, fast-xml-parser@5.5.8: version "5.5.8" resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz" integrity sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ== @@ -2835,17 +2855,7 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.2.9: - version "3.4.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" - integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== - -fluent-ffmpeg@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz#d6846be257777844249a4adeb320f25326d239f3" - integrity sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q== - dependencies: - async "^0.2.9" - which "^1.1.1" + version "3.3.3" follow-redirects@^1.15.11: version "1.15.11" @@ -2907,7 +2917,7 @@ forwarded@0.2.0: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fresh@0.5.2, fresh@^0.5.2: +fresh@^0.5.2, fresh@0.5.2: version "0.5.2" resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== @@ -2927,11 +2937,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -3001,7 +3006,29 @@ get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-intrinsic@^1.2.1, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-intrinsic@^1.2.3: version "1.2.4" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -3068,18 +3095,6 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^10.4.2: version "10.5.0" resolved "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz" @@ -3115,6 +3130,18 @@ glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^13.19.0: version "13.24.0" resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" @@ -3299,6 +3326,20 @@ https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1: agent-base "^7.1.2" debug "4" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -3306,13 +3347,6 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - ieee754@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" @@ -3354,7 +3388,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: +inherits@^2.0.3, inherits@^2.0.4, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3651,7 +3685,16 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-symbol@^1.0.4, is-symbol@^1.1.1: +is-symbol@^1.0.4: + version "1.1.1" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-symbol@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz" integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== @@ -3983,16 +4026,16 @@ media-typer@0.3.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - merge-descriptors@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" @@ -4010,7 +4053,7 @@ mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34: dependencies: mime-db "1.52.0" -mime@1.6.0, mime@^1.3.4: +mime@^1.3.4, mime@1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -4041,7 +4084,14 @@ minimatch@^5.0.1, minimatch@^5.1.6: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1, minimatch@^9.0.4: +minimatch@^9.0.1: + version "9.0.9" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +minimatch@^9.0.4: version "9.0.9" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz" integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== @@ -4091,11 +4141,16 @@ moment-timezone@^0.5.43: dependencies: moment "^2.29.4" -moment@2.30.1, moment@^2.29.4: +moment@^2.29.4, moment@2.30.1: version "2.30.1" resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== +ms@^2.1.1, ms@^2.1.3, ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -4106,11 +4161,6 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - multer@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz" @@ -4327,6 +4377,11 @@ open@^8.0.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-types@>=7: + version "12.1.3" + resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" @@ -4401,7 +4456,7 @@ passport-microsoft@^2.0.0: dependencies: passport-oauth2 "1.8.0" -passport-oauth2@1.8.0, passport-oauth2@^1.1.2: +passport-oauth2@^1.1.2, passport-oauth2@1.8.0: version "1.8.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz" integrity sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA== @@ -4412,7 +4467,7 @@ passport-oauth2@1.8.0, passport-oauth2@^1.1.2: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x.x, passport-strategy@^1.0.0: +passport-strategy@^1.0.0, passport-strategy@1.x.x: version "1.0.0" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== @@ -4512,7 +4567,7 @@ pg-types@2.2.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.20.0: +pg@^8.20.0, pg@>=8.0: version "8.20.0" resolved "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz" integrity sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA== @@ -4879,7 +4934,7 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4935,7 +4990,12 @@ semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4: +semver@^7.5.3: + version "7.7.4" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +semver@^7.5.4: version "7.7.4" resolved "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== @@ -4987,7 +5047,7 @@ sequelize-pool@^7.1.0: resolved "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz" integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== -sequelize@^6.37.0: +sequelize@^6.37.0, "sequelize@>= 4": version "6.37.8" resolved "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz" integrity sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw== @@ -5198,6 +5258,13 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -5276,13 +5343,6 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -5628,7 +5688,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -5645,7 +5705,7 @@ util-deprecate@^1.0.1: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: +utils-merge@^1.0.1, utils-merge@1.0.1, utils-merge@1.x.x: version "1.0.1" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== @@ -5655,7 +5715,12 @@ uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0, uuid@^9.0.1: +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +uuid@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -5734,7 +5799,18 @@ which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.14, which-typed-array@^1.1.15: +which-typed-array@^1.1.14: + version "1.1.15" + resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which-typed-array@^1.1.15: version "1.1.15" resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== @@ -5758,13 +5834,6 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.19: gopd "^1.2.0" has-tostringtag "^1.0.2" -which@^1.1.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e03e333..393e99a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,3 +1,5 @@ + + version: "3.9" services: web: @@ -12,8 +14,12 @@ services: options: max-size: "10m" max-file: "3" - db: + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" image: postgres volumes: - ./data/db:/var/lib/postgresql/data @@ -30,19 +36,23 @@ services: backend: image: backend - build: ../backend volumes: - - ./wait-for-it.sh:/usr/src/app/wait-for-it.sh - - ./start-backend.sh:/usr/src/app/start-backend.sh + - ./wait-for-it.sh:/usr/src/app/wait-for-it.sh + - ./start-backend.sh:/usr/src/app/start-backend.sh + build: ../backend environment: - - DB_HOST=db + - DB_HOST=db ports: - - "8080:8080" - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" + - "8080:8080" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" depends_on: - - "db" + - "db" + + + command: ["bash", "./wait-for-it.sh", "db:5432", "--timeout=0", "--strict", "--", "bash", "./start-backend.sh"] + diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 00a8785..254b73c 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/src/components/Constructor/CanvasBackground.tsx b/frontend/src/components/Constructor/CanvasBackground.tsx index d223d44..98e662b 100644 --- a/frontend/src/components/Constructor/CanvasBackground.tsx +++ b/frontend/src/components/Constructor/CanvasBackground.tsx @@ -15,10 +15,8 @@ interface CanvasBackgroundProps { backgroundVideoUrl?: string; backgroundAudioUrl?: string; previousBgImageUrl?: string; - previousBgVideoUrl?: string; isSwitching?: boolean; isNewBgReady?: boolean; - isFadingIn?: boolean; onBackgroundReady?: () => void; // Video playback settings videoAutoplay?: boolean; @@ -33,10 +31,8 @@ const CanvasBackground: React.FC = ({ backgroundVideoUrl, backgroundAudioUrl, previousBgImageUrl, - previousBgVideoUrl, isSwitching = false, isNewBgReady = false, - isFadingIn = false, onBackgroundReady, videoAutoplay = true, videoLoop = true, @@ -98,12 +94,10 @@ const CanvasBackground: React.FC = ({ )} - {/* Previous background overlays - show during loading AND crossfade. - Uses CSS animation for fade-out effect during crossfade. - z-0 keeps them BELOW new backgrounds (z-1). */} - {previousBgImageUrl && (isFadingIn || (isSwitching && !isNewBgReady)) && ( + {/* Previous background overlay - shows during page switch until new bg is ready */} + {previousBgImageUrl && isSwitching && !isNewBgReady && (
= ({ }} /> )} - {previousBgVideoUrl && (isFadingIn || (isSwitching && !isNewBgReady)) && ( -