feat(admin): show email count and last-email date per feed

Surface each feed's email count on its Emails button and a "Last email …"
freshness line under the title, in both dashboard views. The values are
projected into feeds:list (kept to a single KV read) via the Feed aggregate,
so toListItemDTO now maps the whole aggregate through its intention-revealing
accessors instead of threading scalar projections. Also fixes long titles
overflowing into the Feed ID column in the table view.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Julien HerrandClaude Opus 4.7 committed 2026-05-25 23:18:38 +02:00
1 parent e258206384
commit fd3ff8c40a
11 files changed
+273 -51

No files matched your search

+18 -15
View File
@@ -1,6 +1,6 @@
import { FeedConfig, FeedListItem } from "../types";
import { FeedState } from "../domain/feed-state";
import { FeedId } from "../domain/value-objects/feed-id";
import { Feed } from "../domain/feed.aggregate";
/**
* The translation seam between the Feed aggregate's domain state (camelCase) and
@@ -44,20 +44,23 @@ export function toConfigDTO(state: FeedState): FeedConfig {
};
}
/** Domain state → the projection cached in the global `feeds:list` registry. */
export function toListItemDTO(
id: FeedId,
state: FeedState,
pendingConfirmation = false,
hasNativeFeed = false,
): FeedListItem {
/**
* The Feed aggregate → the projection cached in the global `feeds:list` registry.
* Unlike the config DTO, the list item is a read-model view: it folds in the
* aggregate's metadata-derived signals (pending confirmation, native feed,
* email count/last-received) alongside the config fields, so it reads the whole
* aggregate through its intention-revealing accessors.
*/
export function toListItemDTO(feed: Feed): FeedListItem {
return {
id: id.value,
title: state.title,
description: state.description,
mailbox_id: state.mailboxId,
expires_at: state.expiresAt,
pendingConfirmation,
hasNativeFeed,
id: feed.id.value,
title: feed.title,
description: feed.description,
mailbox_id: feed.mailboxId.value,
expires_at: feed.expiresAt,
pendingConfirmation: feed.pendingConfirmation,
hasNativeFeed: feed.hasNativeFeed(),
emailCount: feed.emailCount,
lastEmailAt: feed.lastEmailAt,
};
}