chore(team): checkpoint follow-up frontend edits

This commit is contained in:
777genius 2026-05-08 21:49:29 +03:00
parent 651cfa1846
commit 88b3ea2358
2 changed files with 47 additions and 9 deletions

View file

@ -66,19 +66,36 @@ function normalizeActorKey(value) {
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : ''; return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
} }
function closeTimestampForInterval(interval, timestamp) {
const startedAtMs = Date.parse(interval.startedAt);
const timestampMs = Date.parse(timestamp);
if (Number.isFinite(startedAtMs) && Number.isFinite(timestampMs) && timestampMs < startedAtMs) {
return interval.startedAt;
}
return timestamp;
}
function openReviewInterval(task, reviewer, timestamp = new Date().toISOString()) { function openReviewInterval(task, reviewer, timestamp = new Date().toISOString()) {
const reviewerName = typeof reviewer === 'string' && reviewer.trim() ? reviewer.trim() : ''; const reviewerName = typeof reviewer === 'string' && reviewer.trim() ? reviewer.trim() : '';
if (!reviewerName) return false; if (!reviewerName) return false;
const reviewerKey = normalizeActorKey(reviewerName); const reviewerKey = normalizeActorKey(reviewerName);
const intervals = Array.isArray(task.reviewIntervals) ? [...task.reviewIntervals] : []; const intervals = Array.isArray(task.reviewIntervals) ? [...task.reviewIntervals] : [];
const hasOpenForReviewer = intervals.some( let changed = false;
(interval) => !interval.completedAt && normalizeActorKey(interval.reviewer) === reviewerKey let hasOpenForReviewer = false;
); const nextIntervals = intervals.map((interval) => {
if (interval.completedAt) return interval;
if (normalizeActorKey(interval.reviewer) === reviewerKey) {
hasOpenForReviewer = true;
return interval;
}
changed = true;
return { ...interval, completedAt: closeTimestampForInterval(interval, timestamp) };
});
if (hasOpenForReviewer) { if (hasOpenForReviewer) {
task.reviewIntervals = intervals; task.reviewIntervals = nextIntervals;
return false; return changed;
} }
task.reviewIntervals = [...intervals, { reviewer: reviewerName, startedAt: timestamp }]; task.reviewIntervals = [...nextIntervals, { reviewer: reviewerName, startedAt: timestamp }];
return true; return true;
} }
@ -88,7 +105,7 @@ function closeReviewIntervals(task, timestamp = new Date().toISOString()) {
task.reviewIntervals = task.reviewIntervals.map((interval) => { task.reviewIntervals = task.reviewIntervals.map((interval) => {
if (interval.completedAt) return interval; if (interval.completedAt) return interval;
changed = true; changed = true;
return { ...interval, completedAt: timestamp }; return { ...interval, completedAt: closeTimestampForInterval(interval, timestamp) };
}); });
return changed; return changed;
} }
@ -315,7 +332,7 @@ function startReview(context, taskId, flags = {}) {
}); });
} else { } else {
tasks.updateTask(context, task.id, (t) => { tasks.updateTask(context, task.id, (t) => {
openReviewInterval(t, existingActor); openReviewInterval(t, existingActor, latestReviewEvent.timestamp);
return t; return t;
}); });
} }

View file

@ -1,7 +1,9 @@
import { getTasksBasePath } from '@main/utils/pathDecoder'; import { getTasksBasePath, getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { withFileLockSync } from './fileLock';
import { TeamTaskReader } from './TeamTaskReader'; import { TeamTaskReader } from './TeamTaskReader';
import type { import type {
@ -20,6 +22,7 @@ type MutableTeamTask = TeamTask & {
}; };
const CRASH_REPAIR_GRACE_MS = 5_000; const CRASH_REPAIR_GRACE_MS = 5_000;
const logger = createLogger('Service:TeamTaskActivityIntervalService');
function normalizeMemberName(value: string | null | undefined): string { function normalizeMemberName(value: string | null | undefined): string {
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : ''; return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
@ -142,6 +145,23 @@ export class TeamTaskActivityIntervalService {
private mutateTeamTasks( private mutateTeamTasks(
teamName: string, teamName: string,
mutate: (task: MutableTeamTask) => boolean mutate: (task: MutableTeamTask) => boolean
): ActivityIntervalResult {
const lockScope = path.join(getTeamsBasePath(), teamName, 'board-state');
try {
return withFileLockSync(lockScope, () => this.mutateTeamTasksUnlocked(teamName, mutate));
} catch (error) {
logger.warn(
`[${teamName}] Failed to update task activity intervals: ${
error instanceof Error ? error.message : String(error)
}`
);
return { changedTasks: 0 };
}
}
private mutateTeamTasksUnlocked(
teamName: string,
mutate: (task: MutableTeamTask) => boolean
): ActivityIntervalResult { ): ActivityIntervalResult {
const tasksDir = path.join(getTasksBasePath(), teamName); const tasksDir = path.join(getTasksBasePath(), teamName);
let entries: string[]; let entries: string[];
@ -216,6 +236,7 @@ export class TeamTaskActivityIntervalService {
const activeReviewer = getActiveReviewActor(task); const activeReviewer = getActiveReviewActor(task);
if ( if (
task.status === 'completed' &&
activeReviewer && activeReviewer &&
normalizeMemberName(activeReviewer) === memberKey && normalizeMemberName(activeReviewer) === memberKey &&
!hasOpenReviewInterval(task, activeReviewer) !hasOpenReviewInterval(task, activeReviewer)