eletrotupi / tcc/ commit / 58fc6ed

api: introduce daily energy insight

Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago 58fc6edcba4ecd00dfece8d7b69e529d18da9ae5
Parents: a2d0b31
6 file(s) changed
  • api/prisma/migrations/20260614055540_add_daily_energy_insight_type/migration.sql +2 -0
  • api/prisma/schema.prisma +1 -0
  • api/src/lib/queue/processors/insights/dailyEnergy.ts +102 -0
  • api/src/lib/queue/processors/insights/index.ts +4 -0
  • api/src/lib/queue/types.ts +3 -1
  • api/src/utils/calc.ts +3 -0
api/prisma/migrations/20260614055540_add_daily_energy_insight_type/migration.sql
@@ -0,0 +1,2 @@
1 + -- AlterEnum
2 + ALTER TYPE "InsightType" ADD VALUE 'DAILY_ENERGY';
api/prisma/schema.prisma
1 1
generator client {
2 2
  provider = "prisma-client-js"
3 3
  //output   = "../src/generated/prisma"
4 4
}
5 5

▸ 130 unchanged lines
136 136
  MOOD_TREND
137 137
  ENERGY_SLEEP_CORRELATION
138 138
  TRIGGER_PATTERN
139 139
  WEEKLY_SUMMARY
140 140
  STREAK
141 +
  DAILY_ENERGY
141 142
}
142 143

143 144
enum InsightPeriod {
144 145
  DAILY
145 146
  WEEKLY
▸ 40 unchanged lines
186 187
  ANXIETY
187 188
  GRATITUDE
188 189
  FOCUS
189 190
  TIREDNESS
190 191
}
api/src/lib/queue/processors/insights/dailyEnergy.ts
@@ -0,0 +1,102 @@
1 + import { prisma } from "@app/lib/prisma";
2 + import { InsightType, InsightPeriod } from "@prisma/client";
3 + import { InsightPeriodPayload } from "@app/lib/queue/types";
4 + import { avg } from "@app/utils/calc";
5 +
6 + export async function processDailyEnergy({
7 + userId,
8 + periodStart,
9 + periodEnd,
10 + }: InsightPeriodPayload): Promise<void> {
11 + const start = new Date(periodStart);
12 + const end = new Date(periodEnd);
13 +
14 + const moods = await prisma.mood.findMany({
15 + where: { userId, moment: { gte: start, lte: end } },
16 + orderBy: { moment: "asc" },
17 + select: {
18 + energyLevel: true,
19 + stressLevel: true,
20 + anxietyLevel: true,
21 + selectedMood: true,
22 + moment: true,
23 + },
24 + });
25 +
26 + if (moods.length === 0) return;
27 +
28 + const avgEnergy = parseFloat(avg(moods.map((m) => m.energyLevel)).toFixed(2));
29 + const avgStress = parseFloat(avg(moods.map((m) => m.stressLevel)).toFixed(2));
30 + const avgAnxiety = parseFloat(
31 + avg(moods.map((m) => m.anxietyLevel)).toFixed(2),
32 + );
33 + const peak = Math.max(...moods.map((m) => m.energyLevel));
34 + const trough = Math.min(...moods.map((m) => m.energyLevel));
35 +
36 + // Trend across the day: compare first half vs second half entries
37 + // Not that great, but it works
38 + const mid = Math.floor(moods.length / 2);
39 + const morningAvg =
40 + moods.length > 1
41 + ? avg(moods.slice(0, mid).map((m) => m.energyLevel))
42 + : null;
43 +
44 + const eveningAvg =
45 + moods.length > 1 ? avg(moods.slice(mid).map((m) => m.energyLevel)) : null;
46 +
47 + const dayTrend =
48 + morningAvg && eveningAvg
49 + ? parseFloat((((eveningAvg - morningAvg) / morningAvg) * 100).toFixed(1))
50 + : null;
51 +
52 + await prisma.insight.upsert({
53 + where: {
54 + userId_type_periodStart: {
55 + userId,
56 + type: InsightType.DAILY_ENERGY,
57 + periodStart: start,
58 + },
59 + },
60 + update: {
61 + body: buildBody(avgEnergy, dayTrend),
62 + metadata: {
63 + avgEnergy,
64 + avgStress,
65 + avgAnxiety,
66 + peak,
67 + trough,
68 + dayTrend,
69 + entryCount: moods.length,
70 + },
71 + generatedAt: new Date(),
72 + },
73 + create: {
74 + userId,
75 + type: InsightType.DAILY_ENERGY,
76 + period: InsightPeriod.DAILY,
77 + title: "Energia do Dia",
78 + body: buildBody(avgEnergy, dayTrend),
79 + metadata: {
80 + avgEnergy,
81 + avgStress,
82 + avgAnxiety,
83 + peak,
84 + trough,
85 + dayTrend,
86 + entryCount: moods.length,
87 + },
88 + periodStart: start,
89 + periodEnd: end,
90 + },
91 + });
92 + }
93 +
94 + function buildBody(avgEnergy: number, dayTrend: number | null): string {
95 + const level = avgEnergy >= 7 ? "alta" : avgEnergy >= 4 ? "moderada" : "baixa";
96 + if (dayTrend === null) return `Energia ${level} hoje.`;
97 + if (dayTrend > 10)
98 + return `Energia ${level} hoje, com melhora ao longo do dia.`;
99 + if (dayTrend < -10)
100 + return `Energia ${level} hoje, com queda ao longo do dia.`;
101 + return `Energia ${level} e estável ao longo do dia.`;
102 + }
api/src/lib/queue/processors/insights/index.ts
1 1
import { Job } from "bullmq";
2 2
import {
3 3
  InsightJobName,
4 4
  InsightJobData,
5 5
  InsightPeriodPayload,
▸ 1 unchanged lines
7 7
import { getQueue } from "@app/lib/queue/QueueRegistry";
8 8
import { prisma } from "@app/lib/prisma";
9 9
import { processMoodTrend } from "@app/lib/queue/processors/insights/moodTrend";
10 10
import { processEnergySleep } from "@app/lib/queue/processors/insights/energySleep";
11 11
import { processTriggerPattern } from "@app/lib/queue/processors/insights/triggerPattern";
12 +
import { processDailyEnergy } from "@app/lib/queue/processors/insights/dailyEnergy";
12 13

13 14
export async function insightProcessor(
14 15
  job: Job<InsightJobData["data"]>,
15 16
): Promise<void> {
16 17
  console.log(`Insight job ${job.name} started`);
▸ 8 unchanged lines
25 26
    case InsightJobName.EnergySleep:
26 27
      return processEnergySleep(job.data as InsightPeriodPayload);
27 28

28 29
    case InsightJobName.TriggerPattern:
29 30
      return processTriggerPattern(job.data as InsightPeriodPayload);
31 +

32 +
    case InsightJobName.DailyEnergy:
33 +
      return processDailyEnergy(job.data as InsightPeriodPayload);
30 34

31 35
    default:
32 36
      throw new Error(`Unknown insight job: ${job.name}`);
33 37
  }
34 38
}
▸ 23 unchanged lines
58 62
        data: { userId: u.id, ...payload },
