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:
1 parent
e258206384
commit
fd3ff8c40a
11 files changed
+273
-51
No files matched your search
@@ -200,6 +200,34 @@ describe("Feed.removeEmails", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feed.emailCount / lastEmailAt", () => {
|
||||
it("reports zero and undefined for an empty feed", () => {
|
||||
const feed = Feed.reconstitute(FID, state(), { emails: [] });
|
||||
expect(feed.emailCount).toBe(0);
|
||||
expect(feed.lastEmailAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("counts emails and reports the newest receivedAt (index head)", () => {
|
||||
const feed = Feed.reconstitute(FID, state(), {
|
||||
emails: [
|
||||
entry({ key: "k2", receivedAt: 2000 }),
|
||||
entry({ key: "k1", receivedAt: 1000 }),
|
||||
],
|
||||
});
|
||||
expect(feed.emailCount).toBe(2);
|
||||
expect(feed.lastEmailAt).toBe(2000);
|
||||
});
|
||||
|
||||
it("tracks the latest email after ingest", () => {
|
||||
const feed = Feed.reconstitute(FID, state(), {
|
||||
emails: [entry({ key: "old", receivedAt: 1000 })],
|
||||
});
|
||||
feed.ingest(entry({ key: "new", receivedAt: 5000 }), { maxBytes: 10_000 });
|
||||
expect(feed.emailCount).toBe(2);
|
||||
expect(feed.lastEmailAt).toBe(5000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feed events", () => {
|
||||
it("records FeedCreated on create and drains it once", () => {
|
||||
const feed = Feed.create(FID, createInput(), { mailboxId: MBOX });
|
||||
@@ -333,6 +361,27 @@ describe("FeedRepository.load / save round-trip", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("projects email count and last-email timestamp into feeds:list", async () => {
|
||||
const repo = new FeedRepository(mockEnv().EMAIL_STORAGE);
|
||||
const created = Feed.create(FID, createInput({ title: "Proj" }), {
|
||||
mailboxId: MBOX,
|
||||
});
|
||||
await repo.save(created);
|
||||
|
||||
let listed = await repo.listFeeds();
|
||||
expect(listed[0].emailCount).toBe(0);
|
||||
expect(listed[0].lastEmailAt).toBeUndefined();
|
||||
|
||||
created.ingest(entry({ key: "feed:opaque-feed-id:1", receivedAt: 4242 }), {
|
||||
maxBytes: 1_000_000,
|
||||
});
|
||||
await repo.saveMetadata(created);
|
||||
|
||||
listed = await repo.listFeeds();
|
||||
expect(listed[0].emailCount).toBe(1);
|
||||
expect(listed[0].lastEmailAt).toBe(4242);
|
||||
});
|
||||
|
||||
it("returns null when the feed has no config", async () => {
|
||||
const repo = new FeedRepository(mockEnv().EMAIL_STORAGE);
|
||||
expect(await repo.load(FeedId.unchecked("missing"))).toBeNull();
|
||||
|
||||
Reference in new issue
Block a user