Take the audit log and file checksums out of the chat surface
The thread panel rendered an "Audit" feed — "Chat message sent", "Task run completed", "Approval requested" rows with monospace timestamps — a server log sitting inside a conversation. Attachments showed an eight- character SHA-256 fragment next to the file type. Neither belongs in a surface a non-technical operator reads as people talking. Remove the audit replay from the chat thread (and its now-dead replayEventsForThread/auditPayload plumbing) and drop the checksum from the attachment card. The audit trail itself is unchanged — it is still recorded and still shown on the task activity tab. The e2e thread check now asserts the reply box, not the log.
This commit is contained in:
1 parent
e80c8487f3
commit
287c072c9f
5 files changed
+3
-77
No files matched your search
@@ -8,7 +8,6 @@ import {
|
|||||||
collectParticipants,
|
collectParticipants,
|
||||||
filterApprovalsForSpace,
|
filterApprovalsForSpace,
|
||||||
isRootMessage,
|
isRootMessage,
|
||||||
replayEventsForThread,
|
|
||||||
} from "./chat/chat-helpers";
|
} from "./chat/chat-helpers";
|
||||||
import { ROLE_KEY } from "../../../shared/domain";
|
import { ROLE_KEY } from "../../../shared/domain";
|
||||||
import type { PageProps } from "./types";
|
import type { PageProps } from "./types";
|
||||||
@@ -47,10 +46,6 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
|||||||
() => snapshot.approvals.filter((a) => a.orgId === orgId && a.state === "pending"),
|
() => snapshot.approvals.filter((a) => a.orgId === orgId && a.state === "pending"),
|
||||||
[snapshot.approvals, orgId],
|
[snapshot.approvals, orgId],
|
||||||
);
|
);
|
||||||
const auditEventsAll = useMemo(
|
|
||||||
() => snapshot.auditEvents.filter((event) => event.orgId === orgId),
|
|
||||||
[snapshot.auditEvents, orgId],
|
|
||||||
);
|
|
||||||
const channels = useMemo(() => spaces.filter((space) => space.spaceType !== "direct"), [spaces]);
|
const channels = useMemo(() => spaces.filter((space) => space.spaceType !== "direct"), [spaces]);
|
||||||
const directs = useMemo(() => spaces.filter((space) => space.spaceType === "direct"), [spaces]);
|
const directs = useMemo(() => spaces.filter((space) => space.spaceType === "direct"), [spaces]);
|
||||||
const canBoardIntervene = useMemo(
|
const canBoardIntervene = useMemo(
|
||||||
@@ -106,10 +101,6 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
|||||||
const threadMessages = selectedThread
|
const threadMessages = selectedThread
|
||||||
? messagesInSpace.filter((m) => m.threadId === selectedThread.id && m.id !== selectedThread.rootMessageId)
|
? messagesInSpace.filter((m) => m.threadId === selectedThread.id && m.id !== selectedThread.rootMessageId)
|
||||||
: [];
|
: [];
|
||||||
const threadAuditEvents = useMemo(
|
|
||||||
() => (selectedThread ? replayEventsForThread(auditEventsAll, selectedThread, messagesInSpace) : []),
|
|
||||||
[auditEventsAll, selectedThread, messagesInSpace],
|
|
||||||
);
|
|
||||||
const selectedBoardInterventions = useMemo(
|
const selectedBoardInterventions = useMemo(
|
||||||
() => (selectedThread ? boardInterventionsAll.filter((item) => item.threadId === selectedThread.id) : []),
|
() => (selectedThread ? boardInterventionsAll.filter((item) => item.threadId === selectedThread.id) : []),
|
||||||
[boardInterventionsAll, selectedThread],
|
[boardInterventionsAll, selectedThread],
|
||||||
@@ -309,7 +300,6 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
|||||||
slashInvocations={slashInvocationsAll}
|
slashInvocations={slashInvocationsAll}
|
||||||
boardInterventions={selectedBoardInterventions}
|
boardInterventions={selectedBoardInterventions}
|
||||||
files={files}
|
files={files}
|
||||||
auditEvents={threadAuditEvents}
|
|
||||||
context={{ employees, agents }}
|
context={{ employees, agents }}
|
||||||
canIntervene={canBoardIntervene}
|
canIntervene={canBoardIntervene}
|
||||||
attachmentFileId={threadAttachmentFileId}
|
attachmentFileId={threadAttachmentFileId}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{file.kind.toUpperCase()} · {file.sha256.slice(0, 8)}</div>
|
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{file.kind.toUpperCase()}</div>
|
||||||
</div>
|
</div>
|
||||||
{canOpen && <IconButton icon="arrowUpRight" label={`Open ${file.name}`} onClick={onOpen ?? undefined} />}
|
{canOpen && <IconButton icon="arrowUpRight" label={`Open ${file.name}`} onClick={onOpen ?? undefined} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { Button, Icon, IconButton, Pill } from "../../shared/ui";
|
import { Button, Icon, IconButton, Pill } from "../../shared/ui";
|
||||||
import type {
|
import type {
|
||||||
AuditEventRecord,
|
|
||||||
BoardInterventionRecord,
|
BoardInterventionRecord,
|
||||||
ChatAttachmentRecord,
|
ChatAttachmentRecord,
|
||||||
ChatMentionRecord,
|
ChatMentionRecord,
|
||||||
@@ -10,7 +9,7 @@ import type {
|
|||||||
ChatThreadRecord,
|
ChatThreadRecord,
|
||||||
FileObjectRecord,
|
FileObjectRecord,
|
||||||
} from "../../../../shared/domain";
|
} from "../../../../shared/domain";
|
||||||
import { auditEventLabel, formatTime } from "../types";
|
import { formatTime } from "../types";
|
||||||
import { ChatMessageRow } from "./ChatMessageRow";
|
import { ChatMessageRow } from "./ChatMessageRow";
|
||||||
import { MentionTextarea } from "./MentionTextarea";
|
import { MentionTextarea } from "./MentionTextarea";
|
||||||
import { senderName, type MessageContext } from "./chat-helpers";
|
import { senderName, type MessageContext } from "./chat-helpers";
|
||||||
@@ -24,7 +23,6 @@ export function ChatThreadPanel({
|
|||||||
slashInvocations,
|
slashInvocations,
|
||||||
boardInterventions,
|
boardInterventions,
|
||||||
files,
|
files,
|
||||||
auditEvents,
|
|
||||||
context,
|
context,
|
||||||
canIntervene,
|
canIntervene,
|
||||||
attachmentFileId,
|
attachmentFileId,
|
||||||
@@ -43,7 +41,6 @@ export function ChatThreadPanel({
|
|||||||
slashInvocations: ChatSlashInvocationRecord[];
|
slashInvocations: ChatSlashInvocationRecord[];
|
||||||
boardInterventions: BoardInterventionRecord[];
|
boardInterventions: BoardInterventionRecord[];
|
||||||
files: FileObjectRecord[];
|
files: FileObjectRecord[];
|
||||||
auditEvents: AuditEventRecord[];
|
|
||||||
context: MessageContext;
|
context: MessageContext;
|
||||||
canIntervene: boolean;
|
canIntervene: boolean;
|
||||||
attachmentFileId: string;
|
attachmentFileId: string;
|
||||||
@@ -118,7 +115,6 @@ export function ChatThreadPanel({
|
|||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{auditEvents.length > 0 && <AuditReplay events={auditEvents} />}
|
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={onSubmit} style={{ padding: 12, flexShrink: 0 }}>
|
<form onSubmit={onSubmit} style={{ padding: 12, flexShrink: 0 }}>
|
||||||
<div style={{ border: "1px solid var(--border-strong)", borderRadius: 10, background: "var(--panel)", padding: "8px 10px" }}>
|
<div style={{ border: "1px solid var(--border-strong)", borderRadius: 10, background: "var(--panel)", padding: "8px 10px" }}>
|
||||||
@@ -244,33 +240,3 @@ function boardStateLabel(state: string): string {
|
|||||||
function formatBoardAction(action: string): string {
|
function formatBoardAction(action: string): string {
|
||||||
return action.replace(/_/g, " ");
|
return action.replace(/_/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuditReplay({ events }: { events: AuditEventRecord[] }) {
|
|
||||||
return (
|
|
||||||
<section style={{ borderTop: "1px solid var(--border-soft)", marginTop: 18, paddingTop: 14 }}>
|
|
||||||
<div className="section-label" style={{ marginBottom: 10 }}>Audit</div>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
||||||
{events.map((event) => (
|
|
||||||
<div
|
|
||||||
key={event.id}
|
|
||||||
className="row"
|
|
||||||
style={{
|
|
||||||
alignItems: "baseline",
|
|
||||||
gap: 8,
|
|
||||||
border: "1px solid var(--border)",
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: "8px 10px",
|
|
||||||
background: "var(--panel)",
|
|
||||||
minWidth: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)", flexShrink: 0 }}>{formatTime(event.createdAt)}</span>
|
|
||||||
<span style={{ fontSize: 12, color: "var(--text-1)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
||||||
{auditEventLabel(event.entityType, event.action)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AgentRecord,
|
AgentRecord,
|
||||||
ApprovalRecord,
|
ApprovalRecord,
|
||||||
AuditEventRecord,
|
|
||||||
ChatAttachmentRecord,
|
ChatAttachmentRecord,
|
||||||
ChatMessageRecord,
|
ChatMessageRecord,
|
||||||
ChatSpaceRecord,
|
ChatSpaceRecord,
|
||||||
@@ -81,33 +80,6 @@ export function countMessagesIn(spaceId: string, messages: ChatMessageRecord[]):
|
|||||||
return messages.filter((message) => message.spaceId === spaceId).length;
|
return messages.filter((message) => message.spaceId === spaceId).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replayEventsForThread(
|
|
||||||
events: AuditEventRecord[],
|
|
||||||
thread: ChatThreadRecord,
|
|
||||||
messages: ChatMessageRecord[],
|
|
||||||
): AuditEventRecord[] {
|
|
||||||
const messageIds = new Set(
|
|
||||||
messages
|
|
||||||
.filter((message) => message.threadId === thread.id || message.id === thread.rootMessageId)
|
|
||||||
.map((message) => message.id),
|
|
||||||
);
|
|
||||||
return events
|
|
||||||
.filter((event) => {
|
|
||||||
if (event.entityType === "chat_message" && messageIds.has(event.entityId)) return true;
|
|
||||||
const payload = auditPayload(event);
|
|
||||||
return payload.threadId === thread.id || payload.sourceThreadId === thread.id;
|
|
||||||
})
|
|
||||||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
function auditPayload(event: AuditEventRecord): Record<string, unknown> {
|
|
||||||
const parsed = JSON.parse(event.payloadJson) as unknown;
|
|
||||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
||||||
throw new Error("Audit event payload is invalid.");
|
|
||||||
}
|
|
||||||
return parsed as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDay(iso: string): string {
|
export function formatDay(iso: string): string {
|
||||||
const date = new Date(iso);
|
const date = new Date(iso);
|
||||||
if (Number.isNaN(date.getTime())) return "—";
|
if (Number.isNaN(date.getTime())) return "—";
|
||||||
|
|||||||
@@ -102,9 +102,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
|
|||||||
await expect(page.getByText("@Board").first()).toBeVisible();
|
await expect(page.getByText("@Board").first()).toBeVisible();
|
||||||
await expect(page.getByText("/escalate").first()).toBeVisible();
|
await expect(page.getByText("/escalate").first()).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Open thread" }).first().click();
|
await page.getByRole("button", { name: "Open thread" }).first().click();
|
||||||
await expect(page.getByText("Audit")).toBeVisible();
|
await expect(page.getByRole("textbox", { name: "Thread reply" })).toBeVisible();
|
||||||
await expect(page.getByText("Chat message sent").first()).toBeVisible();
|
|
||||||
await expect(page.getByText("Approval requested").first()).toBeVisible();
|
|
||||||
await page.getByRole("button", { name: "Approvals" }).click();
|
await page.getByRole("button", { name: "Approvals" }).click();
|
||||||
await expect(page.getByText("Escalate release").first()).toBeVisible();
|
await expect(page.getByText("Escalate release").first()).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user