FN-5914: fix mission activity counts and chronology
Keep mission activity counts accurate before event data loads and show events in chronological order. - add persisted mission event counts to core mission summaries and dashboard mission summary types - use the summary event count for the Activity tab preload badge instead of the currently loaded page total - render mission activity oldest-to-newest, keep load-more prepends stable, and scroll to the latest event on initial/live updates - extend mission store, dashboard, and MissionManager tests for event count and ordering coverage Files changed: packages/core/src/__tests__/mission-store.test.ts | 19 ++++ packages/core/src/mission-store.ts | 24 ++++- packages/dashboard/app/api/legacy.ts | 2 + .../dashboard/app/components/MissionManager.tsx | 19 ++-- .../components/__tests__/MissionManager.test.tsx | 107 ++++++++++++++++----- packages/dashboard/app/components/mission-types.ts | 1 + .../dashboard/src/__tests__/mission-e2e.test.ts | 7 +- 7 files changed, 143 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-5914 Fusion-Task-Lineage: b3119e94-66bd-437d-bcde-d5e4e43cb29e
This commit is contained in:
@@ -246,6 +246,7 @@ describe("MissionStore", () => {
|
||||
totalFeatures: 0,
|
||||
completedFeatures: 0,
|
||||
linkedGoalCount: 0,
|
||||
eventCount: 0,
|
||||
progressPercent: 0,
|
||||
});
|
||||
});
|
||||
@@ -324,6 +325,18 @@ describe("MissionStore", () => {
|
||||
expect(store.getMissionSummary(mission.id).linkedGoalCount).toBe(2);
|
||||
});
|
||||
|
||||
it("getMissionSummary reports unfiltered event counts", () => {
|
||||
const mission = store.createMission({ title: "Eventful mission" });
|
||||
|
||||
expect(store.getMissionSummary(mission.id).eventCount).toBe(0);
|
||||
|
||||
store.logMissionEvent(mission.id, "mission_started", "started");
|
||||
store.logMissionEvent(mission.id, "warning", "warning");
|
||||
store.logMissionEvent(mission.id, "error", "error");
|
||||
|
||||
expect(store.getMissionSummary(mission.id).eventCount).toBe(3);
|
||||
});
|
||||
|
||||
it("findNextPendingSlice skips completed slices in earlier milestones", () => {
|
||||
const mission = store.createMission({ title: "Next pending" });
|
||||
const m1 = store.addMilestone(mission.id, { title: "M1" });
|
||||
@@ -398,6 +411,7 @@ describe("MissionStore", () => {
|
||||
totalFeatures: 0,
|
||||
completedFeatures: 0,
|
||||
linkedGoalCount: 0,
|
||||
eventCount: 0,
|
||||
progressPercent: 0,
|
||||
});
|
||||
|
||||
@@ -409,6 +423,7 @@ describe("MissionStore", () => {
|
||||
totalFeatures: 0,
|
||||
completedFeatures: 0,
|
||||
linkedGoalCount: 0,
|
||||
eventCount: 0,
|
||||
progressPercent: 0,
|
||||
});
|
||||
|
||||
@@ -420,6 +435,7 @@ describe("MissionStore", () => {
|
||||
totalFeatures: 2,
|
||||
completedFeatures: 1,
|
||||
linkedGoalCount: 0,
|
||||
eventCount: 0,
|
||||
progressPercent: 50,
|
||||
});
|
||||
});
|
||||
@@ -437,6 +453,8 @@ describe("MissionStore", () => {
|
||||
createGoalInDb(db, "G-004", "Reliability");
|
||||
store.linkGoal(mission.id, "G-003");
|
||||
store.linkGoal(mission.id, "G-004");
|
||||
store.logMissionEvent(mission.id, "mission_started", "started");
|
||||
store.logMissionEvent(mission.id, "warning", "warning");
|
||||
|
||||
const singleSummary = store.getMissionSummary(mission.id);
|
||||
const batchedResult = store.listMissionsWithSummaries().find((m) => m.id === mission.id)!;
|
||||
@@ -446,6 +464,7 @@ describe("MissionStore", () => {
|
||||
expect(batchedResult.summary.totalFeatures).toBe(singleSummary.totalFeatures);
|
||||
expect(batchedResult.summary.completedFeatures).toBe(singleSummary.completedFeatures);
|
||||
expect(batchedResult.summary.linkedGoalCount).toBe(singleSummary.linkedGoalCount);
|
||||
expect(batchedResult.summary.eventCount).toBe(singleSummary.eventCount);
|
||||
expect(batchedResult.summary.progressPercent).toBe(singleSummary.progressPercent);
|
||||
});
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ export interface MissionSummary {
|
||||
completedFeatures: number;
|
||||
/** Number of goals linked to the mission */
|
||||
linkedGoalCount: number;
|
||||
/** Unfiltered total number of persisted mission lifecycle events */
|
||||
eventCount: number;
|
||||
/** Computed progress percentage (0–100), based on features or milestones */
|
||||
progressPercent: number;
|
||||
}
|
||||
@@ -772,6 +774,11 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
.get(missionId) as { count?: number | bigint } | undefined;
|
||||
const linkedGoalCount = Number(linkedGoalRow?.count ?? 0);
|
||||
|
||||
const eventCountRow = this.db
|
||||
.prepare("SELECT COUNT(*) AS count FROM mission_events WHERE missionId = ?")
|
||||
.get(missionId) as { count?: number | bigint } | undefined;
|
||||
const eventCount = Number(eventCountRow?.count ?? 0);
|
||||
|
||||
let progressPercent = 0;
|
||||
if (totalFeatures > 0) {
|
||||
progressPercent = Math.round((completedFeatures / totalFeatures) * 100);
|
||||
@@ -785,6 +792,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
totalFeatures,
|
||||
completedFeatures,
|
||||
linkedGoalCount,
|
||||
eventCount,
|
||||
progressPercent,
|
||||
};
|
||||
}
|
||||
@@ -829,7 +837,15 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
linkedGoalRows.map((row) => [row.missionId, Number(row.count ?? 0)]),
|
||||
);
|
||||
|
||||
// 6. Group in-memory: slices by milestoneId, features by sliceId
|
||||
// 6. Batch query mission event counts
|
||||
const eventCountRows = this.db.prepare(
|
||||
"SELECT missionId, COUNT(*) AS count FROM mission_events GROUP BY missionId"
|
||||
).all() as Array<{ missionId: string; count?: number | bigint }>;
|
||||
const eventCountByMissionId = new Map(
|
||||
eventCountRows.map((row) => [row.missionId, Number(row.count ?? 0)]),
|
||||
);
|
||||
|
||||
// 7. Group in-memory: slices by milestoneId, features by sliceId
|
||||
const slicesByMilestoneId = new Map<string, Slice[]>();
|
||||
for (const slice of allSlices) {
|
||||
const list = slicesByMilestoneId.get(slice.milestoneId) || [];
|
||||
@@ -844,7 +860,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
featuresBySliceId.set(feature.sliceId, list);
|
||||
}
|
||||
|
||||
// 7. Group milestones by missionId
|
||||
// 8. Group milestones by missionId
|
||||
const milestonesByMissionId = new Map<string, Milestone[]>();
|
||||
for (const milestone of allMilestones) {
|
||||
const list = milestonesByMissionId.get(milestone.missionId) || [];
|
||||
@@ -852,7 +868,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
milestonesByMissionId.set(milestone.missionId, list);
|
||||
}
|
||||
|
||||
// 8. Compute summary for each mission using grouped data
|
||||
// 9. Compute summary for each mission using grouped data
|
||||
return missions.map((mission) => {
|
||||
const milestones = milestonesByMissionId.get(mission.id) || [];
|
||||
const totalMilestones = milestones.length;
|
||||
@@ -871,6 +887,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
}
|
||||
|
||||
const linkedGoalCount = linkedGoalCountByMissionId.get(mission.id) ?? 0;
|
||||
const eventCount = eventCountByMissionId.get(mission.id) ?? 0;
|
||||
|
||||
let progressPercent = 0;
|
||||
if (totalFeatures > 0) {
|
||||
@@ -887,6 +904,7 @@ export class MissionStore extends EventEmitter<MissionStoreEvents> {
|
||||
totalFeatures,
|
||||
completedFeatures,
|
||||
linkedGoalCount,
|
||||
eventCount,
|
||||
progressPercent,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user