@studious-lms/server 1.26.0 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.env.example +3 -1
  2. package/.github/workflows/npm-publish.yml +1 -0
  3. package/AGENTS.md +84 -0
  4. package/dist/models/cloudStorage.d.ts +11 -0
  5. package/dist/models/cloudStorage.d.ts.map +1 -1
  6. package/dist/models/cloudStorage.js +18 -2
  7. package/dist/models/cloudStorage.js.map +1 -1
  8. package/dist/models/conversation.d.ts +1 -0
  9. package/dist/models/conversation.d.ts.map +1 -1
  10. package/dist/models/labChat.d.ts +40 -0
  11. package/dist/models/labChat.d.ts.map +1 -1
  12. package/dist/models/labChat.js +32 -2
  13. package/dist/models/labChat.js.map +1 -1
  14. package/dist/models/message.d.ts +3 -0
  15. package/dist/models/message.d.ts.map +1 -1
  16. package/dist/pipelines/aiLabChat.d.ts +2 -0
  17. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  18. package/dist/pipelines/aiLabChat.js +60 -16
  19. package/dist/pipelines/aiLabChat.js.map +1 -1
  20. package/dist/pipelines/aiLabChatContract.d.ts +170 -2
  21. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  22. package/dist/pipelines/aiLabChatContract.js +30 -2
  23. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  24. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  25. package/dist/pipelines/aiNewtonChat.js +30 -5
  26. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  27. package/dist/pipelines/labChatPrompt.d.ts +1 -1
  28. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  29. package/dist/pipelines/labChatPrompt.js +13 -14
  30. package/dist/pipelines/labChatPrompt.js.map +1 -1
  31. package/dist/routers/_app.d.ts +220 -10
  32. package/dist/routers/_app.d.ts.map +1 -1
  33. package/dist/routers/admin.d.ts +0 -1
  34. package/dist/routers/admin.d.ts.map +1 -1
  35. package/dist/routers/conversation.d.ts +1 -0
  36. package/dist/routers/conversation.d.ts.map +1 -1
  37. package/dist/routers/labChat.d.ts +10 -0
  38. package/dist/routers/labChat.d.ts.map +1 -1
  39. package/dist/routers/labChat.js +6 -3
  40. package/dist/routers/labChat.js.map +1 -1
  41. package/dist/routers/message.d.ts +37 -4
  42. package/dist/routers/message.d.ts.map +1 -1
  43. package/dist/routers/message.js +24 -7
  44. package/dist/routers/message.js.map +1 -1
  45. package/dist/routers/parent.d.ts +18 -0
  46. package/dist/routers/parent.d.ts.map +1 -1
  47. package/dist/routers/parent.js +10 -3
  48. package/dist/routers/parent.js.map +1 -1
  49. package/dist/routers/studentProgress.d.ts +44 -0
  50. package/dist/routers/studentProgress.d.ts.map +1 -1
  51. package/dist/routers/studentProgress.js +12 -3
  52. package/dist/routers/studentProgress.js.map +1 -1
  53. package/dist/routes/aiStream.js +32 -2
  54. package/dist/routes/aiStream.js.map +1 -1
  55. package/dist/seedDatabase.d.ts.map +1 -1
  56. package/dist/seedDatabase.js +107 -3
  57. package/dist/seedDatabase.js.map +1 -1
  58. package/dist/services/adminUser.d.ts +0 -1
  59. package/dist/services/adminUser.d.ts.map +1 -1
  60. package/dist/services/adminUser.js +7 -4
  61. package/dist/services/adminUser.js.map +1 -1
  62. package/dist/services/aiChatStream.d.ts +15 -0
  63. package/dist/services/aiChatStream.d.ts.map +1 -1
  64. package/dist/services/aiChatStream.js +144 -74
  65. package/dist/services/aiChatStream.js.map +1 -1
  66. package/dist/services/aiGeneration.d.ts +4 -0
  67. package/dist/services/aiGeneration.d.ts.map +1 -0
  68. package/dist/services/aiGeneration.js +88 -0
  69. package/dist/services/aiGeneration.js.map +1 -0
  70. package/dist/services/aiProposalTools.d.ts +4 -1
  71. package/dist/services/aiProposalTools.d.ts.map +1 -1
  72. package/dist/services/aiProposalTools.js +128 -3
  73. package/dist/services/aiProposalTools.js.map +1 -1
  74. package/dist/services/aiProposals.d.ts +70 -0
  75. package/dist/services/aiProposals.d.ts.map +1 -0
  76. package/dist/services/aiProposals.js +363 -0
  77. package/dist/services/aiProposals.js.map +1 -0
  78. package/dist/services/conversation.d.ts +1 -0
  79. package/dist/services/conversation.d.ts.map +1 -1
  80. package/dist/services/labChat.d.ts +10 -0
  81. package/dist/services/labChat.d.ts.map +1 -1
  82. package/dist/services/labChat.js +52 -3
  83. package/dist/services/labChat.js.map +1 -1
  84. package/dist/services/message.d.ts +24 -8
  85. package/dist/services/message.d.ts.map +1 -1
  86. package/dist/services/message.js +170 -117
  87. package/dist/services/message.js.map +1 -1
  88. package/dist/services/parentPortal.d.ts +14 -0
  89. package/dist/services/parentPortal.d.ts.map +1 -1
  90. package/dist/services/parentPortal.js +72 -2
  91. package/dist/services/parentPortal.js.map +1 -1
  92. package/dist/services/studentProgress.d.ts +50 -0
  93. package/dist/services/studentProgress.d.ts.map +1 -1
  94. package/dist/services/studentProgress.js +107 -2
  95. package/dist/services/studentProgress.js.map +1 -1
  96. package/dist/utils/inference.d.ts +10 -0
  97. package/dist/utils/inference.d.ts.map +1 -1
  98. package/dist/utils/inference.js +77 -2
  99. package/dist/utils/inference.js.map +1 -1
  100. package/dist/workers/processors/createRunJobWorker.js +17 -16
  101. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  102. package/package.json +1 -1
  103. package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
  104. package/prisma/schema.prisma +8 -0
  105. package/src/models/cloudStorage.ts +19 -0
  106. package/src/models/labChat.ts +34 -0
  107. package/src/pipelines/aiLabChat.ts +79 -18
  108. package/src/pipelines/aiLabChatContract.ts +35 -0
  109. package/src/pipelines/aiNewtonChat.ts +34 -2
  110. package/src/pipelines/labChatPrompt.ts +11 -12
  111. package/src/routers/labChat.ts +5 -0
  112. package/src/routers/message.ts +34 -5
  113. package/src/routers/parent.ts +11 -0
  114. package/src/routers/studentProgress.ts +15 -0
  115. package/src/routes/aiStream.ts +32 -0
  116. package/src/seedDatabase.ts +107 -2
  117. package/src/services/adminUser.ts +5 -2
  118. package/src/services/aiChatStream.ts +83 -7
  119. package/src/services/aiGeneration.ts +92 -0
  120. package/src/services/aiProposalTools.ts +166 -1
  121. package/src/services/aiProposals.ts +428 -0
  122. package/src/services/labChat.ts +65 -0
  123. package/src/services/message.ts +205 -142
  124. package/src/services/parentPortal.ts +85 -0
  125. package/src/services/studentProgress.ts +135 -0
  126. package/src/utils/inference.ts +85 -0
  127. package/src/workers/processors/createRunJobWorker.ts +17 -17
  128. package/tests/unit/services/adminUser.test.ts +75 -0
  129. package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