59 63
      },
60 64
    ]),
61 65
  );
62 66
}
api/src/lib/queue/types.ts
1 1
// Mail queue
2 2
export enum MailJobName {
3 3
  WelcomeEmail = "mail:welcome",
4 4
  PasswordReset = "mail:password-reset",
5 5
  ActivateAccountEmail = "mail:activate-account",
▸ 15 unchanged lines
21 21
export enum InsightJobName {
22 22
  FanOut = "insight:fan-out",
23 23
  MoodTrend = "insight:mood-trend",
24 24
  EnergySleep = "insight:energy-sleep",
25 25
  TriggerPattern = "insight:trigger-pattern",
26 +
  DailyEnergy = "insight:daily-energy"
26 27
}
27 28

28 29
export type InsightPeriodPayload = {
29 30
  userId: number;
30 31
  periodStart: string;
▸ 4 unchanged lines
35 36

36 37
export type InsightJobData =
37 38
  | { name: InsightJobName.FanOut; data: InsightFanOutPayload }
38 39
  | { name: InsightJobName.MoodTrend; data: InsightPeriodPayload }
39 40
  | { name: InsightJobName.EnergySleep; data: InsightPeriodPayload }
40 -
  | { name: InsightJobName.TriggerPattern; data: InsightPeriodPayload };
41 +
  | { name: InsightJobName.TriggerPattern; data: InsightPeriodPayload }
42 +
  | { name: InsightJobName.DailyEnergy; data: InsightPeriodPayload };
api/src/utils/calc.ts
@@ -0,0 +1,3 @@
1 + // TODO: Move other calculations here
2 + export const avg = (values: number[]) =>
3 + values.reduce((a, b) => a + b, 0) / values.length;