@prenta/core 8.9.2 → 8.11.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 (294) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/dist/actions.d.ts +25 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +51 -4
  5. package/dist/actions.js.map +1 -1
  6. package/dist/ai/batch-links.d.ts +105 -0
  7. package/dist/ai/batch-links.d.ts.map +1 -0
  8. package/dist/ai/batch-links.js +667 -0
  9. package/dist/ai/batch-links.js.map +1 -0
  10. package/dist/ai/batch-list.d.ts +127 -4
  11. package/dist/ai/batch-list.d.ts.map +1 -1
  12. package/dist/ai/batch-list.js +250 -14
  13. package/dist/ai/batch-list.js.map +1 -1
  14. package/dist/ai/providers/http.d.ts +4 -0
  15. package/dist/ai/providers/http.d.ts.map +1 -1
  16. package/dist/ai/providers/http.js +9 -8
  17. package/dist/ai/providers/http.js.map +1 -1
  18. package/dist/ai/tasks/editor-text.d.ts +39 -0
  19. package/dist/ai/tasks/editor-text.d.ts.map +1 -0
  20. package/dist/ai/tasks/editor-text.js +45 -0
  21. package/dist/ai/tasks/editor-text.js.map +1 -0
  22. package/dist/ai/tasks/link-embeddings.d.ts +22 -0
  23. package/dist/ai/tasks/link-embeddings.d.ts.map +1 -1
  24. package/dist/ai/tasks/link-embeddings.js +24 -10
  25. package/dist/ai/tasks/link-embeddings.js.map +1 -1
  26. package/dist/ai/usage.d.ts +7 -0
  27. package/dist/ai/usage.d.ts.map +1 -1
  28. package/dist/ai/usage.js +11 -0
  29. package/dist/ai/usage.js.map +1 -1
  30. package/dist/api/guarded-db.d.ts +6 -0
  31. package/dist/api/guarded-db.d.ts.map +1 -1
  32. package/dist/api/guarded-db.js +13 -1
  33. package/dist/api/guarded-db.js.map +1 -1
  34. package/dist/api/handlers.d.ts.map +1 -1
  35. package/dist/api/handlers.js +2 -0
  36. package/dist/api/handlers.js.map +1 -1
  37. package/dist/api/openapi.d.ts.map +1 -1
  38. package/dist/api/openapi.js +103 -0
  39. package/dist/api/openapi.js.map +1 -1
  40. package/dist/api/route-helpers/forms.d.ts.map +1 -1
  41. package/dist/api/route-helpers/forms.js +7 -1
  42. package/dist/api/route-helpers/forms.js.map +1 -1
  43. package/dist/api/routes/ai-authoring.d.ts.map +1 -1
  44. package/dist/api/routes/ai-authoring.js +1 -0
  45. package/dist/api/routes/ai-authoring.js.map +1 -1
  46. package/dist/api/routes/ai-seo.d.ts.map +1 -1
  47. package/dist/api/routes/ai-seo.js +20 -18
  48. package/dist/api/routes/ai-seo.js.map +1 -1
  49. package/dist/api/routes/media.d.ts.map +1 -1
  50. package/dist/api/routes/media.js +3 -1
  51. package/dist/api/routes/media.js.map +1 -1
  52. package/dist/api/routes/scheduling.d.ts.map +1 -1
  53. package/dist/api/routes/scheduling.js +33 -2
  54. package/dist/api/routes/scheduling.js.map +1 -1
  55. package/dist/appearance/color.d.ts +17 -0
  56. package/dist/appearance/color.d.ts.map +1 -1
  57. package/dist/appearance/color.js +162 -1
  58. package/dist/appearance/color.js.map +1 -1
  59. package/dist/appearance/index.d.ts +3 -3
  60. package/dist/appearance/index.d.ts.map +1 -1
  61. package/dist/appearance/index.js +2 -2
  62. package/dist/appearance/index.js.map +1 -1
  63. package/dist/appearance/resolve.d.ts.map +1 -1
  64. package/dist/appearance/resolve.js +4 -2
  65. package/dist/appearance/resolve.js.map +1 -1
  66. package/dist/appearance/theme-tokens.d.ts +60 -11
  67. package/dist/appearance/theme-tokens.d.ts.map +1 -1
  68. package/dist/appearance/theme-tokens.js +184 -175
  69. package/dist/appearance/theme-tokens.js.map +1 -1
  70. package/dist/appearance/types.d.ts +16 -2
  71. package/dist/appearance/types.d.ts.map +1 -1
  72. package/dist/astro-site.d.ts +1 -11
  73. package/dist/astro-site.d.ts.map +1 -1
  74. package/dist/astro-site.js +4 -0
  75. package/dist/astro-site.js.map +1 -1
  76. package/dist/auth/team.d.ts +1 -0
  77. package/dist/auth/team.d.ts.map +1 -1
  78. package/dist/auth/team.js +1 -1
  79. package/dist/auth/team.js.map +1 -1
  80. package/dist/cache/http.d.ts +20 -0
  81. package/dist/cache/http.d.ts.map +1 -1
  82. package/dist/cache/http.js +42 -0
  83. package/dist/cache/http.js.map +1 -1
  84. package/dist/chrome/announcement.d.ts +30 -0
  85. package/dist/chrome/announcement.d.ts.map +1 -0
  86. package/dist/chrome/announcement.js +190 -0
  87. package/dist/chrome/announcement.js.map +1 -0
  88. package/dist/chrome/codegen.d.ts +17 -0
  89. package/dist/chrome/codegen.d.ts.map +1 -0
  90. package/dist/chrome/codegen.js +96 -0
  91. package/dist/chrome/codegen.js.map +1 -0
  92. package/dist/chrome/contract.d.ts +161 -0
  93. package/dist/chrome/contract.d.ts.map +1 -0
  94. package/dist/chrome/contract.js +2 -0
  95. package/dist/chrome/contract.js.map +1 -0
  96. package/dist/chrome/memory-store.d.ts +15 -0
  97. package/dist/chrome/memory-store.d.ts.map +1 -0
  98. package/dist/chrome/memory-store.js +54 -0
  99. package/dist/chrome/memory-store.js.map +1 -0
  100. package/dist/chrome/overrides.d.ts +25 -0
  101. package/dist/chrome/overrides.d.ts.map +1 -0
  102. package/dist/chrome/overrides.js +180 -0
  103. package/dist/chrome/overrides.js.map +1 -0
  104. package/dist/chrome/parse.d.ts +36 -0
  105. package/dist/chrome/parse.d.ts.map +1 -0
  106. package/dist/chrome/parse.js +277 -0
  107. package/dist/chrome/parse.js.map +1 -0
  108. package/dist/chrome/prisma-store.d.ts +11 -0
  109. package/dist/chrome/prisma-store.d.ts.map +1 -0
  110. package/dist/chrome/prisma-store.js +48 -0
  111. package/dist/chrome/prisma-store.js.map +1 -0
  112. package/dist/chrome/public.d.ts +22 -0
  113. package/dist/chrome/public.d.ts.map +1 -0
  114. package/dist/chrome/public.js +181 -0
  115. package/dist/chrome/public.js.map +1 -0
  116. package/dist/chrome/resolve.d.ts +36 -0
  117. package/dist/chrome/resolve.d.ts.map +1 -0
  118. package/dist/chrome/resolve.js +165 -0
  119. package/dist/chrome/resolve.js.map +1 -0
  120. package/dist/chrome/revalidate.d.ts +9 -0
  121. package/dist/chrome/revalidate.d.ts.map +1 -0
  122. package/dist/chrome/revalidate.js +51 -0
  123. package/dist/chrome/revalidate.js.map +1 -0
  124. package/dist/chrome/routes.d.ts +14 -0
  125. package/dist/chrome/routes.d.ts.map +1 -0
  126. package/dist/chrome/routes.js +349 -0
  127. package/dist/chrome/routes.js.map +1 -0
  128. package/dist/chrome/schema.d.ts +231 -0
  129. package/dist/chrome/schema.d.ts.map +1 -0
  130. package/dist/chrome/schema.js +73 -0
  131. package/dist/chrome/schema.js.map +1 -0
  132. package/dist/chrome/service.d.ts +76 -0
  133. package/dist/chrome/service.d.ts.map +1 -0
  134. package/dist/chrome/service.js +286 -0
  135. package/dist/chrome/service.js.map +1 -0
  136. package/dist/chrome/settings.d.ts +14 -0
  137. package/dist/chrome/settings.d.ts.map +1 -0
  138. package/dist/chrome/settings.js +142 -0
  139. package/dist/chrome/settings.js.map +1 -0
  140. package/dist/chrome/store.d.ts +48 -0
  141. package/dist/chrome/store.d.ts.map +1 -0
  142. package/dist/chrome/store.js +26 -0
  143. package/dist/chrome/store.js.map +1 -0
  144. package/dist/chrome/values.d.ts +98 -0
  145. package/dist/chrome/values.d.ts.map +1 -0
  146. package/dist/chrome/values.js +491 -0
  147. package/dist/chrome/values.js.map +1 -0
  148. package/dist/config/define.d.ts +2 -0
  149. package/dist/config/define.d.ts.map +1 -1
  150. package/dist/config/define.js +2 -0
  151. package/dist/config/define.js.map +1 -1
  152. package/dist/db/model-types.d.ts +62 -0
  153. package/dist/db/model-types.d.ts.map +1 -1
  154. package/dist/forms/attribution-settings.d.ts +23 -0
  155. package/dist/forms/attribution-settings.d.ts.map +1 -0
  156. package/dist/forms/attribution-settings.js +42 -0
  157. package/dist/forms/attribution-settings.js.map +1 -0
  158. package/dist/forms/attribution.d.ts +5 -1
  159. package/dist/forms/attribution.d.ts.map +1 -1
  160. package/dist/forms/attribution.js +6 -4
  161. package/dist/forms/attribution.js.map +1 -1
  162. package/dist/forms/client-validation.d.ts.map +1 -1
  163. package/dist/forms/client-validation.js +3 -0
  164. package/dist/forms/client-validation.js.map +1 -1
  165. package/dist/forms/form-model.d.ts +109 -0
  166. package/dist/forms/form-model.d.ts.map +1 -0
  167. package/dist/forms/form-model.js +192 -0
  168. package/dist/forms/form-model.js.map +1 -0
  169. package/dist/forms/index.d.ts +2 -2
  170. package/dist/forms/index.d.ts.map +1 -1
  171. package/dist/forms/index.js +1 -1
  172. package/dist/forms/index.js.map +1 -1
  173. package/dist/forms/service.d.ts +19 -1
  174. package/dist/forms/service.d.ts.map +1 -1
  175. package/dist/forms/service.js +152 -0
  176. package/dist/forms/service.js.map +1 -1
  177. package/dist/forms/spam.d.ts +0 -12
  178. package/dist/forms/spam.d.ts.map +1 -1
  179. package/dist/forms/spam.js +30 -0
  180. package/dist/forms/spam.js.map +1 -1
  181. package/dist/forms/submission.d.ts +7 -0
  182. package/dist/forms/submission.d.ts.map +1 -1
  183. package/dist/forms/submission.js +33 -0
  184. package/dist/forms/submission.js.map +1 -1
  185. package/dist/forms/types.d.ts +26 -1
  186. package/dist/forms/types.d.ts.map +1 -1
  187. package/dist/forms/types.js +9 -1
  188. package/dist/forms/types.js.map +1 -1
  189. package/dist/generated/browser.d.ts +35 -3
  190. package/dist/generated/client.d.ts +35 -3
  191. package/dist/generated/internal/class.d.ts +44 -0
  192. package/dist/generated/internal/class.js +4 -4
  193. package/dist/generated/internal/prismaNamespace.d.ts +403 -1
  194. package/dist/generated/internal/prismaNamespace.js +95 -1
  195. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +102 -0
  196. package/dist/generated/internal/prismaNamespaceBrowser.js +95 -1
  197. package/dist/generated/models/ChromeRegion.d.ts +1203 -0
  198. package/dist/generated/models/ChromeRegion.js +1 -0
  199. package/dist/generated/models/Job.d.ts +732 -5
  200. package/dist/generated/models/JobEvent.d.ts +1339 -0
  201. package/dist/generated/models/JobEvent.js +1 -0
  202. package/dist/generated/models/JobReviewItem.d.ts +1470 -0
  203. package/dist/generated/models/JobReviewItem.js +1 -0
  204. package/dist/generated/models/SystemSetting.d.ts +952 -0
  205. package/dist/generated/models/SystemSetting.js +1 -0
  206. package/dist/generated/models.d.ts +4 -0
  207. package/dist/index.d.ts +12 -5
  208. package/dist/index.d.ts.map +1 -1
  209. package/dist/index.js +8 -3
  210. package/dist/index.js.map +1 -1
  211. package/dist/jobs/batch-ai.d.ts +53 -0
  212. package/dist/jobs/batch-ai.d.ts.map +1 -0
  213. package/dist/jobs/batch-ai.js +314 -0
  214. package/dist/jobs/batch-ai.js.map +1 -0
  215. package/dist/jobs/contract.d.ts +193 -0
  216. package/dist/jobs/contract.d.ts.map +1 -0
  217. package/dist/jobs/contract.js +2 -0
  218. package/dist/jobs/contract.js.map +1 -0
  219. package/dist/jobs/error-text.d.ts +23 -0
  220. package/dist/jobs/error-text.d.ts.map +1 -0
  221. package/dist/jobs/error-text.js +110 -0
  222. package/dist/jobs/error-text.js.map +1 -0
  223. package/dist/jobs/handlers.d.ts.map +1 -1
  224. package/dist/jobs/handlers.js +48 -7
  225. package/dist/jobs/handlers.js.map +1 -1
  226. package/dist/jobs/memory-store.d.ts +26 -2
  227. package/dist/jobs/memory-store.d.ts.map +1 -1
  228. package/dist/jobs/memory-store.js +157 -3
  229. package/dist/jobs/memory-store.js.map +1 -1
  230. package/dist/jobs/outcomes.d.ts +46 -0
  231. package/dist/jobs/outcomes.d.ts.map +1 -0
  232. package/dist/jobs/outcomes.js +251 -0
  233. package/dist/jobs/outcomes.js.map +1 -0
  234. package/dist/jobs/prisma-store.d.ts +14 -2
  235. package/dist/jobs/prisma-store.d.ts.map +1 -1
  236. package/dist/jobs/prisma-store.js +257 -15
  237. package/dist/jobs/prisma-store.js.map +1 -1
  238. package/dist/jobs/queue.d.ts +62 -2
  239. package/dist/jobs/queue.d.ts.map +1 -1
  240. package/dist/jobs/queue.js +412 -36
  241. package/dist/jobs/queue.js.map +1 -1
  242. package/dist/jobs/retention.d.ts +11 -0
  243. package/dist/jobs/retention.d.ts.map +1 -0
  244. package/dist/jobs/retention.js +18 -0
  245. package/dist/jobs/retention.js.map +1 -0
  246. package/dist/jobs/review.d.ts +111 -0
  247. package/dist/jobs/review.d.ts.map +1 -0
  248. package/dist/jobs/review.js +461 -0
  249. package/dist/jobs/review.js.map +1 -0
  250. package/dist/jobs/routes.d.ts.map +1 -1
  251. package/dist/jobs/routes.js +491 -16
  252. package/dist/jobs/routes.js.map +1 -1
  253. package/dist/jobs/serialize.d.ts +40 -0
  254. package/dist/jobs/serialize.d.ts.map +1 -0
  255. package/dist/jobs/serialize.js +275 -0
  256. package/dist/jobs/serialize.js.map +1 -0
  257. package/dist/jobs/types.d.ts +141 -2
  258. package/dist/jobs/types.d.ts.map +1 -1
  259. package/dist/jobs/worker-status.d.ts +27 -0
  260. package/dist/jobs/worker-status.d.ts.map +1 -0
  261. package/dist/jobs/worker-status.js +66 -0
  262. package/dist/jobs/worker-status.js.map +1 -0
  263. package/dist/media/optimize.d.ts.map +1 -1
  264. package/dist/media/optimize.js +4 -2
  265. package/dist/media/optimize.js.map +1 -1
  266. package/dist/next-site.d.ts +1 -0
  267. package/dist/next-site.d.ts.map +1 -1
  268. package/dist/next-site.js +3 -0
  269. package/dist/next-site.js.map +1 -1
  270. package/dist/seo/audit-runner.d.ts.map +1 -1
  271. package/dist/seo/audit-runner.js +23 -2
  272. package/dist/seo/audit-runner.js.map +1 -1
  273. package/dist/seo/issue-fix-link-insert.d.ts +11 -0
  274. package/dist/seo/issue-fix-link-insert.d.ts.map +1 -1
  275. package/dist/seo/issue-fix-link-insert.js +122 -8
  276. package/dist/seo/issue-fix-link-insert.js.map +1 -1
  277. package/dist/server-site.d.ts.map +1 -1
  278. package/dist/server-site.js +15 -0
  279. package/dist/server-site.js.map +1 -1
  280. package/dist/site.d.ts +8 -0
  281. package/dist/site.d.ts.map +1 -1
  282. package/dist/site.js +6 -0
  283. package/dist/site.js.map +1 -1
  284. package/dist/webhooks/index.d.ts +3 -1
  285. package/dist/webhooks/index.d.ts.map +1 -1
  286. package/dist/webhooks/index.js.map +1 -1
  287. package/package.json +6 -1
  288. package/prisma/migrations/0025_job_cancel_review/down.sql +60 -0
  289. package/prisma/migrations/0025_job_cancel_review/migration.sql +56 -0
  290. package/prisma/migrations/0026_job_creator_system_settings/down.sql +30 -0
  291. package/prisma/migrations/0026_job_creator_system_settings/migration.sql +14 -0
  292. package/prisma/migrations/0027_chrome_regions/down.sql +24 -0
  293. package/prisma/migrations/0027_chrome_regions/migration.sql +16 -0
  294. package/prisma/schema.prisma +105 -15
