feat: a home page with life

Greeting and summary of the day, the coming week, the latest across my
projects, who is online now, the week in numbers (mine and the team's,
with a streak), sprints under way, «Retomar», project cards with their
activity, what to pick up when nothing is mine and first steps for
newcomers. REST (GET /api/me/home), GraphQL (myHome) and MCP
(get_my_week).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 19:20:41 +02:00
parent affce615a7
commit 0946e9517d
57 changed files with 3728 additions and 53 deletions

View File

@@ -7,6 +7,7 @@ import {
ProjectAnalytics,
ProjectInsights,
PortfolioProject,
Home,
ProjectSettings,
ProjectsGateway,
ProjectStats,
@@ -24,6 +25,7 @@ import {
ProjectAnalyticsDocument,
ProjectInsightsDocument,
PortfolioDocument,
MyHomeDocument,
ProjectDocument,
ProjectStatsDocument,
PublicProjectsDocument,
@@ -68,6 +70,12 @@ export class GraphqlProjectsGateway extends ProjectsGateway {
.pipe(map((d) => toStats(d.project.stats)));
}
myHome(): Observable<Home> {
return this.gql
.query(MyHomeDocument, {})
.pipe(map((d) => d.myHome as Home));
}
portfolio(): Observable<PortfolioProject[]> {
return this.gql.query(PortfolioDocument, {}).pipe(
map((d) =>

View File

@@ -198,6 +198,7 @@ fragment ReleaseFields on Release {
fragment ActivityFields on Activity {
id
type
projectId
issueId
issueKey
summary

View File

@@ -706,6 +706,7 @@ export type ReleaseFieldsFragment = {
export type ActivityFieldsFragment = {
id: string;
type: string;
projectId: string;
issueId: string | null;
issueKey: string | null;
summary: string;
@@ -2142,6 +2143,73 @@ export type PortfolioQuery = {
}>;
};
export type MyHomeQueryVariables = Exact<{ [key: string]: never }>;
export type MyHomeQuery = {
myHome: {
agenda: Array<{
day: string;
kind: string;
projectKey: string;
title: string;
issueKey: string | null;
}>;
activity: Array<{
id: string;
type: string;
projectId: string;
issueId: string | null;
issueKey: string | null;
summary: string;
occurredAt: string;
actor: { id: string; name: string };
}>;
team: { perDay: Array<number>; thisWeek: number; lastWeek: number };
me: { thisWeek: number; lastWeek: number; streak: number };
sprints: Array<{
projectKey: string;
projectName: string;
sprint: {
sprintId: string;
name: string;
startDate: string;
endDate: string;
totalDays: number;
daysLeft: number;
tickets: number;
doneTickets: number;
points: number;
donePoints: number;
pacePerDay: number | null;
neededPerDay: number | null;
behind: boolean;
};
}>;
projects: Array<{
projectId: string;
open: number;
overdue: number;
activity: Array<number>;
sprint: { name: string; percent: number; behind: boolean } | null;
}>;
suggestions: Array<{ kind: string; projectKey: string; count: number }>;
firstSteps: {
project: boolean;
invited: boolean;
ticket: boolean;
calendar: boolean;
};
online: Array<{
projectKey: string;
issueKey: string | null;
issueTitle: string | null;
editing: boolean;
at: string;
user: { id: string; name: string };
}>;
};
};
export type ReleasesQueryVariables = Exact<{
idOrKey: string;
}>;
@@ -2198,6 +2266,7 @@ export type ActivityQuery = {
activity: Array<{
id: string;
type: string;
projectId: string;
issueId: string | null;
issueKey: string | null;
summary: string;
@@ -2540,6 +2609,7 @@ export type ActivityAddedSubscription = {
activityAdded: {
id: string;
type: string;
projectId: string;
issueId: string | null;
issueKey: string | null;
summary: string;
@@ -3600,6 +3670,7 @@ export const ActivityFieldsFragmentDoc = {
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'type' } },
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },
@@ -12690,6 +12761,353 @@ export const PortfolioDocument = {
},
],
} as unknown as DocumentNode<PortfolioQuery, PortfolioQueryVariables>;
export const MyHomeDocument = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: { kind: 'Name', value: 'MyHome' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'myHome' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'agenda' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'day' } },
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'projectKey' },
},
{ kind: 'Field', name: { kind: 'Name', value: 'title' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'issueKey' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'activity' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'ActivityFields' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'team' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'perDay' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'thisWeek' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'lastWeek' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'me' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'thisWeek' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'lastWeek' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'streak' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'sprints' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'projectKey' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'projectName' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'sprint' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'sprintId' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'name' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'startDate' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'endDate' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'totalDays' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'daysLeft' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'tickets' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'doneTickets' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'points' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'donePoints' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'pacePerDay' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'neededPerDay' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'behind' },
},
],
},
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'projects' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'projectId' },
},
{ kind: 'Field', name: { kind: 'Name', value: 'open' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'overdue' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'activity' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'sprint' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'name' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'percent' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'behind' },
},
],
},
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'suggestions' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'kind' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'projectKey' },
},
{ kind: 'Field', name: { kind: 'Name', value: 'count' } },
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'firstSteps' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'project' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'invited' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'ticket' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'calendar' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'online' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: { kind: 'Name', value: 'user' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'UserRefFields' },
},
],
},
},
{
kind: 'Field',
name: { kind: 'Name', value: 'projectKey' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'issueKey' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'issueTitle' },
},
{
kind: 'Field',
name: { kind: 'Name', value: 'editing' },
},
{ kind: 'Field', name: { kind: 'Name', value: 'at' } },
],
},
},
],
},
},
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'UserRefFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'UserRef' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'name' } },
],
},
},
{
kind: 'FragmentDefinition',
name: { kind: 'Name', value: 'ActivityFields' },
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: 'Activity' },
},
selectionSet: {
kind: 'SelectionSet',
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'type' } },
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },
{ kind: 'Field', name: { kind: 'Name', value: 'occurredAt' } },
{
kind: 'Field',
name: { kind: 'Name', value: 'actor' },
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'FragmentSpread',
name: { kind: 'Name', value: 'UserRefFields' },
},
],
},
},
],
},
},
],
} as unknown as DocumentNode<MyHomeQuery, MyHomeQueryVariables>;
export const ReleasesDocument = {
kind: 'Document',
definitions: [
@@ -13144,6 +13562,7 @@ export const ActivityDocument = {
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'type' } },
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },
@@ -14875,6 +15294,7 @@ export const ActivityAddedDocument = {
selections: [
{ kind: 'Field', name: { kind: 'Name', value: 'id' } },
{ kind: 'Field', name: { kind: 'Name', value: 'type' } },
{ kind: 'Field', name: { kind: 'Name', value: 'projectId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueId' } },
{ kind: 'Field', name: { kind: 'Name', value: 'issueKey' } },
{ kind: 'Field', name: { kind: 'Name', value: 'summary' } },

View File

@@ -351,3 +351,79 @@ query Portfolio {
}
}
}
query MyHome {
myHome {
agenda {
day
kind
projectKey
title
issueKey
}
activity {
...ActivityFields
}
team {
perDay
thisWeek
lastWeek
}
me {
thisWeek
lastWeek
streak
}
sprints {
projectKey
projectName
sprint {
sprintId
name
startDate
endDate
totalDays
daysLeft
tickets
doneTickets
points
donePoints
pacePerDay
neededPerDay
behind
}
}
projects {
projectId
open
overdue
activity
sprint {
name
percent
behind
}
}
suggestions {
kind
projectKey
count
}
firstSteps {
project
invited
ticket
calendar
}
online {
user {
...UserRefFields
}
projectKey
issueKey
issueTitle
editing
at
}
}
}