notification_template.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. """Pydantic schemas for notification templates."""
  2. from datetime import datetime
  3. from pydantic import BaseModel, Field
  4. from backend.app.core.compat import StrEnum
  5. from backend.app.schemas.not_null import IgnoresNullForRequiredColumns
  6. class EventType(StrEnum):
  7. """Supported notification event types."""
  8. PRINT_START = "print_start"
  9. PRINT_COMPLETE = "print_complete"
  10. PRINT_FAILED = "print_failed"
  11. PRINT_STOPPED = "print_stopped"
  12. PRINT_PROGRESS = "print_progress"
  13. PRINT_MISSING_SPOOL_ASSIGNMENT = "print_missing_spool_assignment"
  14. BILLING_CHARGE_FAILED = "billing_charge_failed"
  15. PRINTER_OFFLINE = "printer_offline"
  16. PRINTER_ERROR = "printer_error"
  17. AI_FAILURE_DETECTION = "ai_failure_detection"
  18. FILAMENT_LOW = "filament_low"
  19. MAINTENANCE_DUE = "maintenance_due"
  20. AMS_HUMIDITY_HIGH = "ams_humidity_high"
  21. AMS_TEMPERATURE_HIGH = "ams_temperature_high"
  22. AMS_DRYING_SUSPENDED = "ams_drying_suspended"
  23. BED_COOLED = "bed_cooled"
  24. HA_SENSOR_ALERT = "ha_sensor_alert"
  25. TEST = "test"
  26. # Available variables for each event type
  27. EVENT_VARIABLES: dict[str, list[str]] = {
  28. # Post-print outcome request (#1898): good/reject are the one-tap links,
  29. # confirm_url opens the archive's confirmation dialog.
  30. "print_confirm_request": [
  31. "printer",
  32. "filename",
  33. "confirm_url",
  34. "good_url",
  35. "reject_url",
  36. "archive_id",
  37. "finish_photo_url",
  38. "timestamp",
  39. "app_name",
  40. ],
  41. "print_start": ["printer", "filename", "estimated_time", "eta", "timestamp", "app_name"],
  42. "print_complete": [
  43. "printer",
  44. "filename",
  45. "duration",
  46. "filament_grams",
  47. "filament_details",
  48. "archive_id",
  49. "run_id",
  50. "finish_photo_url",
  51. "timestamp",
  52. "app_name",
  53. ],
  54. "print_failed": [
  55. "printer",
  56. "filename",
  57. "duration",
  58. "filament_grams",
  59. "filament_details",
  60. "progress",
  61. "reason",
  62. "archive_id",
  63. "run_id",
  64. "finish_photo_url",
  65. "timestamp",
  66. "app_name",
  67. ],
  68. "print_stopped": [
  69. "printer",
  70. "filename",
  71. "duration",
  72. "filament_grams",
  73. "filament_details",
  74. "progress",
  75. "archive_id",
  76. "run_id",
  77. "finish_photo_url",
  78. "timestamp",
  79. "app_name",
  80. ],
  81. "print_progress": ["printer", "filename", "progress", "remaining_time", "eta", "timestamp", "app_name"],
  82. "print_missing_spool_assignment": [
  83. "printer",
  84. "missing_slots",
  85. "missing_slot_details",
  86. "timestamp",
  87. "app_name",
  88. ],
  89. "billing_charge_failed": ["printer", "filename", "archive_id", "error", "timestamp", "app_name"],
  90. "printer_offline": ["printer", "timestamp", "app_name"],
  91. "printer_error": ["printer", "error_type", "error_detail", "timestamp", "app_name"],
  92. "ai_failure_detection": [
  93. "printer",
  94. "task_name",
  95. "provider",
  96. "confidence",
  97. "print_quality",
  98. "action",
  99. "timestamp",
  100. "app_name",
  101. ],
  102. "plate_not_empty": ["printer", "difference_percent", "timestamp", "app_name"],
  103. "plate_clear_required": ["printer", "timestamp", "app_name"],
  104. "filament_low": ["printer", "slot", "remaining_percent", "color", "timestamp", "app_name"],
  105. "maintenance_due": ["printer", "items", "timestamp", "app_name"],
  106. "ams_humidity_high": ["printer", "ams_label", "humidity", "threshold", "timestamp", "app_name"],
  107. "ams_temperature_high": ["printer", "ams_label", "temperature", "threshold", "timestamp", "app_name"],
  108. "ams_drying_suspended": [
  109. "printer",
  110. "ams_label",
  111. "humidity",
  112. "threshold",
  113. "cycles",
  114. "timestamp",
  115. "app_name",
  116. ],
  117. "bed_cooled": ["printer", "bed_temp", "threshold", "filename", "timestamp", "app_name"],
  118. "ha_sensor_alert": ["printer", "sensor", "state", "timestamp", "app_name"],
  119. "location_ha_sensor_alert": ["location", "sensor", "state", "timestamp", "app_name"],
  120. "first_layer_complete": ["printer", "filename", "total_layers", "timestamp", "app_name"],
  121. "print_pause_upcoming": [
  122. "printer",
  123. "filename",
  124. "time_to_pause",
  125. "eta",
  126. "layer",
  127. "total_layers",
  128. "timestamp",
  129. "app_name",
  130. ],
  131. "test": ["app_name", "timestamp"],
  132. # Queue notifications
  133. "queue_job_added": ["job_name", "target", "timestamp", "app_name"],
  134. "queue_job_assigned": ["job_name", "printer", "target_model", "timestamp", "app_name"],
  135. "queue_job_started": ["printer", "job_name", "estimated_time", "eta", "timestamp", "app_name"],
  136. "queue_job_waiting": ["job_name", "target_model", "waiting_reason", "timestamp", "app_name"],
  137. "queue_job_skipped": ["printer", "job_name", "reason", "timestamp", "app_name"],
  138. "queue_job_failed": ["printer", "job_name", "reason", "timestamp", "app_name"],
  139. "queue_completed": ["completed_count", "timestamp", "app_name"],
  140. "stock_reorder_alert": [
  141. "material",
  142. "subtype",
  143. "brand",
  144. "color",
  145. "stock_g",
  146. "rate_g_day",
  147. "days_left",
  148. "timestamp",
  149. "app_name",
  150. ],
  151. "stock_break_alert": [
  152. "material",
  153. "subtype",
  154. "brand",
  155. "color",
  156. "stock_g",
  157. "rate_g_day",
  158. "days_left",
  159. "lead_time_days",
  160. "timestamp",
  161. "app_name",
  162. ],
  163. # User management notifications
  164. "user_created": ["username", "login_url", "app_name", "timestamp"],
  165. "password_reset": ["username", "app_name", "timestamp"],
  166. # User email print notifications
  167. "user_print_start": ["username", "printer", "filename", "timestamp", "app_name"],
  168. "user_print_complete": ["username", "printer", "filename", "finish_photo_url", "timestamp", "app_name"],
  169. "user_print_failed": ["username", "printer", "filename", "finish_photo_url", "timestamp", "app_name"],
  170. "user_print_stopped": ["username", "printer", "filename", "finish_photo_url", "timestamp", "app_name"],
  171. }
  172. # Events whose real notification can carry a camera snapshot — mirrors which
  173. # NotificationService.on_* methods accept image_data.
  174. PHOTO_CAPABLE_EVENTS: set[str] = {
  175. "print_confirm_request",
  176. "print_start",
  177. "print_complete",
  178. "print_failed",
  179. "print_stopped",
  180. "print_progress",
  181. "printer_error",
  182. "ai_failure_detection",
  183. "plate_not_empty",
  184. "first_layer_complete",
  185. "user_print_complete",
  186. "user_print_failed",
  187. "user_print_stopped",
  188. "test",
  189. }
  190. # Sample data for previewing templates
  191. SAMPLE_DATA: dict[str, dict[str, str]] = {
  192. "print_start": {
  193. "printer": "Bambu X1C",
  194. "filename": "Benchy.3mf",
  195. "estimated_time": "1h 23m",
  196. "eta": "15:53",
  197. "timestamp": "2024-01-15 14:30",
  198. "app_name": "Bambuddy",
  199. },
  200. "print_complete": {
  201. "printer": "Bambu X1C",
  202. "filename": "Benchy.3mf",
  203. "duration": "1h 18m",
  204. "filament_grams": "15.2",
  205. "filament_details": "AMS-A T1 PLA: 12.4g | AMS-A T3 PETG: 2.8g",
  206. "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_154800_abc12345.jpg",
  207. "archive_id": "123",
  208. "run_id": "456",
  209. "timestamp": "2024-01-15 15:48",
  210. "app_name": "Bambuddy",
  211. },
  212. "print_failed": {
  213. "printer": "Bambu X1C",
  214. "filename": "Benchy.3mf",
  215. "duration": "0h 45m",
  216. "filament_grams": "7.6",
  217. "filament_details": "AMS-A T1 PLA: 7.6g",
  218. "progress": "50",
  219. "reason": "Filament runout",
  220. "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_151500_def67890.jpg",
  221. "archive_id": "123",
  222. "run_id": "456",
  223. "timestamp": "2024-01-15 15:15",
  224. "app_name": "Bambuddy",
  225. },
  226. "print_stopped": {
  227. "printer": "Bambu X1C",
  228. "filename": "Benchy.3mf",
  229. "duration": "0h 30m",
  230. "filament_grams": "4.6",
  231. "filament_details": "AMS-A T2 PLA: 4.6g",
  232. "progress": "30",
  233. "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_150000_ghi11223.jpg",
  234. "archive_id": "123",
  235. "run_id": "456",
  236. "timestamp": "2024-01-15 15:00",
  237. "app_name": "Bambuddy",
  238. },
  239. "print_progress": {
  240. "printer": "Bambu X1C",
  241. "filename": "Benchy.3mf",
  242. "progress": "50",
  243. "remaining_time": "0h 41m",
  244. "eta": "15:41",
  245. "timestamp": "2024-01-15 15:00",
  246. "app_name": "Bambuddy",
  247. },
  248. "print_missing_spool_assignment": {
  249. "printer": "Bambu X1C",
  250. "missing_slots": "A1, A3",
  251. "missing_slot_details": "- A1: PLA Basic\n- A3: PETG HF",
  252. "timestamp": "2024-01-15 14:30",
  253. "app_name": "Bambuddy",
  254. },
  255. "billing_charge_failed": {
  256. "printer": "Bambu X1C",
  257. "filename": "Benchy.3mf",
  258. "archive_id": "123",
  259. "error": "The transaction could not be persisted",
  260. "timestamp": "2024-01-15 15:48",
  261. "app_name": "Bambuddy",
  262. },
  263. "printer_offline": {
  264. "printer": "Bambu X1C",
  265. "timestamp": "2024-01-15 14:30",
  266. "app_name": "Bambuddy",
  267. },
  268. "printer_error": {
  269. "printer": "Bambu X1C",
  270. "error_type": "AMS Error",
  271. "error_detail": "Filament slot 1 jammed",
  272. "timestamp": "2024-01-15 14:30",
  273. "app_name": "Bambuddy",
  274. },
  275. "ai_failure_detection": {
  276. "printer": "Bambu X1C",
  277. "task_name": "Benchy.3mf",
  278. "provider": "OctoEverywhere",
  279. "confidence": "N/A",
  280. "print_quality": "2/10",
  281. "action": "pause",
  282. "timestamp": "2024-01-15 14:30",
  283. "app_name": "Bambuddy",
  284. },
  285. "print_confirm_request": {
  286. "printer": "Bambu X1C",
  287. "filename": "Benchy",
  288. "confirm_url": "http://bambuddy.local:8000/archives?confirm=42",
  289. "good_url": "http://bambuddy.local:8000/api/v1/archives/confirm/sample-token/good",
  290. "reject_url": "http://bambuddy.local:8000/api/v1/archives/confirm/sample-token/reject",
  291. "archive_id": "42",
  292. "finish_photo_url": "/api/v1/archives/42/photos/finish_20240115_143500_abc12345.jpg",
  293. "timestamp": "2024-01-15 14:35",
  294. "app_name": "Bambuddy",
  295. },
  296. "plate_not_empty": {
  297. "printer": "Bambu X1C",
  298. "difference_percent": "12.4",
  299. "timestamp": "2024-01-15 09:00",
  300. "app_name": "Bambuddy",
  301. },
  302. "plate_clear_required": {
  303. "printer": "Bambu X1C",
  304. "timestamp": "2024-01-15 15:48",
  305. "app_name": "Bambuddy",
  306. },
  307. "filament_low": {
  308. "printer": "Bambu X1C",
  309. "slot": "1",
  310. "remaining_percent": "15",
  311. "color": "Black PLA",
  312. "timestamp": "2024-01-15 14:30",
  313. "app_name": "Bambuddy",
  314. },
  315. "maintenance_due": {
  316. "printer": "Bambu X1C",
  317. "items": "• Nozzle cleaning (OVERDUE)\n• Carbon rod lubrication (Soon)",
  318. "timestamp": "2024-01-15 14:30",
  319. "app_name": "Bambuddy",
  320. },
  321. "ams_humidity_high": {
  322. "printer": "Bambu X1C",
  323. "ams_label": "AMS-A",
  324. "humidity": "75",
  325. "threshold": "60",
  326. "timestamp": "2024-01-15 14:30",
  327. "app_name": "Bambuddy",
  328. },
  329. "ams_temperature_high": {
  330. "printer": "Bambu X1C",
  331. "ams_label": "AMS-A",
  332. "temperature": "42",
  333. "threshold": "35",
  334. "timestamp": "2024-01-15 14:30",
  335. "app_name": "Bambuddy",
  336. },
  337. "ams_drying_suspended": {
  338. "printer": "Bambu X1C",
  339. "ams_label": "AMS-A",
  340. "humidity": "16",
  341. "threshold": "14",
  342. "cycles": "2",
  343. "timestamp": "2024-01-15 14:30",
  344. "app_name": "Bambuddy",
  345. },
  346. "bed_cooled": {
  347. "printer": "Bambu X1C",
  348. "bed_temp": "34",
  349. "threshold": "35",
  350. "filename": "Benchy",
  351. "timestamp": "2024-01-15 14:30",
  352. "app_name": "Bambuddy",
  353. },
  354. "ha_sensor_alert": {
  355. "printer": "Bambu X1C",
  356. "sensor": "Enclosure Door",
  357. "state": "open",
  358. "timestamp": "2024-01-15 14:30",
  359. "app_name": "Bambuddy",
  360. },
  361. "location_ha_sensor_alert": {
  362. "location": "Drybox 1",
  363. "sensor": "Drybox 1 Humidity",
  364. "state": "68.00 %",
  365. "timestamp": "2024-01-15 14:30",
  366. "app_name": "Bambuddy",
  367. },
  368. "first_layer_complete": {
  369. "printer": "Bambu X1C",
  370. "filename": "Benchy.3mf",
  371. "total_layers": "184",
  372. "timestamp": "2024-01-15 14:35",
  373. "app_name": "Bambuddy",
  374. },
  375. "print_pause_upcoming": {
  376. "printer": "Bambu X1C",
  377. "filename": "Wifi_QR_NFC.3mf",
  378. "time_to_pause": "5m",
  379. "eta": "16:05",
  380. "layer": "42",
  381. "total_layers": "120",
  382. "timestamp": "2024-01-15 16:00",
  383. "app_name": "Bambuddy",
  384. },
  385. "test": {
  386. "app_name": "Bambuddy",
  387. "timestamp": "2024-01-15 14:30",
  388. },
  389. # Queue notifications
  390. "queue_job_added": {
  391. "job_name": "Benchy.3mf",
  392. "target": "Bambu X1C",
  393. "timestamp": "2024-01-15 14:30",
  394. "app_name": "Bambuddy",
  395. },
  396. "queue_job_assigned": {
  397. "job_name": "Benchy.3mf",
  398. "printer": "Bambu X1C #1",
  399. "target_model": "X1C",
  400. "timestamp": "2024-01-15 14:30",
  401. "app_name": "Bambuddy",
  402. },
  403. "queue_job_started": {
  404. "printer": "Bambu X1C",
  405. "job_name": "Benchy.3mf",
  406. "estimated_time": "1h 23m",
  407. "eta": "15:53",
  408. "timestamp": "2024-01-15 14:30",
  409. "app_name": "Bambuddy",
  410. },
  411. "queue_job_waiting": {
  412. "job_name": "Benchy.3mf",
  413. "target_model": "X1C",
  414. "waiting_reason": "Printer1 (needs PLA)",
  415. "timestamp": "2024-01-15 14:30",
  416. "app_name": "Bambuddy",
  417. },
  418. "queue_job_skipped": {
  419. "printer": "Bambu X1C",
  420. "job_name": "Benchy.3mf",
  421. "reason": "Previous print failed",
  422. "timestamp": "2024-01-15 14:30",
  423. "app_name": "Bambuddy",
  424. },
  425. "queue_job_failed": {
  426. "printer": "Bambu X1C",
  427. "job_name": "Benchy.3mf",
  428. "reason": "Upload failed: connection timeout",
  429. "timestamp": "2024-01-15 14:30",
  430. "app_name": "Bambuddy",
  431. },
  432. "queue_completed": {
  433. "completed_count": "5",
  434. "timestamp": "2024-01-15 18:30",
  435. "app_name": "Bambuddy",
  436. },
  437. "stock_reorder_alert": {
  438. "material": "PLA",
  439. "subtype": "Matte",
  440. "brand": "Bambu Lab",
  441. "color": "Charcoal",
  442. "stock_g": "420",
  443. "rate_g_day": "18.5",
  444. "days_left": "22",
  445. "timestamp": "2024-01-15 14:30",
  446. "app_name": "Bambuddy",
  447. },
  448. "stock_break_alert": {
  449. "material": "PLA",
  450. "subtype": "Matte",
  451. "brand": "Bambu Lab",
  452. "color": "Charcoal",
  453. "stock_g": "160",
  454. "rate_g_day": "18.5",
  455. "days_left": "8",
  456. "lead_time_days": "14",
  457. "timestamp": "2024-01-15 14:30",
  458. "app_name": "Bambuddy",
  459. },
  460. # User management notifications
  461. "user_created": {
  462. "username": "john_doe",
  463. "login_url": "https://bambuddy.example.com/login",
  464. "app_name": "Bambuddy",
  465. "timestamp": "2024-01-15 14:30",
  466. },
  467. "password_reset": {
  468. "username": "john_doe",
  469. "app_name": "Bambuddy",
  470. "timestamp": "2024-01-15 14:30",
  471. },
  472. # User email print notifications
  473. "user_print_start": {
  474. "username": "john_doe",
  475. "printer": "Bambu X1C",
  476. "filename": "Benchy.3mf",
  477. "timestamp": "2024-01-15 14:30",
  478. "app_name": "Bambuddy",
  479. },
  480. "user_print_complete": {
  481. "username": "john_doe",
  482. "printer": "Bambu X1C",
  483. "filename": "Benchy.3mf",
  484. "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_154800_abc12345.jpg",
  485. "timestamp": "2024-01-15 15:48",
  486. "app_name": "Bambuddy",
  487. },
  488. "user_print_failed": {
  489. "username": "john_doe",
  490. "printer": "Bambu X1C",
  491. "filename": "Benchy.3mf",
  492. "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_151500_def67890.jpg",
  493. "timestamp": "2024-01-15 15:15",
  494. "app_name": "Bambuddy",
  495. },
  496. "user_print_stopped": {
  497. "username": "john_doe",
  498. "printer": "Bambu X1C",
  499. "filename": "Benchy.3mf",
  500. "finish_photo_url": "/api/v1/archives/123/photos/finish_20240115_150000_ghi11223.jpg",
  501. "timestamp": "2024-01-15 15:15",
  502. "app_name": "Bambuddy",
  503. },
  504. }
  505. class NotificationTemplateBase(BaseModel):
  506. """Base schema for notification templates."""
  507. title_template: str = Field(..., min_length=1, max_length=200)
  508. body_template: str = Field(..., min_length=1, max_length=2000)
  509. class NotificationTemplateUpdate(IgnoresNullForRequiredColumns):
  510. """Schema for updating a notification template."""
  511. __orm_table__ = "notification_templates"
  512. title_template: str | None = Field(default=None, min_length=1, max_length=200)
  513. body_template: str | None = Field(default=None, min_length=1, max_length=2000)
  514. class NotificationTemplateResponse(NotificationTemplateBase):
  515. """Schema for notification template API responses."""
  516. id: int
  517. event_type: str
  518. name: str
  519. is_default: bool
  520. created_at: datetime
  521. updated_at: datetime
  522. class Config:
  523. from_attributes = True
  524. class EventVariablesResponse(BaseModel):
  525. """Response for available variables per event type."""
  526. event_type: str
  527. event_name: str
  528. variables: list[str]
  529. supports_photo: bool = False
  530. class TemplatePreviewRequest(BaseModel):
  531. """Request to preview a template with sample data."""
  532. event_type: str
  533. title_template: str
  534. body_template: str
  535. class TemplatePreviewResponse(BaseModel):
  536. """Response with rendered template preview."""
  537. title: str
  538. body: str