Files
task-stream/libs/web/tracker/domain/src/lib/models/models.ts
Josep Piguillem 034704e46f feat: alerts open bugs, and repeats of the same problem go to that bug
Owners and admins turn on a secret URL per project (Settings →
Integrations), POST /api/webhooks/alerts/{project}/{token}, that Sentry,
Uptime Kuma, Prometheus Alertmanager, Grafana or any tool sending JSON
with a title can call. A new problem opens a bug as TaskStream (label
«alerta», chosen type and priority); while that bug is open, the same
fingerprint goes to it (a comment at most every 30 minutes) and
«resolved» is said on it; once it is closed, a new bug names the old
one. Alerts of a project are handled one at a time, so two at once open
one bug, and a bug not yet projected still counts as open. Unknown
project, alerts off or a bad token all answer 403.

TaskStream may now comment as the system actor, like it already opened
tickets for public proposals. Stored in Mongo (alert_integrations,
alert_traces), not in the event store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:36:55 +02:00

520 lines
13 KiB
TypeScript

import {
NotificationKind,
IssueLinkType,
IssuePriority,
ProjectRole,
ReleaseStatus,
SprintStatus,
StatusCategory,
} from './enums';
import { CustomFieldValue, ProjectSettings } from './workflow';
export interface UserRef {
id: string;
name: string;
}
export interface ProjectMember {
user: UserRef;
role: ProjectRole;
}
export interface Project {
id: string;
key: string;
name: string;
description: string | null;
/** Anyone can read it, also without signing in. */
isPublic: boolean;
/** Takes bug reports and ideas from anyone (while public). */
acceptsProposals: boolean;
owner: UserRef;
members: ProjectMember[];
settings: ProjectSettings;
/** Archived since then (read-only); null while active. */
archivedAt: string | null;
updatedAt: string;
version: number;
}
export interface IssueLink {
type: IssueLinkType;
/** OUTWARD: this issue → other. INWARD: other → this issue. */
direction: 'OUTWARD' | 'INWARD';
issueId: string;
issueKey: string;
}
/** The ticket another one hangs from. */
export interface IssueParent {
id: string;
key: string;
title: string;
}
/** A step inside a ticket that does not deserve a ticket of its own. */
export interface ChecklistItem {
id: string;
text: string;
done: boolean;
}
/** How far the tickets hanging from one have got. */
export interface ChildProgress {
total: number;
/** Done or cancelled. */
finished: number;
percent: number;
storyPoints: number;
finishedStoryPoints: number;
}
/** A top-level ticket that groups others. */
export interface Epic {
issue: IssueSummary;
progress: ChildProgress;
/** Its relations: «bloquea» / «depende de» are the roadmap's arrows. */
links: IssueLink[];
}
/** What a saved view narrows the board to; empty: everything. */
export interface SavedFilterCriteria {
types?: string[] | null;
areas?: string[] | null;
priorities?: IssuePriority[] | null;
assigneeId?: string | null;
unassigned?: boolean | null;
releaseId?: string | null;
labels?: string[] | null;
search?: string | null;
}
/** What a view of «my work» narrows to, across my projects. */
export interface MyViewCriteria {
/** Only these projects (empty: all of mine). */
projectIds?: string[] | null;
assignedToMe?: boolean | null;
watching?: boolean | null;
categories?: StatusCategory[] | null;
priorities?: IssuePriority[] | null;
/** Only types drawn with the bug icon. */
bugsOnly?: boolean | null;
/** Only open tickets past their due date. */
overdue?: boolean | null;
search?: string | null;
}
/** A named view of «my work», mine only. */
export interface MyView {
id: string;
name: string;
criteria: MyViewCriteria;
createdAt: string;
}
/** A named view of the board: private, or shared with the project. */
export interface SavedFilter {
id: string;
projectId: string;
owner: UserRef;
name: string;
shared: boolean;
criteria: SavedFilterCriteria;
createdAt: string;
}
/** What boards and lists show. */
export interface IssueSummary {
id: string;
projectId: string;
key: string;
/** Ids of the project's settings. */
type: string;
area: string | null;
/** A team id of the project's settings. */
team: string | null;
title: string;
status: string;
statusCategory: StatusCategory;
priority: IssuePriority;
assignee: UserRef | null;
release: { id: string; name: string } | null;
sprint: { id: string; name: string } | null;
labels: string[];
/** The epic or ticket this one breaks down. */
parent: IssueParent | null;
/** Open tickets it waits for: the ones that block it or it depends on. */
blockedBy: IssueParent[];
/** How many want it done, and whether I am one of them. */
votes: number;
voted: boolean;
/** Place in the backlog and in its board column (ascending, as text). */
rank: string;
/** Estimate; null when nobody estimated it. */
storyPoints: number | null;
/** Planned start and end (`YYYY-MM-DD`), for the roadmap. */
startDate: string | null;
dueDate: string | null;
/** Values of the project's custom fields (only those set). */
customFields: CustomFieldValue[];
/** In the trash since then (null: not in it). */
trashedAt: string | null;
commentCount: number;
updatedAt: string;
version: number;
}
/** A page of tickets: the ones returned and how many match in total. */
export interface IssuePage {
items: IssueSummary[];
total: number;
}
/** A file on a ticket (or under one of its comments). */
export interface Attachment {
id: string;
fileName: string;
mimeType: string;
/** Bytes. */
size: number;
uploadedBy: UserRef;
uploadedAt: string;
commentId: string | null;
}
export interface Issue extends IssueSummary {
description: string | null;
reporter: UserRef;
figmaUrl: string | null;
links: IssueLink[];
attachments: Attachment[];
/** People following the ticket. */
watchers: UserRef[];
/** Whether I keep it pinned (home page and search). */
pinned: boolean;
/** Its comments marked as decisions, oldest first. */
decisions: Comment[];
checklist: ChecklistItem[];
/** Commits, branches and pull requests that mention it. */
devLinks: DevLink[];
allowedTransitions: string[];
createdAt: string;
}
/** Someone else on a ticket's page right now. */
export interface Presence {
user: UserRef;
/** Editing the ticket or writing a comment. */
editing: boolean;
/** Last heartbeat. */
at: string;
}
/** A commit, branch or pull request that mentions a ticket. */
export interface DevLink {
kind: 'COMMIT' | 'BRANCH' | 'PULL_REQUEST';
url: string;
title: string;
/** Short SHA, branch or PR number. */
ref: string;
state: 'OPEN' | 'MERGED' | 'CLOSED' | null;
provider: string;
/** The project's name for the repository it came from. */
repository: string | null;
author: string | null;
at: string;
}
export interface WebhookDelivery {
at: string;
event: string;
/** HTTP status, null when it never answered. */
status: number | null;
ok: boolean;
error: string | null;
}
/** Where the project tells another system what happened. */
export interface Webhook {
id: string;
url: string;
/** Event names, `family.*` or `*`. */
events: string[];
active: boolean;
createdBy: UserRef;
createdAt: string;
lastDelivery: WebhookDelivery | null;
}
export type AutomationTriggerKind =
| 'ISSUE_CREATED'
| 'STATUS_CHANGED'
| 'ISSUE_ASSIGNED'
| 'COMMENT_ADDED'
| 'PULL_REQUEST_MERGED';
export type AutomationActionKind =
| 'SET_STATUS'
| 'ASSIGN'
| 'ASSIGN_REPORTER'
| 'ADD_LABEL'
| 'SET_PRIORITY'
| 'ADD_COMMENT'
| 'ADD_TO_ACTIVE_SPRINT';
/** «When this happens to a ticket, if it is like this, do that.» */
export interface Automation {
id: string;
name: string;
active: boolean;
trigger: { kind: AutomationTriggerKind; status?: string | null };
conditions: {
types?: string[] | null;
priorities?: IssuePriority[] | null;
areas?: string[] | null;
label?: string | null;
};
action: { kind: AutomationActionKind; value?: string | null };
createdBy: UserRef;
runs: number;
lastRunAt: string | null;
lastError: string | null;
}
export type AutomationInput = Pick<
Automation,
'name' | 'trigger' | 'conditions' | 'action'
>;
/** What an import did (or, as a preview, would do). */
export interface ImportReport {
format: 'taskstream' | 'jira';
dryRun: boolean;
rows: number;
created: number;
keys: string[];
skipped: { line: number; reason?: string }[];
warnings: { line: number; message?: string }[];
}
/** The project's webhook for GitHub, Gitea or GitLab. */
/** A repository whose webhooks the project accepts. */
export interface GitRepository {
id: string;
/** The project's name for it: api, web, admin… */
name: string;
/** Only for owners and admins. */
secret: string | null;
createdAt: string;
}
export interface GitIntegration {
/** On while there is at least one repository. */
enabled: boolean;
/** The same for every repository: the secret tells them apart. */
webhookPath: string;
repositories: GitRepository[];
}
/** Alerts → bugs: the secret URL monitoring tools call. */
export interface AlertIntegration {
enabled: boolean;
/** With its secret token; only for owners and admins. */
webhookPath: string | null;
/** Ticket type it opens; null: the project's first bug type. */
typeId: string | null;
priority: IssuePriority;
}
export interface Comment {
id: string;
issueId: string;
author: UserRef;
/** Empty on a deleted comment kept for its replies. */
body: string;
/** The top-level comment it answers; null for a top-level one. */
replyTo: string | null;
/** Deleted, but kept (without its text) while it has replies. */
deleted: boolean;
/** 👍… and who chose each (only the ones somebody chose). */
reactions: { emoji: string; users: UserRef[] }[];
/** Marked as a decision of the ticket: shown above it and searchable. */
decision: boolean;
/** Who marked it as a decision (null when it is not one). */
decidedBy: UserRef | null;
createdAt: string;
editedAt: string | null;
}
/** A decision found by its text, with its ticket. */
export interface Decision {
comment: Comment;
issue: { id: string; key: string; title: string };
}
export interface SprintProgress {
total: number;
/** Done or cancelled. */
finished: number;
percent: number;
storyPoints: number;
finishedStoryPoints: number;
}
/** A time-boxed iteration of a project. */
export interface Sprint {
id: string;
projectId: string;
name: string;
goal: string | null;
/** `YYYY-MM-DD`, both included. */
startDate: string;
endDate: string;
status: SprintStatus;
startedAt: string | null;
closedAt: string | null;
progress: SprintProgress;
}
/** Per day of a sprint; null for the days to come. */
export interface SprintBurndown {
sprintId: string;
dates: string[];
scope: (number | null)[];
remaining: (number | null)[];
remainingPoints: (number | null)[];
ideal: number[];
}
export interface Release {
id: string;
projectId: string;
name: string;
description: string | null;
targetDate: string | null;
status: ReleaseStatus;
releasedAt: string | null;
/** What shipped, told for people (Markdown). */
notes: string | null;
progress: { total: number; finished: number; percent: number };
}
export interface Activity {
id: string;
type: string;
/** Absent in activity read before it carried the project. */
projectId?: string;
issueId: string | null;
issueKey: string | null;
actor: UserRef;
summary: string;
occurredAt: string;
}
export interface CountBy<T = string> {
key: T;
count: number;
}
export interface ProjectStats {
total: number;
open: number;
byStatus: CountBy[];
byType: CountBy[];
byPriority: CountBy[];
byArea: CountBy[];
byAssignee: { user: UserRef | null; count: number }[];
}
export interface BoardColumn {
status: string;
issues: IssueSummary[];
total: number;
}
export interface DurationStats {
count: number;
averageDays: number | null;
/** 85% of tickets take at most this long. */
p85Days: number | null;
}
/** Statistics of a project over the last days. */
export interface ProjectAnalytics {
from: string;
to: string;
/** Per week (Monday, YYYY-MM-DD). */
createdVsResolved: { week: string; created: number; resolved: number }[];
/** Tickets in each status at the end of every day. */
cumulativeFlow: {
dates: string[];
series: { status: string; counts: number[] }[];
};
/** Start of work → done. */
cycleTime: DurationStats;
/** Creation → done. */
leadTime: DurationStats;
throughput: { week: string; user: UserRef | null; count: number }[];
qa: { passed: number; failed: number; failureRate: number | null };
}
/** Scope vs finished tickets of a release, day by day. */
export interface ReleaseBurnup {
releaseId: string;
targetDate: string | null;
dates: string[];
scope: number[];
done: number[];
}
export interface UserDirectoryEntry {
id: string;
name: string;
email: string;
}
/** Ack of a command: the read model catches up asynchronously. */
export interface CommandResult {
id: string;
key?: string | null;
version: number;
}
/** Something the signed-in person should know. */
export interface Notification {
id: string;
kind: NotificationKind;
projectId: string;
projectKey: string;
issueId: string | null;
issueKey: string | null;
actor: UserRef;
summary: string;
/** Null: unread. */
readAt: string | null;
createdAt: string;
}
/** My calendar subscription: a secret URL calendar apps read (iCalendar). */
export interface CalendarFeed {
/** Absolute: what goes into Google Calendar, Outlook or Apple Calendar. */
url: string;
createdAt: string;
}
export interface NotificationSettings {
/** Copies by e-mail. */
email: boolean;
/** Whether this installation can send e-mail at all. */
emailAvailable: boolean;
/** VAPID key to subscribe a device to push with; null: no push here. */
pushPublicKey: string | null;
}
/** A browser's Web Push subscription, as the API takes it. */
export interface PushDeviceSubscription {
endpoint: string;
p256dh: string;
auth: string;
}