@@ -0,0 +1,275 @@
1
+ import { initialsFor } from '../auth/team.js';
2
+ import { errorSummary } from './error-text.js';
3
+ const NO_REFS = { users: new Map(), review: new Map() };
4
+ const SCANS = {
5
+ 'seo-scan': {
6
+ title: 'SEO scan',
7
+ scope: 'All indexable pages',
8
+ schedule: 'weekly-seo',
9
+ },
10
+ 'content-health': {
11
+ title: 'Content health',
12
+ scope: 'All published documents',
13
+ schedule: 'nightly-health',
14
+ },
15
+ };
16
+ /** Operation names as `/ai/batch` stores them, plus the kit's names unchanged. */
17
+ const BATCH_OPERATIONS = {
18
+ meta: 'meta',
19
+ 'alt-text': 'alt',
20
+ summarize: 'excerpt',
21
+ alt: 'alt',
22
+ titles: 'titles',
23
+ excerpt: 'excerpt',
24
+ links: 'links',
25
+ custom: 'custom',
26
+ };
27
+ const OPERATION_TITLES = {
28
+ meta: 'Rewrite meta descriptions',
29
+ alt: 'Generate alt text',
30
+ titles: 'Shorten long titles',
31
+ excerpt: 'Write excerpts',
32
+ links: 'Add internal links',
33
+ custom: 'Custom instruction',
34
+ };
35
+ function isScan(type) {
36
+ return type in SCANS;
37
+ }
38
+ function str(value) {
39
+ return typeof value === 'string' && value.length > 0 ? value : null;
40
+ }
41
+ /** The rows `/ai/batch` stores, one per document. */
42
+ function batchItems(job) {
43
+ const items = job.payload.items;
44
+ if (!Array.isArray(items))
45
+ return [];
46
+ return items.filter((item) => !!item && typeof item === 'object');
47
+ }
48
+ /**
49
+ * The kit's payload. Rows queued by `/ai/batch` keep their operation and
50
+ * collection per item and apply directly, so they read as "Apply automatically".
51
+ * They store no review mode, which `origin` keeps visible: unlike a stored
52
+ * `auto`, they aren't admin-only to retry ({@link batchAiWriter}).
53
+ */
54
+ function batchPayload(job) {
55
+ const payload = job.payload;
56
+ const items = batchItems(job);
57
+ const operation = str(payload.operation) ?? str(items[0]?.operation) ?? '';
58
+ const documentIds = Array.isArray(payload.documentIds)
59
+ ? payload.documentIds.filter((id) => typeof id === 'string')
60
+ : items.map((item) => str(item.documentId)).filter((id) => id !== null);
61
+ return {
62
+ operation: BATCH_OPERATIONS[operation] ?? 'custom',
63
+ instruction: str(payload.instruction),
64
+ source: str(payload.source) ?? str(items[0]?.collection) ?? '',
65
+ statusFilter: payload.statusFilter === 'published' ? 'published' : 'all',
66
+ documentIds,
67
+ review: payload.review === 'review' ? 'review' : 'auto',
68
+ onApprove: payload.onApprove === 'publish' ? 'publish' : 'draft',
69
+ ...(isTextField(payload.field) ? { field: payload.field } : {}),
70
+ ...(payload.review !== 'review' && payload.review !== 'auto'
71
+ ? { origin: 'ai-batch' }
72
+ : {}),
73
+ };
74
+ }
75
+ function isTextField(value) {
76
+ return value === 'metaDescription' || value === 'seoTitle' || value === 'excerpt';
77
+ }
78
+ function scanPayload(job) {
79
+ return { trigger: job.payload.trigger === 'manual' || job.createdById ? 'manual' : 'cron' };
80
+ }
81
+ /** Who queued it. Older batch AI rows have no `createdById`, so their first item says. */
82
+ function creatorId(job) {
83
+ if (job.createdById)
84
+ return job.createdById;
85
+ if (job.type === 'batch-ai')
86
+ return str(batchItems(job)[0]?.userId);
87
+ return null;
88
+ }
89
+ /**
90
+ * Who a batch AI job writes as, and where, for permission checks. Each item
91
+ * stores the user and role it was queued with, and the worker writes with them.
92
+ * `autoApply` is true only for a stored `review: 'auto'`. Rows from `/ai/batch`
93
+ * have no review mode: any editor could queue them.
94
+ */
95
+ export function batchAiWriter(job) {
96
+ const items = batchItems(job);
97
+ const roles = items.map((item) => str(item.role)).filter((role) => role !== null);
98
+ const collections = [str(job.payload.source), ...items.map((item) => str(item.collection))];
99
+ return {
100
+ creatorId: creatorId(job),
101
+ roles: [...new Set(roles)],
102
+ collections: [...new Set(collections.filter((slug) => slug !== null))],
103
+ autoApply: job.payload.review === 'auto',
104
+ };
105
+ }
106
+ function userRef(id, users) {
107
+ return users.get(id) ?? { id, name: 'Unknown user', initials: '?' };
108
+ }
109
+ function createdBy(job, refs) {
110
+ const id = creatorId(job);
111
+ if (id)
112
+ return userRef(id, refs.users);
113
+ if (isScan(job.type))
114
+ return { kind: 'cron', schedule: SCANS[job.type].schedule };
115
+ return null;
116
+ }
117
+ function title(job) {
118
+ if (job.title)
119
+ return job.title;
120
+ if (isScan(job.type))
121
+ return SCANS[job.type].title;
122
+ if (job.type === 'batch-ai')
123
+ return OPERATION_TITLES[batchPayload(job).operation];
124
+ return job.type;
125
+ }
126
+ function scope(job) {
127
+ if (job.scope)
128
+ return job.scope;
129
+ if (isScan(job.type))
130
+ return SCANS[job.type].scope;
131
+ if (job.type === 'batch-ai') {
132
+ const count = batchPayload(job).documentIds.length;
133
+ return `${count} ${count === 1 ? 'document' : 'documents'}`;
134
+ }
135
+ return '';
136
+ }
137
+ /** seo-scan saves `skip`, content-health `cursor`, batch AI `index`. */
138
+ function cursor(payload) {
139
+ if (typeof payload.cursor === 'string')
140
+ return payload.cursor;
141
+ if (typeof payload.skip === 'number' && payload.skip > 0)
142
+ return String(payload.skip);
143
+ if (typeof payload.index === 'number' && payload.index > 0)
144
+ return String(payload.index);
145
+ return null;
146
+ }
147
+ function payloadView(job) {
148
+ if (job.type === 'batch-ai')
149
+ return batchPayload(job);
150
+ if (isScan(job.type))
151
+ return scanPayload(job);
152
+ return {};
153
+ }
154
+ /**
155
+ * The stored row in the contract shape (DATA_CONTRACTS §1). `lastError` is one
156
+ * summary line unless `fullError` is set, because the full text has the stack,
157
+ * with server paths and query text. `detail` adds scan results.
158
+ */
159
+ export function serializeJob(job, refs = NO_REFS, options = {}) {
160
+ const { completed, total, message } = job.progress;
161
+ const serialized = {
162
+ id: job.id,
163
+ type: job.type,
164
+ status: job.status,
165
+ title: title(job),
166
+ scope: scope(job),
167
+ runAt: job.runAt.toISOString(),
168
+ attempts: job.attempts,
169
+ maxAttempts: job.maxAttempts,
170
+ progress: {
171
+ done: completed,
172
+ total,
173
+ unit: job.type === 'seo-scan' ? 'pages' : 'documents',
174
+ cursor: cursor(job.payload),
175
+ completed,
176
+ ...(message !== undefined ? { message } : {}),
177
+ },
178
+ lastError: options.fullError ? job.lastError : errorSummary(job.lastError),
179
+ // '' marks a note claimed but not (or never) written.
180
+ explain: job.explain || null,
181
+ summary: job.summary || null,
182
+ lockedAt: job.lockedAt?.toISOString() ?? null,
183
+ cancelRequestedAt: job.cancelRequestedAt?.toISOString() ?? null,
184
+ cancelledBy: job.cancelledById ? userRef(job.cancelledById, refs.users) : null,
185
+ createdBy: createdBy(job, refs),
186
+ createdAt: job.createdAt.toISOString(),
187
+ updatedAt: job.updatedAt.toISOString(),
188
+ finishedAt: job.finishedAt?.toISOString() ?? null,
189
+ usage: { tokensIn: job.tokensIn ?? 0, tokensOut: job.tokensOut ?? 0 },
190
+ payload: payloadView(job),
191
+ };
192
+ if (options.detail && isScan(job.type) && job.results) {
193
+ serialized.results = job.results;
194
+ }
195
+ if (job.type === 'batch-ai') {
196
+ serialized.review = refs.review.get(job.id) ?? { pending: 0, approved: 0, rejected: 0 };
197
+ }
198
+ return serialized;
199
+ }
200
+ export function serializeJobEvent(event) {
201
+ return {
202
+ at: event.at.toISOString(),
203
+ kind: event.kind,
204
+ message: event.message,
205
+ ...(event.detail ? { detail: event.detail } : {}),
206
+ ...(typeof event.durationMs === 'number' ? { durationMs: event.durationMs } : {}),
207
+ ...(typeof event.tokens === 'number' ? { tokens: event.tokens } : {}),
208
+ };
209
+ }
210
+ /** People by id, one query. An id with no user is left out; {@link userRef} fills it in. */
211
+ export async function loadUserRefs(db, ids) {
212
+ const unique = [...new Set(ids)];
213
+ const refs = new Map();
214
+ if (unique.length === 0)
215
+ return refs;
216
+ const users = await db.user.findMany({
217
+ where: { id: { in: unique } },
218
+ select: { id: true, name: true, email: true },
219
+ });
220
+ for (const user of users) {
221
+ const name = user.name || user.email;
222
+ refs.set(user.id, { id: user.id, name, initials: initialsFor(user.name, user.email) });
223
+ }
224
+ return refs;
225
+ }
226
+ /** One query for the people on these jobs and one for batch AI review counts. */
227
+ export async function loadJobRefs(db, jobs) {
228
+ const userIds = new Set();
229
+ for (const job of jobs) {
230
+ const creator = creatorId(job);
231
+ if (creator)
232
+ userIds.add(creator);
233
+ if (job.cancelledById)
234
+ userIds.add(job.cancelledById);
235
+ }
236
+ const batchIds = jobs.filter((job) => job.type === 'batch-ai').map((job) => job.id);
237
+ const [users, counts] = await Promise.all([
238
+ loadUserRefs(db, userIds),
239
+ batchIds.length > 0
240
+ ? db.jobReviewItem.groupBy({
241
+ by: ['jobId', 'status'],
242
+ where: { jobId: { in: batchIds } },
243
+ _count: { _all: true },
244
+ })
245
+ : [],
246
+ ]);
247
+ const review = new Map();
248
+ for (const row of counts) {
249
+ const tally = review.get(row.jobId) ?? { pending: 0, approved: 0, rejected: 0 };
250
+ if (row.status === 'pending' || row.status === 'approved' || row.status === 'rejected') {
251
+ tally[row.status] += row._count._all;
252
+ }
253
+ review.set(row.jobId, tally);
254
+ }
255
+ return { users, review };
256
+ }
257
+ /** A review item in the contract shape, with who decided it. */
258
+ export function serializeReviewItem(item, users) {
259
+ return {
260
+ id: item.id,
261
+ jobId: item.jobId,
262
+ documentId: item.documentId,
263
+ documentTitle: item.documentTitle,
264
+ url: item.url,
265
+ field: item.field,
266
+ before: item.before,
267
+ after: item.after,
268
+ editedAfter: item.editedAfter,
269
+ status: item.status,
270
+ decidedBy: item.decidedById ? userRef(item.decidedById, users) : null,
271
+ decidedAt: item.decidedAt?.toISOString() ?? null,
272
+ appliedRevisionId: item.appliedRevisionId,
273
+ };
274
+ }
275
+ //# sourceMappingURL=serialize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/jobs/serialize.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- export type JobStatus = 'queued' | 'running' | 'succeeded' | 'dead';
1
+ import type { ScanResults } from './contract.js';
2
+ export type JobStatus = 'queued' | 'running' | 'succeeded' | 'dead' | 'cancelled';
2
3
  export interface JobProgress {
3
4
  completed: number;
4
5
  total: number;
@@ -17,6 +18,30 @@ export interface JobRecord {
17
18
  progress: JobProgress;
18
19
  createdAt: Date;
19
20
  updatedAt: Date;
21
+ /** Set while a running job should stop after the current slice. */
22
+ cancelRequestedAt?: Date | null;
23
+ cancelledById?: string | null;
24
+ /** Who queued the job. Null for cron jobs. Written once, on insert. */
25
+ createdById?: string | null;
26
+ title?: string | null;
27
+ scope?: string | null;
28
+ tokensIn?: number;
29
+ tokensOut?: number;
30
+ /**
31
+ * The AI notes (worker rules 3 and 4): `explain` on a dead job, `summary` on
32
+ * a finished scan. Null until the worker claims the note, then `''` until
33
+ * its text lands. `''` stays when the AI had nothing to say.
34
+ */
35
+ explain?: string | null;
36
+ summary?: string | null;
37
+ /** Scan results in the contract's `ScanResults` shape, written when a scan finishes. */
38
+ results?: ScanResults | null;
39
+ finishedAt?: Date | null;
40
+ }
41
+ /** AI tokens one slice used. The queue adds them to the job's running totals. */
42
+ export interface JobUsage {
43
+ tokensIn: number;
44
+ tokensOut: number;
20
45
  }
21
46
  interface JobHandlerContext {
22
47
  now: Date;
@@ -25,12 +50,47 @@ interface JobHandlerContext {
25
50
  type JobHandlerResult = {
26
51
  done: true;
27
52
  progress?: JobProgress;
53
+ usage?: JobUsage;
54
+ /** Worker rule 3: a scan's results, saved with the success. The AI summary follows later. */
55
+ results?: ScanResults;
28
56
  } | {
29
57
  done: false;
30
58
  progress: JobProgress;
31
59
  payload?: Record<string, unknown>;
60
+ usage?: JobUsage;
61
+ /**
62
+ * Yield until `until` instead of running again at once, for example when
63
+ * an AI usage cap is spent. Attempts stay put: waiting is not a failure.
64
+ */
65
+ wait?: {
66
+ until: Date;
67
+ reason: string;
68
+ };
32
69
  };
33
70
  export type JobHandler = (job: JobRecord, ctx: JobHandlerContext) => Promise<JobHandlerResult>;
71
+ export type JobEventKind = 'queued' | 'claimed' | 'slice_ok' | 'slice_failed' | 'yielded' | 'retry_scheduled' | 'dead' | 'cancel_requested' | 'cancelled' | 'finished' | 'stale_lock';
72
+ export interface JobEventRecord {
73
+ jobId: string;
74
+ at: Date;
75
+ kind: JobEventKind;
76
+ message: string;
77
+ detail?: string | null;
78
+ durationMs?: number | null;
79
+ tokens?: number | null;
80
+ }
81
+ /** Which AI note: `explain` on a dead job, `summary` on a finished scan. */
82
+ export type JobNote = 'explain' | 'summary';
83
+ export interface JobListFilter {
84
+ type?: string;
85
+ statuses?: JobStatus[];
86
+ }
87
+ /** How much one retention run may delete. */
88
+ export interface PruneLimit {
89
+ /** Jobs, at most. */
90
+ jobs: number;
91
+ /** Events and review items deleted with them, at most, past the first job. */
92
+ rows: number;
93
+ }
34
94
  export interface JobStore {
35
95
  insert(job: JobRecord): Promise<void>;
36
96
  /** A queued or running job of this type, if one exists. */
@@ -39,14 +99,93 @@ export interface JobStore {
39
99
  claimNext(now: Date): Promise<JobRecord | null>;
40
100
  /** Running jobs locked before `cutoff` (the worker was killed mid-slice). */
41
101
  findStaleRunning(cutoff: Date): Promise<JobRecord[]>;
102
+ /**
103
+ * Writes the row except the cancel fields and `createdById`, so a slice saving
104
+ * a copy it read earlier can never clear a cancel. Only
105
+ * {@link JobStore.requestCancel} sets the cancel fields and only
106
+ * {@link JobStore.retry} clears them.
107
+ */
42
108
  save(job: JobRecord): Promise<void>;
43
- list(limit: number): Promise<JobRecord[]>;
109
+ /**
110
+ * Sets the cancel fields and `updatedAt` only, as one conditional write. It does
111
+ * nothing when a cancel was already requested or the job is no longer queued or
112
+ * running, so a cancel that loses a race with the job finishing leaves no trace.
113
+ * Returns whether this call set the flag.
114
+ */
115
+ requestCancel(id: string, at: Date, byId: string | null): Promise<boolean>;
116
+ /**
117
+ * Writes `job` (already marked cancelled) like {@link JobStore.save}, but only
118
+ * while the stored row is still queued or running and still waits on the same
119
+ * cancel: its `cancelRequestedAt` equals `job.cancelRequestedAt`. So a stale
120
+ * copy can't undo a retry that cleared the flag, end a job twice, or finish a
121
+ * newer cancel requested after a retry with fields from before it. Returns
122
+ * whether it applied.
123
+ */
124
+ finishCancel(job: JobRecord): Promise<boolean>;
125
+ /**
126
+ * Puts a dead job, or a queued job waiting out a failure, back in the queue
127
+ * now, as one conditional write. Resets attempts and clears `lastError`,
128
+ * `explain`, `finishedAt` and the cancel fields. Payload and progress are kept,
129
+ * so the handler continues from its saved cursor. Returns whether it applied.
130
+ */
131
+ retry(id: string, now: Date): Promise<boolean>;
132
+ /**
133
+ * Finished jobs whose AI note nobody has claimed: dead jobs with `explain`
134
+ * null, and succeeded jobs of `summaryTypes` with `summary` null, that
135
+ * finished at or after `since`. Oldest first, at most `limit`.
136
+ */
137
+ findPendingNotes(since: Date, summaryTypes: readonly string[], limit: number): Promise<JobRecord[]>;
138
+ /**
139
+ * Claims a finished job's AI note as one conditional write: sets it from null
140
+ * to `''` while the job is still the same finish (dead for `explain`,
141
+ * succeeded for `summary`, same `finishedAt`). Only the caller that gets
142
+ * true asks the AI, so each finish gets at most one call, even from two
143
+ * overlapping ticks. A retry clears `explain` back to null.
144
+ */
145
+ claimNote(id: string, note: JobNote, finishedAt: Date): Promise<boolean>;
146
+ /**
147
+ * Adds the note's tokens to the job, then writes its text while the note is
148
+ * still claimed (`''`) for the same finish, so an answer can't land on a job
149
+ * that was retried since. The tokens are added either way. Returns whether
150
+ * the text was written.
151
+ */
152
+ writeNote(id: string, note: JobNote, finishedAt: Date, text: string, usage?: JobUsage): Promise<boolean>;
153
+ /** Newest first. */
154
+ list(limit: number, filter?: JobListFilter): Promise<JobRecord[]>;
155
+ get(id: string): Promise<JobRecord | null>;
156
+ /**
157
+ * Queued jobs whose cancel flag was set. The worker finalises these at the
158
+ * start of each tick: a cancel can land just before a slice yields, and a job
159
+ * waiting out a usage cap may not be claimed again for weeks. Running jobs are
160
+ * left to the tick that holds their lock, which finalises the cancel at its
161
+ * slice boundary with that slice's work, or to the stale-lock path once the
162
+ * lock is older than 10 minutes.
163
+ */
164
+ findCancelRequested(): Promise<JobRecord[]>;
165
+ /**
166
+ * Retention: deletes finished jobs (succeeded, dead or cancelled) that
167
+ * finished before `cutoff`, oldest first, with their events and review items,
168
+ * within `limit`. A row with no `finishedAt` (finished before 0025) counts
169
+ * from `updatedAt`. Queued and running jobs are never deleted, and neither is
170
+ * a job with a change waiting for review or decided since `cutoff`, so a
171
+ * decision can be undone for the whole window. Returns how many it deleted.
172
+ */
173
+ pruneFinished(cutoff: Date, limit: PruneLimit): Promise<number>;
174
+ addEvent(event: JobEventRecord): Promise<void>;
175
+ /** Newest first. */
176
+ listEvents(jobId: string, limit: number): Promise<JobEventRecord[]>;
177
+ /**
178
+ * Deletes all but a job's newest `keep` events, in {@link JobStore.listEvents}
179
+ * order. Returns how many it deleted.
180
+ */
181
+ trimEvents(jobId: string, keep: number): Promise<number>;
44
182
  }
45
183
  export interface QueueTickResult {
46
184
  succeeded: number;
47
185
  continued: number;
48
186
  retried: number;
49
187
  dead: number;
188
+ cancelled: number;
50
189
  }
51
190
  export {};
52
191
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/jobs/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAA;AAEnE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,MAAM,EAAE,SAAS,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,IAAI,CAAA;IACX,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,WAAW,CAAA;IACrB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;CAChB;AAED,UAAU,iBAAiB;IACzB,GAAG,EAAE,IAAI,CAAA;IACT,QAAQ,EAAE,IAAI,CAAA;CACf;AAED,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,WAAW,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAE7E,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAA;AAE9F,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrC,2DAA2D;IAC3D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IACnD,kEAAkE;IAClE,SAAS,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAC/C,6EAA6E;IAC7E,gBAAgB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;IACpD,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/jobs/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { PrentaDB } from '../db/client-types.js';
2
+ import type { JobWorkerSchedule, JobWorkerStatus } from './contract.js';
3
+ import type { JobStore } from './types.js';
4
+ /** Time one worker tick may spend running slices. */
5
+ export declare const WORKER_BUDGET_MS = 20000;
6
+ interface ScanSchedule {
7
+ type: JobWorkerSchedule['type'];
8
+ cron: string;
9
+ /** UTC day of the week, 0 = Sunday. Null runs every day. */
10
+ weekday: number | null;
11
+ hour: number;
12
+ minute: number;
13
+ }
14
+ /** The first time after `now` that a daily or weekly UTC schedule fires. */
15
+ export declare function nextScheduledRun(schedule: Pick<ScanSchedule, 'weekday' | 'hour' | 'minute'>, now: Date): Date;
16
+ /**
17
+ * Stamp a finished worker tick: when it started and how long it ran. A settings
18
+ * row, so no job or user is needed.
19
+ */
20
+ export declare function recordWorkerTick(db: PrentaDB, tick: {
21
+ at: Date;
22
+ ms: number;
23
+ }): Promise<void>;
24
+ /** `GET /jobs/worker`: the worker strip on the Jobs screen. */
25
+ export declare function getWorkerStatus(db: PrentaDB, store: JobStore, now: Date): Promise<JobWorkerStatus>;
26
+ export {};
27
+ //# sourceMappingURL=worker-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-status.d.ts","sourceRoot":"","sources":["../../src/jobs/worker-status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAM1C,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC,UAAU,YAAY;IACpB,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,4DAA4D;IAC5D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAQD,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC,EAC3D,GAAG,EAAE,IAAI,GACR,IAAI,CAeN;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC7B,OAAO,CAAC,IAAI,CAAC,CAOf;AAWD,+DAA+D;AAC/D,wBAAsB,eAAe,CACnC,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,QAAQ,EACf,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,eAAe,CAAC,CAsB1B"}
@@ -0,0 +1,66 @@
1
+ /** `prenta_system_settings` key for the worker's last tick. */
2
+ const WORKER_TICK_KEY = 'jobs.worker';
3
+ /** The worker cron (`/cron/jobs`) runs once a minute. */
4
+ const TICK_INTERVAL_MS = 60_000;
5
+ /** Time one worker tick may spend running slices. */
6
+ export const WORKER_BUDGET_MS = 20_000;
7
+ /** The scan crons as create-prenta schedules them, in UTC. */
8
+ const SCAN_SCHEDULES = [
9
+ { type: 'seo-scan', cron: '0 4 * * 1', weekday: 1, hour: 4, minute: 0 },
10
+ { type: 'content-health', cron: '30 2 * * *', weekday: null, hour: 2, minute: 30 },
11
+ ];
12
+ /** The first time after `now` that a daily or weekly UTC schedule fires. */
13
+ export function nextScheduledRun(schedule, now) {
14
+ const next = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), schedule.hour, schedule.minute));
15
+ if (next.getTime() <= now.getTime())
16
+ next.setUTCDate(next.getUTCDate() + 1);
17
+ while (schedule.weekday !== null && next.getUTCDay() !== schedule.weekday) {
18
+ next.setUTCDate(next.getUTCDate() + 1);
19
+ }
20
+ return next;
21
+ }
22
+ /**
23
+ * Stamp a finished worker tick: when it started and how long it ran. A settings
24
+ * row, so no job or user is needed.
25
+ */
26
+ export async function recordWorkerTick(db, tick) {
27
+ const value = { lastTickAt: tick.at.toISOString(), lastTickMs: tick.ms };
28
+ await db.systemSetting.upsert({
29
+ where: { key: WORKER_TICK_KEY },
30
+ create: { key: WORKER_TICK_KEY, value },
31
+ update: { value },
32
+ });
33
+ }
34
+ async function readWorkerTick(db) {
35
+ const row = await db.systemSetting.findUnique({ where: { key: WORKER_TICK_KEY } });
36
+ const value = row?.value;
37
+ if (typeof value?.lastTickAt !== 'string')
38
+ return null;
39
+ const at = new Date(value.lastTickAt);
40
+ if (Number.isNaN(at.getTime()))
41
+ return null;
42
+ return { at, ms: typeof value.lastTickMs === 'number' ? value.lastTickMs : null };
43
+ }
44
+ /** `GET /jobs/worker`: the worker strip on the Jobs screen. */
45
+ export async function getWorkerStatus(db, store, now) {
46
+ const [tick, schedules] = await Promise.all([
47
+ readWorkerTick(db),
48
+ Promise.all(SCAN_SCHEDULES.map(async (schedule) => {
49
+ const active = await store.findActive(schedule.type);
50
+ return {
51
+ type: schedule.type,
52
+ cron: schedule.cron,
53
+ nextAt: nextScheduledRun(schedule, now).toISOString(),
54
+ activeJobId: active?.id ?? null,
55
+ };
56
+ })),
57
+ ]);
58
+ return {
59
+ lastTickAt: tick?.at.toISOString() ?? null,
60
+ lastTickMs: tick?.ms ?? null,
61
+ nextTickAt: tick ? new Date(tick.at.getTime() + TICK_INTERVAL_MS).toISOString() : null,
62
+ budgetMs: WORKER_BUDGET_MS,
63
+ schedules,
64
+ };
65
+ }
66
+ //# sourceMappingURL=worker-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-status.js","sourceRoot":"","sources":["../../src/jobs/worker-status.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/media/optimize.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,GAAG,UAAU,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED,oFAAoF;AACpF,eAAO,MAAM,mCAAmC,QAAQ,CAAA;AACxD,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,KAAK,CAAA;AACtC,uEAAuE;AACvE,eAAO,MAAM,0BAA0B,KAAK,CAAA;AA+D5C;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GACvC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW7B;AAcD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,EACxC,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAyF7B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU;;;;;;;UAmB9E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
1
+ {"version":3,"file":"optimize.d.ts","sourceRoot":"","sources":["../../src/media/optimize.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,GAAG,UAAU,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED,oFAAoF;AACpF,eAAO,MAAM,mCAAmC,QAAQ,CAAA;AACxD,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,KAAK,CAAA;AACtC,uEAAuE;AACvE,eAAO,MAAM,0BAA0B,KAAK,CAAA;AA+D5C;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GACvC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW7B;AAcD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,EACxC,gBAAgB,EAAE,MAAM,EACxB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CA2F7B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU;;;;;;;UAmB9E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD"}
@@ -134,10 +134,12 @@ export async function optimizeImage(input, originalFilename, mimeType, opts = {}
134
134
  })
135
135
  .toBuffer();
136
136
  const optimizedSize = optimizedBuffer.length;
137
- // If WebP is not smaller, keep the original bytes — no pointless re-encode.
137
+ // If lossy WebP is not smaller, keep the original bytes — no pointless
138
+ // re-encode. A requested lossless WebP is kept even when it grows: a JPEG
139
+ // screenshot would otherwise stay a JPEG, and the caller asked for WebP.
138
140
  // Still attach dimensions + blur so a later /optimize or backfill can heal
139
141
  // rows that were stored without metadata when sharp was missing.
140
- if (optimizedSize >= originalSize) {
142
+ if (!useLossless && optimizedSize >= originalSize) {
141
143
  return {
142
144
  ...unchangedResult(inputBuffer, originalSize, mimeType, originalFilename, metadata.width ?? 0, metadata.height ?? 0),
143
145
  blurHash: await tryBlurHash(sharp, inputBuffer),