Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84045a9c92 | ||
|
|
1b09a7d901 | ||
|
|
28dd8852af | ||
|
|
51057a3d26 | ||
|
|
0d6b901995 | ||
|
|
fa8db55f93 |
@@ -17,6 +17,16 @@ export enum ErrorCode {
|
||||
AUTH_SESSION_EXPIRED = 'AUTH_SESSION_EXPIRED',
|
||||
AUTH_EMAIL_NOT_VERIFIED = 'AUTH_EMAIL_NOT_VERIFIED',
|
||||
|
||||
// Invite Code Errors (INVITE_CODE_*)
|
||||
INVITE_CODE_INVALID = 'INVITE_CODE_INVALID',
|
||||
INVITE_CODE_EXPIRED = 'INVITE_CODE_EXPIRED',
|
||||
INVITE_CODE_ALREADY_USED = 'INVITE_CODE_ALREADY_USED',
|
||||
INVITE_CODE_MAX_USES_REACHED = 'INVITE_CODE_MAX_USES_REACHED',
|
||||
|
||||
// Email Verification Errors (EMAIL_VERIFICATION_*)
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID = 'EMAIL_VERIFICATION_TOKEN_INVALID',
|
||||
EMAIL_VERIFICATION_TOKEN_EXPIRED = 'EMAIL_VERIFICATION_TOKEN_EXPIRED',
|
||||
|
||||
// User Errors (USER_*)
|
||||
USER_NOT_FOUND = 'USER_NOT_FOUND',
|
||||
USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
|
||||
@@ -182,6 +192,52 @@ export const ErrorMessages: Record<ErrorCode, Record<string, string>> = {
|
||||
'zh-CN': '请验证您的电子邮件地址以继续',
|
||||
},
|
||||
|
||||
// Invite Code Errors
|
||||
[ErrorCode.INVITE_CODE_INVALID]: {
|
||||
'en-US': 'Invalid or inactive invite code. Please check the code and try again',
|
||||
'es-ES': 'Código de invitación inválido o inactivo. Por favor verifica el código e intenta de nuevo',
|
||||
'fr-FR': 'Code d\'invitation invalide ou inactif. Veuillez vérifier le code et réessayer',
|
||||
'pt-BR': 'Código de convite inválido ou inativo. Por favor, verifique o código e tente novamente',
|
||||
'zh-CN': '无效或未激活的邀请码。请检查代码并重试',
|
||||
},
|
||||
[ErrorCode.INVITE_CODE_EXPIRED]: {
|
||||
'en-US': 'This invite code has expired. Please request a new code',
|
||||
'es-ES': 'Este código de invitación ha expirado. Por favor solicita un nuevo código',
|
||||
'fr-FR': 'Ce code d\'invitation a expiré. Veuillez demander un nouveau code',
|
||||
'pt-BR': 'Este código de convite expirou. Por favor, solicite um novo código',
|
||||
'zh-CN': '此邀请码已过期。请申请新代码',
|
||||
},
|
||||
[ErrorCode.INVITE_CODE_ALREADY_USED]: {
|
||||
'en-US': 'This invite code has already been used',
|
||||
'es-ES': 'Este código de invitación ya ha sido utilizado',
|
||||
'fr-FR': 'Ce code d\'invitation a déjà été utilisé',
|
||||
'pt-BR': 'Este código de convite já foi usado',
|
||||
'zh-CN': '此邀请码已被使用',
|
||||
},
|
||||
[ErrorCode.INVITE_CODE_MAX_USES_REACHED]: {
|
||||
'en-US': 'This invite code has reached its maximum number of uses',
|
||||
'es-ES': 'Este código de invitación ha alcanzado su número máximo de usos',
|
||||
'fr-FR': 'Ce code d\'invitation a atteint son nombre maximum d\'utilisations',
|
||||
'pt-BR': 'Este código de convite atingiu seu número máximo de usos',
|
||||
'zh-CN': '此邀请码已达到最大使用次数',
|
||||
},
|
||||
|
||||
// Email Verification Errors
|
||||
[ErrorCode.EMAIL_VERIFICATION_TOKEN_INVALID]: {
|
||||
'en-US': 'Invalid email verification token. Please request a new verification email',
|
||||
'es-ES': 'Token de verificación de correo inválido. Por favor solicita un nuevo correo de verificación',
|
||||
'fr-FR': 'Jeton de vérification d\'email invalide. Veuillez demander un nouvel email de vérification',
|
||||
'pt-BR': 'Token de verificação de email inválido. Por favor, solicite um novo email de verificação',
|
||||
'zh-CN': '无效的电子邮件验证令牌。请申请新的验证电子邮件',
|
||||
},
|
||||
[ErrorCode.EMAIL_VERIFICATION_TOKEN_EXPIRED]: {
|
||||
'en-US': 'Email verification link has expired. Please request a new verification email',
|
||||
'es-ES': 'El enlace de verificación de correo ha expirado. Por favor solicita un nuevo correo de verificación',
|
||||
'fr-FR': 'Le lien de vérification d\'email a expiré. Veuillez demander un nouvel email de vérification',
|
||||
'pt-BR': 'O link de verificação de email expirou. Por favor, solicite um novo email de verificação',
|
||||
'zh-CN': '电子邮件验证链接已过期。请申请新的验证电子邮件',
|
||||
},
|
||||
|
||||
// User Errors
|
||||
[ErrorCode.USER_NOT_FOUND]: {
|
||||
'en-US': 'User not found',
|
||||
|
||||
@@ -392,6 +392,62 @@ export class AnalyticsController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced Analytics Dashboard - Aggregates all advanced analytics
|
||||
* GET /api/v1/analytics/advanced/dashboard/:childId
|
||||
*/
|
||||
@Get('advanced/dashboard/:childId')
|
||||
async getAdvancedDashboard(
|
||||
@CurrentUser() user: any,
|
||||
@Param('childId') childId: string,
|
||||
) {
|
||||
try {
|
||||
// Fetch all analytics data in parallel for better performance
|
||||
const [
|
||||
circadianRhythm,
|
||||
anomalies,
|
||||
correlations,
|
||||
growthAnalysis,
|
||||
sleepTrends,
|
||||
feedingTrends,
|
||||
sleepClusters,
|
||||
feedingClusters,
|
||||
] = await Promise.all([
|
||||
this.advancedPatternService.analyzeCircadianRhythm(childId, 14),
|
||||
this.advancedPatternService.detectAnomalies(childId, 30),
|
||||
this.advancedPatternService.analyzeCorrelations(childId, 14),
|
||||
this.growthPercentileService.analyzeGrowth(childId),
|
||||
this.advancedPatternService.analyzeTrends(childId, ActivityType.SLEEP),
|
||||
this.advancedPatternService.analyzeTrends(childId, ActivityType.FEEDING),
|
||||
this.advancedPatternService.clusterActivities(childId, ActivityType.SLEEP, 30),
|
||||
this.advancedPatternService.clusterActivities(childId, ActivityType.FEEDING, 30),
|
||||
]);
|
||||
|
||||
// Combine all analytics into a comprehensive dashboard
|
||||
const dashboard = {
|
||||
circadianRhythm,
|
||||
anomalies,
|
||||
correlations,
|
||||
growthAnalysis,
|
||||
trends: {
|
||||
sleep: sleepTrends,
|
||||
feeding: feedingTrends,
|
||||
},
|
||||
clusters: {
|
||||
sleep: sleepClusters,
|
||||
feeding: feedingClusters,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: dashboard,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprehensive Analytics Dashboard Endpoint
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreateFamilyDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
}
|
||||
@@ -14,12 +14,33 @@ import {
|
||||
import { FamiliesService } from './families.service';
|
||||
import { InviteFamilyMemberDto } from './dto/invite-family-member.dto';
|
||||
import { JoinFamilyDto } from './dto/join-family.dto';
|
||||
import { CreateFamilyDto } from './dto/create-family.dto';
|
||||
import { FamilyRole } from '../../database/entities/family-member.entity';
|
||||
|
||||
@Controller('api/v1/families')
|
||||
export class FamiliesController {
|
||||
constructor(private readonly familiesService: FamiliesService) {}
|
||||
|
||||
/**
|
||||
* Create a new family
|
||||
* POST /api/v1/families
|
||||
* Body: { name: string }
|
||||
*/
|
||||
@Post()
|
||||
async createFamily(@Req() req: any, @Body() createFamilyDto: CreateFamilyDto) {
|
||||
const family = await this.familiesService.createFamily(
|
||||
req.user.userId,
|
||||
createFamilyDto,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
family,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('invite')
|
||||
async inviteMember(
|
||||
@Req() req: any,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { User } from '../../database/entities/user.entity';
|
||||
import { InviteFamilyMemberDto } from './dto/invite-family-member.dto';
|
||||
import { JoinFamilyDto } from './dto/join-family.dto';
|
||||
import { CreateFamilyDto } from './dto/create-family.dto';
|
||||
import { EmailService } from '../../common/services/email.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -29,6 +30,53 @@ export class FamiliesService {
|
||||
private emailService: EmailService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new family
|
||||
*/
|
||||
async createFamily(
|
||||
userId: string,
|
||||
createFamilyDto: CreateFamilyDto,
|
||||
): Promise<Family> {
|
||||
// Check if user already has a family
|
||||
const existingMembership = await this.familyMemberRepository.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (existingMembership) {
|
||||
throw new ConflictException(
|
||||
'You are already a member of a family. Leave your current family before creating a new one.',
|
||||
);
|
||||
}
|
||||
|
||||
// Create the family (ID and shareCode are auto-generated via @BeforeInsert)
|
||||
const family = this.familyRepository.create({
|
||||
name: createFamilyDto.name,
|
||||
shareCodeExpiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
||||
createdBy: userId,
|
||||
subscriptionTier: 'free', // Default tier
|
||||
});
|
||||
|
||||
const savedFamily = await this.familyRepository.save(family);
|
||||
|
||||
// Add creator as a parent (admin) of the family
|
||||
const creatorMembership = this.familyMemberRepository.create({
|
||||
userId,
|
||||
familyId: savedFamily.id,
|
||||
role: FamilyRole.PARENT,
|
||||
permissions: {
|
||||
canAddChildren: true,
|
||||
canEditChildren: true,
|
||||
canLogActivities: true,
|
||||
canViewReports: true,
|
||||
canInviteMembers: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.familyMemberRepository.save(creatorMembership);
|
||||
|
||||
return savedFamily;
|
||||
}
|
||||
|
||||
async inviteMember(
|
||||
userId: string,
|
||||
familyId: string,
|
||||
|
||||
@@ -40,6 +40,7 @@ import { useTheme } from '@mui/material/styles';
|
||||
import { StepIconProps } from '@mui/material/StepIcon';
|
||||
import { FamilySetupStep, ChildData } from '@/components/onboarding/FamilySetupStep';
|
||||
import { familiesApi } from '@/lib/api/families';
|
||||
import { extractError } from '@/lib/utils/errorHandler';
|
||||
|
||||
const steps = ['Welcome', 'Preferences', 'Family Setup', 'Complete'];
|
||||
|
||||
@@ -230,7 +231,8 @@ export default function OnboardingPage() {
|
||||
setActiveStep((prev) => prev + 1);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to create family:', err);
|
||||
setError(err.response?.data?.message || 'Failed to create family. Please try again.');
|
||||
const errorData = extractError(err);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -246,7 +248,8 @@ export default function OnboardingPage() {
|
||||
setActiveStep((prev) => prev + 1);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to join family:', err);
|
||||
setError(err.response?.data?.message || 'Failed to join family. Please check the code and try again.');
|
||||
const errorData = extractError(err);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { LockReset, Visibility, VisibilityOff, CheckCircle } from '@mui/icons-ma
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { extractError } from '@/lib/utils/errorHandler';
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -92,9 +93,8 @@ export default function ResetPasswordPage() {
|
||||
}, 3000);
|
||||
} catch (err: any) {
|
||||
console.error('Reset password error:', err);
|
||||
setError(
|
||||
err.response?.data?.message || 'Failed to reset password. The link may have expired.'
|
||||
);
|
||||
const errorData = extractError(err);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import { Loader2, RefreshCw, Activity, Brain, TrendingUp, Baby, Link } from 'lucide-react';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { extractError } from '@/lib/utils/errorHandler';
|
||||
|
||||
export default function AdvancedAnalyticsPage() {
|
||||
const { user } = useAuth();
|
||||
@@ -105,9 +106,10 @@ export default function AdvancedAnalyticsPage() {
|
||||
}
|
||||
}
|
||||
setError('');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AdvancedAnalytics] Failed to load children:', error);
|
||||
setError('Failed to load children');
|
||||
const errorData = extractError(error);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -129,8 +131,9 @@ export default function AdvancedAnalyticsPage() {
|
||||
try {
|
||||
const data = await analyticsApi.getCircadianRhythm(selectedChildId, 14);
|
||||
setCircadianData(data);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AdvancedAnalytics] Failed to load circadian rhythm:', error);
|
||||
console.error("Circadian error:", extractError(error).message);
|
||||
setCircadianError(error as Error);
|
||||
} finally {
|
||||
setCircadianLoading(false);
|
||||
@@ -145,8 +148,9 @@ export default function AdvancedAnalyticsPage() {
|
||||
try {
|
||||
const data = await analyticsApi.getAnomalies(selectedChildId, 30);
|
||||
setAnomalyData(data);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AdvancedAnalytics] Failed to load anomalies:', error);
|
||||
console.error("Anomalies error:", extractError(error).message);
|
||||
setAnomalyError(error as Error);
|
||||
} finally {
|
||||
setAnomalyLoading(false);
|
||||
@@ -161,8 +165,9 @@ export default function AdvancedAnalyticsPage() {
|
||||
try {
|
||||
const data = await analyticsApi.getGrowthAnalysis(selectedChildId);
|
||||
setGrowthData(data);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AdvancedAnalytics] Failed to load growth analysis:', error);
|
||||
console.error("Growth error:", extractError(error).message);
|
||||
setGrowthError(error as Error);
|
||||
} finally {
|
||||
setGrowthLoading(false);
|
||||
@@ -177,8 +182,9 @@ export default function AdvancedAnalyticsPage() {
|
||||
try {
|
||||
const data = await analyticsApi.getCorrelations(selectedChildId, 14);
|
||||
setCorrelationData(data);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AdvancedAnalytics] Failed to load correlations:', error);
|
||||
console.error("Correlations error:", extractError(error).message);
|
||||
setCorrelationError(error as Error);
|
||||
} finally {
|
||||
setCorrelationLoading(false);
|
||||
@@ -197,8 +203,9 @@ export default function AdvancedAnalyticsPage() {
|
||||
]);
|
||||
setSleepTrendData(sleepTrend);
|
||||
setFeedingTrendData(feedingTrend);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AdvancedAnalytics] Failed to load trends:', error);
|
||||
console.error("Trends error:", extractError(error).message);
|
||||
setTrendError(error as Error);
|
||||
} finally {
|
||||
setTrendLoading(false);
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { childrenApi, Child } from '@/lib/api/children';
|
||||
import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics';
|
||||
import PredictionsCard from '@/components/features/analytics/PredictionsCard';
|
||||
import { extractError } from '@/lib/utils/errorHandler';
|
||||
import GrowthSpurtAlert from '@/components/features/analytics/GrowthSpurtAlert';
|
||||
import WeeklyReportCard from '@/components/features/analytics/WeeklyReportCard';
|
||||
import MonthlyReportCard from '@/components/features/analytics/MonthlyReportCard';
|
||||
@@ -121,9 +122,10 @@ export default function AnalyticsPage() {
|
||||
}
|
||||
}
|
||||
setError('');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('[AnalyticsPage] Failed to load children:', error);
|
||||
setError('Failed to load children');
|
||||
const errorData = extractError(error);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -136,8 +138,10 @@ export default function AnalyticsPage() {
|
||||
try {
|
||||
const data = await analyticsApi.getInsights(selectedChildId, days);
|
||||
setInsights(data);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load insights:', error);
|
||||
const errorData = extractError(error);
|
||||
console.error('Insights error:', errorData.message);
|
||||
} finally {
|
||||
setInsightsLoading(false);
|
||||
}
|
||||
@@ -150,8 +154,10 @@ export default function AnalyticsPage() {
|
||||
try {
|
||||
const data = await analyticsApi.getPredictions(selectedChildId);
|
||||
setPredictions(data);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load predictions:', error);
|
||||
const errorData = extractError(error);
|
||||
console.error('Predictions error:', errorData.message);
|
||||
} finally {
|
||||
setPredictionsLoading(false);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { motion } from 'framer-motion';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
|
||||
import { useSelectedFamily } from '@/hooks/useSelectedFamily';
|
||||
import { extractError } from '@/lib/utils/errorHandler';
|
||||
|
||||
export default function ChildrenPage() {
|
||||
const { t } = useTranslation('children');
|
||||
@@ -66,7 +67,8 @@ export default function ChildrenPage() {
|
||||
setChildren(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch children:', err);
|
||||
setError(err.response?.data?.message || t('errors.loadFailed'));
|
||||
const errorData = extractError(err);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -103,7 +105,8 @@ export default function ChildrenPage() {
|
||||
setDialogOpen(false);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to save child:', err);
|
||||
throw new Error(err.response?.data?.message || t('errors.saveFailed'));
|
||||
const errorData = extractError(err);
|
||||
throw new Error(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
@@ -120,7 +123,8 @@ export default function ChildrenPage() {
|
||||
setChildToDelete(null);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete child:', err);
|
||||
setError(err.response?.data?.message || t('errors.deleteFailed'));
|
||||
const errorData = extractError(err);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { MeasurementUnitSelector } from '@/components/settings/MeasurementUnitSe
|
||||
import { TimeZoneSelector } from '@/components/settings/TimeZoneSelector';
|
||||
import { TimeFormatSelector } from '@/components/settings/TimeFormatSelector';
|
||||
import { PhotoUpload } from '@/components/common/PhotoUpload';
|
||||
import { extractError } from '@/lib/utils/errorHandler';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useThemeContext } from '@/contexts/ThemeContext';
|
||||
@@ -103,7 +104,8 @@ export default function SettingsPage() {
|
||||
} catch (err: any) {
|
||||
console.error('❌ Failed to save settings:', err);
|
||||
console.error('Error response:', err.response);
|
||||
setError(err.response?.data?.message || err.message || 'Failed to save settings. Please try again.');
|
||||
const errorData = extractError(err);
|
||||
setError(errorData.userMessage || errorData.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,25 @@
|
||||
# This script compares the development and production databases and automatically
|
||||
# synchronizes the production database to match the development schema.
|
||||
#
|
||||
# DATABASE ENVIRONMENTS:
|
||||
# ----------------------
|
||||
# Development Environment (this server: /root/maternal-app):
|
||||
# - Uses database: parentflowdev
|
||||
# - Location: 10.0.0.207
|
||||
# - This is where we develop and test changes
|
||||
#
|
||||
# Production Environment (server: 10.0.0.240 /root/parentflowapp-prod):
|
||||
# - Uses database: parentflow
|
||||
# - Location: 10.0.0.207 (same database server, different database)
|
||||
# - This is the live application serving users
|
||||
#
|
||||
# Admin Database (shared between environments):
|
||||
# - Uses database: parentflowadmin
|
||||
# - Location: 10.0.0.207
|
||||
# - Shared admin panel database for both dev and production
|
||||
#
|
||||
# This script syncs: parentflowdev (dev) → parentflow (production)
|
||||
#
|
||||
# Features:
|
||||
# - Creates missing tables in production
|
||||
# - Adds missing columns to existing tables
|
||||
@@ -36,8 +55,9 @@ NC='\033[0m' # No Color
|
||||
DB_HOST="10.0.0.207"
|
||||
DB_USER="postgres"
|
||||
DB_PASSWORD="a3ppq"
|
||||
DEV_DB="parentflowdev"
|
||||
PROD_DB="parentflow"
|
||||
DEV_DB="parentflowdev" # Development database (used by /root/maternal-app)
|
||||
PROD_DB="parentflow" # Production database (used by 10.0.0.240:/root/parentflowapp-prod)
|
||||
ADMIN_DB="parentflowadmin" # Shared admin database (both environments)
|
||||
|
||||
# Backup configuration
|
||||
BACKUP_DIR="/root/maternal-app/backups/db-schema-sync"
|
||||
|
||||
Reference in New Issue
Block a user