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

@@ -0,0 +1,18 @@
import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events';
import { Home } from '@task-stream/web/tracker/domain';
export const myHomePageEvents = eventGroup({
source: 'My Home Page',
events: {
refreshed: type<void>(),
},
});
export const myHomeApiEvents = eventGroup({
source: 'My Home API',
events: {
loaded: type<Home>(),
loadFailed: type<string>(),
},
});

View File

@@ -0,0 +1,23 @@
import { inject, Injectable } from '@angular/core';
import { injectDispatch } from '@ngrx/signals/events';
import { myHomePageEvents } from '../events/my-home.events';
import { MyHomeStore } from '../stores/my-home.store';
/**
* The home page beyond «my work»: agenda, activity, the week, sprints,
* project cards, suggestions, first steps and who is on what now.
*/
@Injectable({ providedIn: 'root' })
export class MyHomeFacade {
private readonly store = inject(MyHomeStore);
private readonly dispatch = injectDispatch(myHomePageEvents);
/** Null until the first load. */
readonly home = this.store.home;
readonly error = this.store.error;
/** Loads it (again, to refresh). */
refresh(): void {
this.dispatch.refreshed();
}
}

View File

@@ -0,0 +1,50 @@
import { inject } from '@angular/core';
import { mapResponse } from '@ngrx/operators';
import { signalStore, withState } from '@ngrx/signals';
import {
Events,
on,
withEventHandlers,
withReducer,
} from '@ngrx/signals/events';
import { Home, ProjectsGateway } from '@task-stream/web/tracker/domain';
import { exhaustMap } from 'rxjs';
import { myHomeApiEvents, myHomePageEvents } from '../events/my-home.events';
import { describeError } from '../notifications';
interface MyHomeState {
home: Home | null;
error: string | null;
}
/**
* The home page beyond «my work». Root-scoped, so coming back shows the
* last reading at once while it refreshes.
*/
export const MyHomeStore = signalStore(
{ providedIn: 'root' },
withState<MyHomeState>({ home: null, error: null }),
withReducer(
on(myHomeApiEvents.loaded, ({ payload }) => ({
home: payload,
error: null,
})),
on(myHomeApiEvents.loadFailed, ({ payload }) => ({ error: payload })),
),
withEventHandlers(
(_store, events = inject(Events), projects = inject(ProjectsGateway)) => ({
// A refresh while one is under way waits for it instead of piling up.
load$: events.on(myHomePageEvents.refreshed).pipe(
exhaustMap(() =>
projects.myHome().pipe(
mapResponse({
next: (home) => myHomeApiEvents.loaded(home),
error: (error) =>
myHomeApiEvents.loadFailed(describeError(error)),
}),
),
),
),
}),
),
);