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:
@@ -614,6 +614,47 @@ export class MongoFindActivityRepository extends FindActivityRepository {
|
||||
.lean<ActivityDoc[]>();
|
||||
return docs.map(fromDoc);
|
||||
}
|
||||
|
||||
async findAcross(
|
||||
projectIds: string[],
|
||||
limit: number,
|
||||
): Promise<ActivityReadModel[]> {
|
||||
if (!projectIds.length) return [];
|
||||
const docs = await this.activity
|
||||
.find({ projectId: { $in: projectIds } })
|
||||
.sort({ occurredAt: -1 })
|
||||
.limit(limit)
|
||||
.lean<ActivityDoc[]>();
|
||||
return docs.map(fromDoc);
|
||||
}
|
||||
|
||||
async dailyCounts(
|
||||
projectIds: string[],
|
||||
fromDay: string,
|
||||
): Promise<{ projectId: string; day: string; count: number }[]> {
|
||||
if (!projectIds.length) return [];
|
||||
const rows = await this.activity.aggregate<{
|
||||
_id: { projectId: string; day: string };
|
||||
count: number;
|
||||
}>([
|
||||
{
|
||||
$match: {
|
||||
projectId: { $in: projectIds },
|
||||
occurredAt: { $gte: fromDay },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
projectId: '$projectId',
|
||||
day: { $substrCP: ['$occurredAt', 0, 10] },
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
return rows.map((r) => ({ ...r._id, count: r.count }));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -20,12 +20,35 @@ export class RedisPresenceStore extends PresenceStore {
|
||||
return `task-stream:presence:${issueId}`;
|
||||
}
|
||||
|
||||
async touch(
|
||||
issueId: string,
|
||||
private projectKey(projectId: string): string {
|
||||
return `task-stream:presence:project:${projectId}`;
|
||||
}
|
||||
|
||||
touchProject(
|
||||
projectId: string,
|
||||
entry: PresenceEntry,
|
||||
ttlMs: number,
|
||||
): Promise<void> {
|
||||
return this.write(this.projectKey(projectId), entry, ttlMs);
|
||||
}
|
||||
|
||||
listProject(projectId: string, since: number): Promise<PresenceEntry[]> {
|
||||
return this.read(this.projectKey(projectId), since);
|
||||
}
|
||||
|
||||
touch(issueId: string, entry: PresenceEntry, ttlMs: number): Promise<void> {
|
||||
return this.write(this.key(issueId), entry, ttlMs);
|
||||
}
|
||||
|
||||
list(issueId: string, since: number): Promise<PresenceEntry[]> {
|
||||
return this.read(this.key(issueId), since);
|
||||
}
|
||||
|
||||
private async write(
|
||||
key: string,
|
||||
entry: PresenceEntry,
|
||||
ttlMs: number,
|
||||
): Promise<void> {
|
||||
const key = this.key(issueId);
|
||||
await this.redis
|
||||
.multi()
|
||||
.hset(key, entry.user.id, JSON.stringify(entry))
|
||||
@@ -33,8 +56,7 @@ export class RedisPresenceStore extends PresenceStore {
|
||||
.exec();
|
||||
}
|
||||
|
||||
async list(issueId: string, since: number): Promise<PresenceEntry[]> {
|
||||
const key = this.key(issueId);
|
||||
private async read(key: string, since: number): Promise<PresenceEntry[]> {
|
||||
const all = await this.redis.hgetall(key);
|
||||
const fresh: PresenceEntry[] = [];
|
||||
const stale: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user