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

@@ -342,6 +342,9 @@ type Mutation {
"""Relate tickets, e.g. FE DEPENDS_ON BE, QA TESTS FE"""
linkIssues(input: LinkIssuesInput!): CommandResult!
"""Mark these as read, or all of them when `ids` is omitted"""
markNotificationsRead(ids: [ID!]): Boolean!
"""
Ship the release. Fails while it has unfinished tickets unless force = true.
"""
@@ -357,6 +360,7 @@ type Mutation {
setIssueRelease(issueId: ID!, releaseId: ID): CommandResult!
unlinkIssues(input: LinkIssuesInput!): CommandResult!
updateIssue(input: UpdateIssueInput!): CommandResult!
updateNotificationSettings(email: Boolean!): NotificationSettings!
updateProject(input: UpdateProjectInput!): CommandResult!
"""
@@ -366,6 +370,48 @@ type Mutation {
updateRelease(input: UpdateReleaseInput!): CommandResult!
}
type MyNotifications {
items: [Notification!]!
"""All my unread ones, not just these"""
unread: Int!
}
type Notification {
actor: UserRef!
createdAt: String!
id: ID!
issueId: ID
issueKey: String
kind: NotificationKind!
projectId: ID!
projectKey: String!
"""Null: unread"""
readAt: String
summary: String!
}
"""Why someone is told"""
enum NotificationKind {
ADDED_TO_PROJECT
ASSIGNED
COMMENTED
MENTIONED
QA_FAILED
REMINDER
STATUS_CHANGED
WATCHED
}
type NotificationSettings {
"""Copies by e-mail"""
email: Boolean!
"""Whether this installation can send e-mail at all"""
emailAvailable: Boolean!
}
type Project {
"""
Statistics of the last `days` days (7-365): created vs resolved, cumulative flow, cycle/lead time, throughput and QA
@@ -498,8 +544,12 @@ type Query {
"""
myIssues(assignedToMe: Boolean! = true, categories: [StatusCategory!], limit: Int! = 50, offset: Int! = 0, statuses: [String!]): IssuePage!
"""My notifications, newest first, and how many are unread"""
myNotifications(limit: Int! = 30, unreadOnly: Boolean! = false): MyNotifications!
"""Projects where I am a member"""
myProjects: [Project!]!
notificationSettings: NotificationSettings!
"""A project of mine, or any public one (no sign-in needed)"""
project(
@@ -603,6 +653,9 @@ type Subscription {
"""Projects I am added to / removed from"""
myProjectsChanged: ProjectChange!
"""My notifications, as they happen"""
notificationAdded: Notification!
projectChanged(projectId: ID!): ProjectChange!
releaseChanged(projectId: ID!): ReleaseChange!
}