feat: notifications in the app and, optionally, by e-mail

Derived from events and kept per person: whoever a ticket is assigned to,
whoever reports or works on it when it moves, fails QA or gets a comment,
and whoever joins a project. Never about what you did yourself, once per
event. They arrive live through a subscription to a bell in the header,
and are copied by e-mail to whoever turns it on when SMTP_URL is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 09:19:59 +02:00
parent 0b65c1c912
commit efa06a0723
60 changed files with 3406 additions and 28 deletions

View File

@@ -4707,6 +4707,363 @@
],
"security": []
}
},
"/api/me/notifications": {
"get": {
"operationId": "NotificationsController_mine",
"parameters": [
{
"name": "unreadOnly",
"required": false,
"in": "query",
"description": "Only unread ones",
"schema": {
"default": false,
"allOf": [
{
"$ref": "#/components/schemas/Object"
}
]
}
},
{
"name": "limit",
"required": false,
"in": "query",
"schema": {
"minimum": 1,
"maximum": 100,
"default": 30,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MyNotificationsDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "My notifications, newest first",
"tags": [
"Users"
]
}
},
"/api/me/notifications/read": {
"post": {
"operationId": "NotificationsController_markRead",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MarkNotificationsReadDto"
}
}
}
},
"responses": {
"200": {
"description": "Done"
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Mark notifications as read (all of them without `ids`)",
"tags": [
"Users"
]
}
},
"/api/me/notification-settings": {
"get": {
"operationId": "NotificationsController_settings",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationSettingsResponseDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Whether I get copies by e-mail",
"tags": [
"Users"
]
},
"put": {
"operationId": "NotificationsController_updateSettings",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationSettingsDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationSettingsResponseDto"
}
}
}
},
"400": {
"description": "Invalid input",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"401": {
"description": "Missing or rejected Pireon token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"403": {
"description": "Not a member of the project (or not allowed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"409": {
"description": "Someone changed it at the same time: retry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
},
"422": {
"description": "Business rule (e.g. a transition the workflow forbids)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiErrorDto"
}
}
}
}
},
"security": [
{
"pireon-oauth": []
},
{
"pireon": []
}
],
"summary": "Turn e-mail copies on or off",
"tags": [
"Users"
]
}
}
},
"info": {
@@ -6714,6 +7071,145 @@
"description": "Publish even with unfinished tickets"
}
}
},
"Object": {
"type": "object",
"properties": {}
},
"NotificationKind": {
"type": "string",
"enum": [
"ASSIGNED",
"STATUS_CHANGED",
"QA_FAILED",
"COMMENTED",
"MENTIONED",
"WATCHED",
"ADDED_TO_PROJECT",
"REMINDER"
]
},
"NotificationDto": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"kind": {
"allOf": [
{
"$ref": "#/components/schemas/NotificationKind"
}
]
},
"projectId": {
"type": "string",
"format": "uuid"
},
"projectKey": {
"type": "string",
"example": "WEB"
},
"issueId": {
"type": "string",
"nullable": true,
"format": "uuid"
},
"issueKey": {
"type": "string",
"nullable": true,
"example": "WEB-12"
},
"actor": {
"$ref": "#/components/schemas/UserRefDto"
},
"summary": {
"type": "string",
"example": "Ana te asignó WEB-12: Login roto en Safari"
},
"readAt": {
"type": "string",
"nullable": true,
"format": "date-time",
"description": "Null: unread"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"kind",
"projectId",
"projectKey",
"issueId",
"issueKey",
"actor",
"summary",
"readAt",
"createdAt"
]
},
"MyNotificationsDto": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationDto"
}
},
"unread": {
"type": "number",
"description": "All my unread ones, not just these"
}
},
"required": [
"items",
"unread"
]
},
"MarkNotificationsReadDto": {
"type": "object",
"properties": {
"ids": {
"description": "Omit to mark all of them",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"NotificationSettingsResponseDto": {
"type": "object",
"properties": {
"email": {
"type": "boolean",
"description": "Copies by e-mail"
},
"emailAvailable": {
"type": "boolean",
"description": "Whether this installation can send e-mail"
}
},
"required": [
"email",
"emailAvailable"
]
},
"NotificationSettingsDto": {
"type": "object",
"properties": {
"email": {
"type": "boolean",
"description": "Copies by e-mail"
}
},
"required": [
"email"
]
}
}
}