Add ntfy.sh

This commit is contained in:
Dries Augustyns
2025-12-04 18:10:21 +01:00
parent c114feb32e
commit 6cdbc43c8e
3 changed files with 65 additions and 3 deletions
+29 -1
View File
@@ -6,7 +6,9 @@
import {type Job, Worker} from 'bullmq'; import {type Job, Worker} from 'bullmq';
import {parse} from 'csv-parse/sync'; import {parse} from 'csv-parse/sync';
import {prisma} from '../database/prisma.js';
import {ContactService} from '../services/ContactService.js'; import {ContactService} from '../services/ContactService.js';
import {NtfyService} from '../services/NtfyService.js';
import {type ContactImportJobData, importQueue} from '../services/QueueService.js'; import {type ContactImportJobData, importQueue} from '../services/QueueService.js';
const BATCH_SIZE = 100; // Process contacts in batches of 100 const BATCH_SIZE = 100; // Process contacts in batches of 100
@@ -28,6 +30,14 @@ export function createImportWorker() {
console.log(`[IMPORT-PROCESSOR] Processing import for project ${projectId} (${filename})`); console.log(`[IMPORT-PROCESSOR] Processing import for project ${projectId} (${filename})`);
// Fetch project information for notifications
const project = await prisma.project.findUnique({
where: {id: projectId},
select: {name: true},
});
const projectName = project?.name || projectId;
const result: ImportResult = { const result: ImportResult = {
totalRows: 0, totalRows: 0,
successCount: 0, successCount: 0,
@@ -58,6 +68,9 @@ export function createImportWorker() {
console.log(`[IMPORT-PROCESSOR] Parsed ${records.length} rows from CSV`); console.log(`[IMPORT-PROCESSOR] Parsed ${records.length} rows from CSV`);
// Notify that import has started
await NtfyService.notifyContactImportStarted(projectName, projectId, filename, result.totalRows);
// Validate that 'email' column exists // Validate that 'email' column exists
const firstRecord = records[0]; const firstRecord = records[0];
if (firstRecord && typeof firstRecord === 'object' && !('email' in firstRecord)) { if (firstRecord && typeof firstRecord === 'object' && !('email' in firstRecord)) {
@@ -142,15 +155,30 @@ export function createImportWorker() {
`[IMPORT-PROCESSOR] Import completed: ${result.createdCount} created, ${result.updatedCount} updated, ${result.failureCount} failed`, `[IMPORT-PROCESSOR] Import completed: ${result.createdCount} created, ${result.updatedCount} updated, ${result.failureCount} failed`,
); );
// Notify that import has completed
await NtfyService.notifyContactImportCompleted(
projectName,
projectId,
filename,
result.successCount,
result.createdCount,
result.updatedCount,
result.failureCount,
);
return result; return result;
} catch (error) { } catch (error) {
console.error(`[IMPORT-PROCESSOR] Failed to process import:`, error); console.error(`[IMPORT-PROCESSOR] Failed to process import:`, error);
// Notify that import has failed
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
await NtfyService.notifyContactImportFailed(projectName, projectId, filename, errorMessage);
// Return partial results with error // Return partial results with error
result.errors.push({ result.errors.push({
row: 0, row: 0,
email: '', email: '',
error: error instanceof Error ? error.message : 'Unknown error', error: errorMessage,
}); });
throw error; // Re-throw to mark job as failed throw error; // Re-throw to mark job as failed
@@ -7,6 +7,7 @@ import {type Job, Worker} from 'bullmq';
import signale from 'signale'; import signale from 'signale';
import {prisma} from '../database/prisma.js'; import {prisma} from '../database/prisma.js';
import {NtfyService} from '../services/NtfyService.js';
import {type SegmentCountJobData, segmentCountQueue} from '../services/QueueService.js'; import {type SegmentCountJobData, segmentCountQueue} from '../services/QueueService.js';
import {SegmentService} from '../services/SegmentService.js'; import {SegmentService} from '../services/SegmentService.js';
@@ -42,6 +43,11 @@ async function processProjectSegments(projectId: string, projectName?: string):
signale.success( signale.success(
`[SEGMENT-COUNT-WORKER] Segment "${segment.name}": +${result.added} entries, -${result.removed} exits, ${result.total} total members`, `[SEGMENT-COUNT-WORKER] Segment "${segment.name}": +${result.added} entries, -${result.removed} exits, ${result.total} total members`,
); );
// Notify about segment membership update (only for tracked segments)
if (projectName) {
await NtfyService.notifySegmentMembershipComputed(segment.name, projectName, projectId, result.total);
}
} catch (error) { } catch (error) {
signale.error(`[SEGMENT-COUNT-WORKER] Failed to compute membership for segment ${segment.id}:`, error); signale.error(`[SEGMENT-COUNT-WORKER] Failed to compute membership for segment ${segment.id}:`, error);
// Continue with other segments // Continue with other segments
+30 -2
View File
@@ -5,6 +5,7 @@ import {prisma} from '../database/prisma.js';
import {HttpException} from '../exceptions/index.js'; import {HttpException} from '../exceptions/index.js';
import {EventService} from './EventService.js'; import {EventService} from './EventService.js';
import {NtfyService} from './NtfyService.js';
// Re-export types for use in other services // Re-export types for use in other services
export type {FilterCondition, FilterGroup, SegmentFilter} from '@plunk/types'; export type {FilterCondition, FilterGroup, SegmentFilter} from '@plunk/types';
@@ -115,7 +116,7 @@ export class SegmentService {
const where = this.buildWhereClause(projectId, data.condition); const where = this.buildWhereClause(projectId, data.condition);
const memberCount = await prisma.contact.count({where}); const memberCount = await prisma.contact.count({where});
return prisma.segment.create({ const segment = await prisma.segment.create({
data: { data: {
projectId, projectId,
name: data.name, name: data.name,
@@ -124,7 +125,17 @@ export class SegmentService {
trackMembership: data.trackMembership ?? false, trackMembership: data.trackMembership ?? false,
memberCount, memberCount,
}, },
include: {
project: {
select: {name: true},
},
},
}); });
// Notify about segment creation
await NtfyService.notifySegmentCreated(segment.name, segment.project.name, projectId);
return segment;
} }
/** /**
@@ -178,7 +189,21 @@ export class SegmentService {
*/ */
public static async delete(projectId: string, segmentId: string): Promise<void> { public static async delete(projectId: string, segmentId: string): Promise<void> {
// First verify segment exists and belongs to project // First verify segment exists and belongs to project
await this.get(projectId, segmentId); const segment = await prisma.segment.findFirst({
where: {
id: segmentId,
projectId,
},
include: {
project: {
select: {name: true},
},
},
});
if (!segment) {
throw new HttpException(404, 'Segment not found');
}
// Check if segment is used in any active campaigns // Check if segment is used in any active campaigns
const campaignsUsingSegment = await prisma.campaign.count({ const campaignsUsingSegment = await prisma.campaign.count({
@@ -200,6 +225,9 @@ export class SegmentService {
await prisma.segment.delete({ await prisma.segment.delete({
where: {id: segmentId}, where: {id: segmentId},
}); });
// Notify about segment deletion
await NtfyService.notifySegmentDeleted(segment.name, segment.project.name, projectId);
} }
/** /**