diff --git a/services/platform-service/src/modules/delivery/delivery.test.ts b/services/platform-service/src/modules/delivery/delivery.test.ts index 8f2b3d41..f2ca8d0d 100644 --- a/services/platform-service/src/modules/delivery/delivery.test.ts +++ b/services/platform-service/src/modules/delivery/delivery.test.ts @@ -81,8 +81,8 @@ describe('SendTestEmailSchema', () => { // ── Template Tests ─────────────────────────────────────────── describe('templates', () => { - it('should have 8 built-in templates', () => { - expect(BUILT_IN_TEMPLATES.length).toBe(8); + it('should have 12 built-in templates', () => { + expect(BUILT_IN_TEMPLATES.length).toBe(12); }); it('should find template by ID', () => { @@ -105,6 +105,11 @@ describe('templates', () => { expect(ids).toContain('invitation'); expect(ids).toContain('payment-failed'); expect(ids).toContain('license-expiring'); + // Diagnostics templates (Phase 1.5) + expect(ids).toContain('diagnostics-session-created'); + expect(ids).toContain('diagnostics-session-cancelled'); + expect(ids).toContain('diagnostics-session-completed'); + expect(ids).toContain('diagnostics-fatal-alert'); }); it('each template should have required fields', () => { diff --git a/services/platform-service/src/modules/delivery/templates.ts b/services/platform-service/src/modules/delivery/templates.ts index a980c1da..f28c1ba0 100644 --- a/services/platform-service/src/modules/delivery/templates.ts +++ b/services/platform-service/src/modules/delivery/templates.ts @@ -122,6 +122,85 @@ export const BUILT_IN_TEMPLATES: EmailTemplate[] = [ bodyText: `License expires in {{daysLeft}} days.\n\nRenew: {{renewUrl}}\n\n— The {{productName}} Team`, variables: ['displayName', 'productName', 'daysLeft', 'renewUrl'], }, + // ── Diagnostics Templates ───────────────────────────────────── + { + id: 'diagnostics-session-created', + name: 'Diagnostics Session Created', + subject: 'Debug session started for your {{productName}} device', + bodyHtml: ` +
Hi {{displayName}},
+Our engineering team has initiated a debug session for your device to help resolve an issue you reported.
+Session ID: {{sessionId}}
+ Started: {{startedAt}}
+ Duration: Up to {{maxDurationMinutes}} minutes
We'll collect diagnostic data including logs and traces. No action needed on your part.
+Questions? Reply to this email or contact support.
+— The {{productName}} Engineering Team
+ `.trim(), + bodyText: `Debug Session Active\n\nHi {{displayName}},\n\nOur engineering team has initiated a debug session for your device to help resolve an issue you reported.\n\nSession ID: {{sessionId}}\nStarted: {{startedAt}}\nDuration: Up to {{maxDurationMinutes}} minutes\n\nWe'll collect diagnostic data including logs and traces. No action needed on your part.\n\n— The {{productName}} Engineering Team`, + variables: ['displayName', 'productName', 'sessionId', 'startedAt', 'maxDurationMinutes'], + }, + { + id: 'diagnostics-session-cancelled', + name: 'Diagnostics Session Cancelled', + subject: 'Debug session cancelled — {{productName}}', + bodyHtml: ` +The debug session {{sessionId}} for user {{targetUserId}} has been cancelled.
+Cancelled by: {{cancelledBy}}
+ Reason: {{reason}}
+ Cancelled at: {{cancelledAt}}
No further diagnostic data will be collected.
+— The {{productName}} Engineering Team
+ `.trim(), + bodyText: `Debug Session Cancelled\n\nThe debug session {{sessionId}} for user {{targetUserId}} has been cancelled.\n\nCancelled by: {{cancelledBy}}\nReason: {{reason}}\nCancelled at: {{cancelledAt}}\n\nNo further diagnostic data will be collected.\n\n— The {{productName}} Engineering Team`, + variables: ['productName', 'sessionId', 'targetUserId', 'cancelledBy', 'reason', 'cancelledAt'], + }, + { + id: 'diagnostics-session-completed', + name: 'Diagnostics Session Completed', + subject: 'Debug session completed — {{productName}}', + bodyHtml: ` +The debug session {{sessionId}} has completed.
+Summary:
+View full results in the admin dashboard.
+— The {{productName}} Engineering Team
+ `.trim(), + bodyText: `Debug Session Completed\n\nThe debug session {{sessionId}} has completed.\n\nSummary:\n- Duration: {{durationMinutes}} minutes\n- Logs collected: {{logCount}}\n- Traces collected: {{traceCount}}\n- Screenshots: {{screenshotCount}}\n\nView full results in the admin dashboard.\n\n— The {{productName}} Engineering Team`, + variables: [ + 'productName', + 'sessionId', + 'durationMinutes', + 'logCount', + 'traceCount', + 'screenshotCount', + ], + }, + { + id: 'diagnostics-fatal-alert', + name: 'Diagnostics Fatal Log Alert', + subject: '🚨 FATAL log detected — {{productName}} debug session', + bodyHtml: ` +A fatal error was captured during debug session {{sessionId}}.
+Product: {{productName}}
+ User: {{userId}}
+ Time: {{timestamp}}
+ Message: {{message}}
— The {{productName}} Alert System
+ `.trim(), + bodyText: `🚨 FATAL Log Detected\n\nA fatal error was captured during debug session {{sessionId}}.\n\nProduct: {{productName}}\nUser: {{userId}}\nTime: {{timestamp}}\nMessage: {{message}}\n\nView in Dashboard: {{dashboardUrl}}\n\n— The {{productName}} Alert System`, + variables: ['productName', 'sessionId', 'userId', 'timestamp', 'message', 'dashboardUrl'], + }, ]; /** diff --git a/services/platform-service/src/modules/diagnostics/subscribers.ts b/services/platform-service/src/modules/diagnostics/subscribers.ts index 0a1fa3b4..4d160b68 100644 --- a/services/platform-service/src/modules/diagnostics/subscribers.ts +++ b/services/platform-service/src/modules/diagnostics/subscribers.ts @@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto'; const noopLog = { info: (..._a: unknown[]) => {}, + warn: (..._a: unknown[]) => {}, error: (..._a: unknown[]) => {}, }; @@ -39,13 +40,18 @@ export function registerDiagnosticsSubscribers( }; await auditRepo.create(auditDoc); - // TODO: Send notification to target user (email/push) via notifications module + // Send email notification to target user if email is available + // Note: In production, we'd look up the user's email from the users repository + // For now, we log that notification would be sent log.info( { sessionId: event.payload.sessionId, targetUserId: event.payload.targetUserId }, '[diagnostics/subscriber] Session created, user notification queued' ); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.created'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle session.created' + ); } }); @@ -66,7 +72,10 @@ export function registerDiagnosticsSubscribers( }; await auditRepo.create(auditDoc); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.started'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle session.started' + ); } }); @@ -87,7 +96,10 @@ export function registerDiagnosticsSubscribers( }; await auditRepo.create(auditDoc); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.updated'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle session.updated' + ); } }); @@ -114,7 +126,10 @@ export function registerDiagnosticsSubscribers( '[diagnostics/subscriber] Session cancelled, admin notification queued' ); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.cancelled'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle session.cancelled' + ); } }); @@ -142,7 +157,10 @@ export function registerDiagnosticsSubscribers( '[diagnostics/subscriber] Session completed, summary email queued' ); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.completed'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle session.completed' + ); } }); @@ -163,7 +181,10 @@ export function registerDiagnosticsSubscribers( }; await auditRepo.create(auditDoc); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle session.expired'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle session.expired' + ); } }); @@ -192,7 +213,10 @@ export function registerDiagnosticsSubscribers( '[diagnostics/subscriber] FATAL log ingested — alerting on-call engineer' ); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle ingest.fatal'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle ingest.fatal' + ); } }); @@ -214,7 +238,10 @@ export function registerDiagnosticsSubscribers( }; await auditRepo.create(auditDoc); } catch (err) { - log.error({ err, eventId: event.id }, '[diagnostics/subscriber] Failed to handle screenshot.captured'); + log.error( + { err, eventId: event.id }, + '[diagnostics/subscriber] Failed to handle screenshot.captured' + ); } }); diff --git a/services/platform-service/src/modules/ratelimit/routes.ts b/services/platform-service/src/modules/ratelimit/routes.ts index 1d80cfff..98b43c9a 100644 --- a/services/platform-service/src/modules/ratelimit/routes.ts +++ b/services/platform-service/src/modules/ratelimit/routes.ts @@ -40,6 +40,10 @@ function loadConfig(): Map