@@ -91,6 +91,14 @@ export function createRunJobWorker<T extends RunJobType>({
91
91
  return existing.result;
92
92
  }
93
93
 
94
+ if (existing.status === 'CANCELLED') {
95
+ logger.info(`${name} RunJob cancelled before start; skipping`, {
96
+ runJobId,
97
+ bullJobId: job.id,
98
+ });
99
+ return existing.result;
100
+ }
101
+
94
102
  await prisma.runJob.update({
95
103
  where: { id: runJobId },
96
104
  data: {
@@ -108,8 +116,10 @@ export function createRunJobWorker<T extends RunJobType>({
108
116
  classId: existing.classId ?? null,
109
117
  });
110
118
 
111
- await prisma.runJob.update({
112
- where: { id: runJobId },
119
+ // `updateMany` with a status guard so a cancellation that landed while
120
+ // the run was in flight is not overwritten with SUCCEEDED.
121
+ await prisma.runJob.updateMany({
122
+ where: { id: runJobId, status: { not: 'CANCELLED' } },
113
123
  data: {
114
124
  status: 'SUCCEEDED',
115
125
  finishedAt: new Date(),
@@ -146,29 +156,19 @@ export function createRunJobWorker<T extends RunJobType>({
146
156
 
147
157
  if (!runJobId) return;
148
158
 
159
+ // Status guard: don't let a late failure (e.g. from an aborted stream)
160
+ // overwrite a RunJob the user already cancelled.
149
161
  await prisma.runJob
150
- .update({
151
- where: { id: runJobId },
162
+ .updateMany({
163
+ where: { id: runJobId, status: { not: 'CANCELLED' } },
152
164
  data: {
153
165
  error: err.message,
154
166
  ...(isFinalAttempt
155
- ? { status: 'FAILED', finishedAt: new Date() }
167
+ ? { status: 'FAILED' as const, finishedAt: new Date() }
156
168
  : {}),
157
169
  },
158
170
  })
159
171
  .catch((updateError) => {
160
- // P2025 = record not found. The RunJob row is gone (orphaned job from a
161
- // shared/reset Redis), so there's nothing to flag as FAILED — log
162
- // quietly instead of as an error to avoid noise.
163
- if (
164
- updateError instanceof Prisma.PrismaClientKnownRequestError &&
165
- updateError.code === 'P2025'
166
- ) {
167
- logger.warn(`${name} failure not recorded; RunJob no longer exists`, {
168
- runJobId,
169
- });
170
- return;
171
- }
172
172
  logger.error(`Failed to record ${name} failure on RunJob`, {
173
173
  runJobId,
174
174
  error:
@@ -0,0 +1,75 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { TRPCError } from '@trpc/server';
3
+
4
+ const prismaMocks = vi.hoisted(() => ({
5
+ user: { findUnique: vi.fn() },
6
+ }));
7
+
8
+ vi.mock('../../../src/lib/prisma.js', () => ({
9
+ prisma: prismaMocks,
10
+ }));
11
+
12
+ import { getUserForAdmin } from '../../../src/services/adminUser.js';
13
+
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('getUserForAdmin', () => {
19
+ test('does not select session ids (they are bearer tokens) and only returns AUTH sessions', async () => {
20
+ prismaMocks.user.findUnique.mockResolvedValue({
21
+ id: 'user-1',
22
+ username: 'ada',
23
+ email: 'ada@example.com',
24
+ verified: true,
25
+ termsAcceptedAt: null,
26
+ termsVersion: null,
27
+ profile: null,
28
+ school: null,
29
+ schoolMemberships: [],
30
+ sessions: [
31
+ {
32
+ type: 'AUTH',
33
+ createdAt: new Date('2026-08-01T00:00:00Z'),
34
+ expiresAt: new Date('2026-09-01T00:00:00Z'),
35
+ },
36
+ ],
37
+ _count: {
38
+ teacherIn: 0,
39
+ studentIn: 0,
40
+ sessions: 1,
41
+ submissions: 0,
42
+ files: 0,
43
+ },
44
+ });
45
+
46
+ const user = await getUserForAdmin('user-1');
47
+
48
+ expect(prismaMocks.user.findUnique).toHaveBeenCalledWith(
49
+ expect.objectContaining({
50
+ where: { id: 'user-1' },
51
+ select: expect.objectContaining({
52
+ sessions: {
53
+ where: { type: 'AUTH' },
54
+ orderBy: { createdAt: 'desc' },
55
+ take: 10,
56
+ select: { type: true, createdAt: true, expiresAt: true },
57
+ },
58
+ _count: {
59
+ select: expect.objectContaining({
60
+ sessions: { where: { type: 'AUTH' } },
61
+ }),
62
+ },
63
+ }),
64
+ }),
65
+ );
66
+ expect(user.sessions[0]).not.toHaveProperty('id');
67
+ });
68
+
69
+ test('throws NOT_FOUND when the user is missing', async () => {
70
+ prismaMocks.user.findUnique.mockResolvedValue(null);
71
+ await expect(getUserForAdmin('missing')).rejects.toMatchObject({
72
+ code: 'NOT_FOUND',
73
+ } satisfies Partial<TRPCError>);
74
+ });
75
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { TRPCError } from '@trpc/server';
3
+ import type { Request } from 'express';
4
+ import { assertAdminPanelPassword } from '../../../src/utils/adminPanelAccess.js';
5
+
6
+ function reqWithHeader(value: string | string[] | undefined): Request {
7
+ return { headers: { 'x-admin-password': value } } as unknown as Request;
8
+ }
9
+
10
+ describe('assertAdminPanelPassword', () => {
11
+ test('accepts the configured admin password header', () => {
12
+ expect(() => assertAdminPanelPassword(reqWithHeader('alan6767'))).not.toThrow();
13
+ });
14
+
15
+ test('rejects a missing or wrong header with FORBIDDEN', () => {
16
+ expect(() => assertAdminPanelPassword(reqWithHeader(undefined))).toThrow(
17
+ TRPCError,
18
+ );
19
+ try {
20
+ assertAdminPanelPassword(reqWithHeader('nope'));
21
+ } catch (error) {
22
+ expect(error).toBeInstanceOf(TRPCError);
23
+ expect((error as TRPCError).code).toBe('FORBIDDEN');
24
+ }
25
+ });
26
+ });