Skip to content
Merged
13 changes: 8 additions & 5 deletions apps/azure/src/components/ControlHandoffEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ const ControlHandoffEditor: React.FC<ControlHandoffEditorProps> = ({
existingHandoff?.operationalOwner.displayName ?? ''
);
const [handoffDate, setHandoffDate] = useState(
existingHandoff?.handoffDate ? existingHandoff.handoffDate.slice(0, 10) : todayString()
existingHandoff?.handoffDate
? new Date(existingHandoff.handoffDate).toISOString().slice(0, 10)
: todayString()
);
const [description, setDescription] = useState(existingHandoff?.description ?? '');
const [referenceUri, setReferenceUri] = useState(existingHandoff?.referenceUri ?? '');
Expand All @@ -59,7 +61,7 @@ const ControlHandoffEditor: React.FC<ControlHandoffEditorProps> = ({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

const now = new Date().toISOString();
const nowMs = Date.now();

const handoff: ControlHandoff = {
id: existingHandoff?.id ?? crypto.randomUUID(),
Expand All @@ -71,11 +73,12 @@ const ControlHandoffEditor: React.FC<ControlHandoffEditorProps> = ({
// We have no people picker yet, so userId is omitted (optional on ProcessParticipantRef);
// recordedBy below carries the submitter's identity.
operationalOwner: { displayName: operationalOwnerName },
handoffDate: new Date(handoffDate + 'T00:00:00.000Z').toISOString(),
handoffDate: new Date(handoffDate + 'T00:00:00.000Z').getTime(),
description,
referenceUri: referenceUri || undefined,
retainSustainmentReview,
recordedAt: existingHandoff?.recordedAt ?? now,
createdAt: existingHandoff?.createdAt ?? nowMs,
deletedAt: existingHandoff?.deletedAt ?? null,
recordedBy: existingHandoff?.recordedBy ?? {
userId: currentUser.userId,
displayName: recordedByDisplayName,
Expand All @@ -88,7 +91,7 @@ const ControlHandoffEditor: React.FC<ControlHandoffEditorProps> = ({
await storage.saveSustainmentRecord({
...relatedRecord,
controlHandoffId: handoff.id,
updatedAt: new Date().toISOString(),
updatedAt: nowMs,
});
}

Expand Down
5 changes: 3 additions & 2 deletions apps/azure/src/components/ImprovementWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,9 @@ const ImprovementWindow: React.FC = () => {
id: `tmp-${Date.now()}`,
text,
selected: false,
createdAt: new Date().toISOString(),
} as import('@variscout/core').ImprovementIdea,
createdAt: Date.now(),
deletedAt: null,
},
],
}
: h
Expand Down
27 changes: 20 additions & 7 deletions apps/azure/src/components/ProcessHubEvidencePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface ProcessHubEvidencePanelProps {
onEvidenceChanged?: () => void;
}

function nowMs(): number {
return Date.now();
}

// capturedAt is the data-time field (string ISO); reuse this helper where strings are needed.
function nowIso(): string {
return new Date().toISOString();
}
Expand Down Expand Up @@ -137,14 +142,15 @@ const ProcessHubEvidencePanel: React.FC<ProcessHubEvidencePanelProps> = ({
// ---------------------------------------------------------------------------

const handleCreateAgentReviewSource = async (): Promise<void> => {
const timestamp = nowIso();
const timestamp = nowMs();
const source: EvidenceSource = {
id: `agent-review-log-${Date.now()}`,
id: `agent-review-log-${timestamp}`,
hubId,
name: 'Agent review log',
cadence: 'weekly',
profileId: AGENT_REVIEW_LOG_PROFILE.id,
createdAt: timestamp,
deletedAt: null,
updatedAt: timestamp,
};
await saveEvidenceSource(source);
Expand Down Expand Up @@ -172,14 +178,17 @@ const ProcessHubEvidencePanel: React.FC<ProcessHubEvidencePanelProps> = ({
severity === 'green'
? `${selectedSource.id}:safe-green`
: `${selectedSource.id}:false-green`;
const snapshotNow = nowMs();
const snapshot: EvidenceSnapshot = {
id: `snapshot-${Date.now()}`,
id: `snapshot-${snapshotNow}`,
hubId,
sourceId: selectedSource.id,
capturedAt,
rowCount: rows.length,
origin: `evidence-source:${selectedSource.id}`,
importedAt: capturedAt,
importedAt: snapshotNow,
createdAt: snapshotNow,
deletedAt: null,
profileApplication: application,
latestSignals: [
{
Expand Down Expand Up @@ -304,28 +313,32 @@ const ProcessHubEvidencePanel: React.FC<ProcessHubEvidencePanelProps> = ({
cadence: chosenCadence,
});
try {
const timestamp = nowIso();
const sourceId = `evidence-source-${Date.now()}`;
const timestamp = nowMs();
const sourceId = `evidence-source-${timestamp}`;
const source: EvidenceSource = {
id: sourceId,
hubId,
name: sourceName,
cadence: chosenCadence,
profileId: profile.id,
createdAt: timestamp,
deletedAt: null,
updatedAt: timestamp,
};
await saveEvidenceSource(source);

const capturedAt = nowIso(); // capturedAt is data-time (string)
const application = profile.apply(rows, mapping);
const snapshot: EvidenceSnapshot = {
id: `snapshot-${Date.now()}`,
hubId,
sourceId,
capturedAt: timestamp,
capturedAt,
rowCount: rows.length,
origin: `evidence-source:${sourceId}`,
importedAt: timestamp,
createdAt: timestamp,
deletedAt: null,
profileApplication: application,
};
await saveEvidenceSnapshot(snapshot, rawText);
Expand Down
10 changes: 6 additions & 4 deletions apps/azure/src/components/ProcessHubFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const formatChangeSignals = (count: number): string =>
export const formatOverdueActions = (count: number): string =>
`${count} ${formatPlural(count, { one: 'overdue action', other: 'overdue actions' })}`;

export const formatLatestActivity = (value: string | null): string => {
if (!value) return 'No activity yet';
export const formatLatestActivity = (value: number | null): string => {
if (value === null || value === undefined) return 'No activity yet';
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return 'Activity date unknown';
return `Latest activity ${date.toLocaleDateString('en', {
Expand Down Expand Up @@ -168,9 +168,11 @@ export const sustainmentBandAnswer = (
);
if (!sustainmentEligible) return null;
const due = records.filter(
r => r.nextReviewDue && new Date(r.nextReviewDue) <= now && !r.tombstoneAt
r => r.nextReviewDue && new Date(r.nextReviewDue) <= now && r.deletedAt === null
).length;
const holdingCount = records.filter(
r => r.latestVerdict === 'holding' && r.deletedAt === null
).length;
const holdingCount = records.filter(r => r.latestVerdict === 'holding' && !r.tombstoneAt).length;

let base: string;
if (due === 0 && holdingCount > 0) {
Expand Down
4 changes: 2 additions & 2 deletions apps/azure/src/components/ProcessHubReviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ const ProcessHubReviewPanel: React.FC<ProcessHubReviewPanelProps> = ({
// change-signals, top-focus). For per-investigation items, the action
// uses item.investigationIds[0] instead.
const defaultInvestigationId = React.useMemo(() => {
const sorted = [...rollup.investigations].sort((a, b) =>
(b.modified ?? '').localeCompare(a.modified ?? '')
const sorted = [...rollup.investigations].sort(
(a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)
);
// Empty fallback when the rollup has no investigations: deriveResponsePathAction
// will then return unsupported actions for hub-aggregate items, which actionToHref
Expand Down
8 changes: 4 additions & 4 deletions apps/azure/src/components/SustainmentRecordEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const SustainmentRecordEditor: React.FC<SustainmentRecordEditorProps> = ({
if (isSubmitting) return;
setIsSubmitting(true);

const now = new Date().toISOString();
const nowMs = Date.now();
const record: SustainmentRecord = {
id: existingRecord?.id ?? crypto.randomUUID(),
investigationId,
Expand All @@ -90,9 +90,9 @@ const SustainmentRecordEditor: React.FC<SustainmentRecordEditorProps> = ({
latestReviewAt: existingRecord?.latestReviewAt,
latestReviewId: existingRecord?.latestReviewId,
controlHandoffId: existingRecord?.controlHandoffId,
tombstoneAt: existingRecord?.tombstoneAt,
createdAt: existingRecord?.createdAt ?? now,
updatedAt: now,
deletedAt: existingRecord?.deletedAt ?? null,
createdAt: existingRecord?.createdAt ?? nowMs,
updatedAt: nowMs,
};

try {
Expand Down
12 changes: 7 additions & 5 deletions apps/azure/src/components/SustainmentReviewLogger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@ const SustainmentReviewLogger: React.FC<SustainmentReviewLoggerProps> = ({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

const reviewedAt = new Date().toISOString();
const nowMs = Date.now();

const review: SustainmentReview = {
id: crypto.randomUUID(),
recordId,
investigationId,
hubId,
reviewedAt,
reviewedAt: nowMs,
createdAt: nowMs,
deletedAt: null,
reviewer: { userId: currentUser.userId, displayName: reviewerDisplayName },
verdict,
snapshotId: snapshotId || undefined,
Expand All @@ -65,13 +67,13 @@ const SustainmentReviewLogger: React.FC<SustainmentReviewLoggerProps> = ({
const records = await storage.listSustainmentRecords(hubId);
const parentRecord = records.find(r => r.id === recordId);
if (parentRecord) {
const nextDue = nextDueFromCadence(cadence, new Date(reviewedAt));
const nextDue = nextDueFromCadence(cadence, new Date(nowMs));
const updatedRecord = {
...parentRecord,
latestVerdict: verdict,
latestReviewAt: reviewedAt,
latestReviewAt: new Date(nowMs).toISOString(),
latestReviewId: review.id,
updatedAt: new Date().toISOString(),
updatedAt: nowMs,
};
if (nextDue !== undefined) {
updatedRecord.nextReviewDue = nextDue;
Expand Down
8 changes: 3 additions & 5 deletions apps/azure/src/components/WhatsNewSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,13 @@ const WhatsNewSection: React.FC<WhatsNewSectionProps> = ({ findings, questions,
}
}

// Question status changes
// NOTE: Question.createdAt and updatedAt are ISO strings — use Date.parse()
// Question status changes — updatedAt is now a Unix ms number
for (const h of questions) {
const updatedAtMs = Date.parse(h.updatedAt);
if (updatedAtMs > lastViewedAt) {
if (h.updatedAt > lastViewedAt) {
result.push({
type: 'question-status',
text: `\u2018${truncate(h.text)}\u2019 question \u2192 ${h.status}`,
timestamp: updatedAtMs,
timestamp: h.updatedAt,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions apps/azure/src/components/__tests__/EvidenceSheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const buildFinding = (id: string, status: Finding['status'], text = 'A finding')
id,
text,
createdAt: 1714000000000,
deletedAt: null,
investigationId: 'general-unassigned',
context: {} as Finding['context'],
status,
comments: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ describe('ProcessHubEvidencePanel', () => {
expect(saved.profileId).toBe('agent-review-log');
expect(saved.cadence).toBe('weekly');
expect(saved.name).toBe('Agent review log');
expect(typeof saved.createdAt).toBe('string');
expect(typeof saved.createdAt).toBe('number');
expect(saved.createdAt).toBe(saved.updatedAt);
expect(saved.deletedAt).toBeNull();
});

it('shows status confirmation after creating a source', async () => {
Expand All @@ -72,8 +73,9 @@ describe('ProcessHubEvidencePanel', () => {
name: 'Agent review log',
cadence: 'weekly' as const,
profileId: 'agent-review-log',
createdAt: '2026-04-26T00:00:00.000Z',
updatedAt: '2026-04-26T00:00:00.000Z',
createdAt: 1745625600000,
deletedAt: null,
updatedAt: 1745625600000,
};
mockListEvidenceSources.mockResolvedValue([source]);

Expand Down Expand Up @@ -105,8 +107,9 @@ describe('ProcessHubEvidencePanel', () => {
name: 'Agent review log',
cadence: 'weekly' as const,
profileId: 'agent-review-log',
createdAt: '2026-04-26T00:00:00.000Z',
updatedAt: '2026-04-26T00:00:00.000Z',
createdAt: 1745625600000,
deletedAt: null,
updatedAt: 1745625600000,
};
mockListEvidenceSources.mockResolvedValue([source]);

Expand All @@ -131,8 +134,9 @@ describe('ProcessHubEvidencePanel', () => {
name: 'Agent review log',
cadence: 'weekly' as const,
profileId: 'agent-review-log',
createdAt: '2026-04-26T00:00:00.000Z',
updatedAt: '2026-04-26T00:00:00.000Z',
createdAt: 1745625600000,
deletedAt: null,
updatedAt: 1745625600000,
};
mockListEvidenceSources.mockResolvedValue([source]);

Expand All @@ -159,8 +163,9 @@ describe('ProcessHubEvidencePanel', () => {
name: 'Agent review log',
cadence: 'weekly' as const,
profileId: 'agent-review-log',
createdAt: '2026-04-26T00:00:00.000Z',
updatedAt: '2026-04-26T00:00:00.000Z',
createdAt: 1745625600000,
deletedAt: null,
updatedAt: 1745625600000,
};
mockListEvidenceSources.mockResolvedValue([source]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('ProcessHubFormat sustainment helpers', () => {
records: Array<{
latestVerdict?: string;
nextReviewDue?: string;
tombstoneAt?: string;
deletedAt?: number | null;
}>,
evidenceSnapshots?: Array<{
capturedAt: string;
Expand All @@ -78,7 +78,7 @@ describe('ProcessHubFormat sustainment helpers', () => {
investigations: investigationStatuses.map(s => ({
metadata: s ? { investigationStatus: s } : undefined,
})),
sustainmentRecords: records,
sustainmentRecords: records.map(r => ({ deletedAt: null, ...r })),
evidenceSnapshots: evidenceSnapshots ?? [],
}) as unknown as ProcessHubRollup<ProcessHubInvestigation>;

Expand Down Expand Up @@ -134,14 +134,14 @@ describe('ProcessHubFormat sustainment helpers', () => {
expect(sustainmentBandAnswer(rollup, NOW)).toBe('2 sustainment reviews due now.');
});

it('ignores tombstoned records', () => {
it('ignores soft-deleted records (deletedAt !== null)', () => {
const rollup = makeRollup(
['resolved'],
[
{
latestVerdict: 'holding',
nextReviewDue: '2026-04-25T00:00:00.000Z',
tombstoneAt: '2026-04-24T00:00:00.000Z',
deletedAt: 1745020800000, // 2026-04-24T00:00:00.000Z
},
]
);
Expand Down
Loading