diff --git a/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx b/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx index 02c16c97..ca343ec6 100644 --- a/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SocialPage.tsx @@ -12,6 +12,7 @@ import { } from "../../../shared/domain"; import { APPROVAL_STATE } from "../../../shared/domain"; import { SocialCalendar } from "./social/SocialCalendar"; +import { SocialActivity } from "./social/SocialActivity"; import { PlatformIcon } from "./social/PlatformIcon"; import type { PageProps } from "./types"; import { activeOrgId, requireActiveEmployee } from "./types"; @@ -37,6 +38,9 @@ export function SocialPage({ snapshot, actions }: PageProps) { return states; }, [snapshot.approvals, orgId]); const employees = snapshot.employees.filter((item) => item.orgId === orgId); + const socialGrants = useMemo(() => snapshot.socialAccountGrants.filter((grant) => grant.orgId === orgId), [snapshot.socialAccountGrants, orgId]); + const socialAudit = useMemo(() => snapshot.socialAuditRecords.filter((record) => record.orgId === orgId), [snapshot.socialAuditRecords, orgId]); + const browserActions = useMemo(() => snapshot.browserActions.filter((action) => action.orgId === orgId), [snapshot.browserActions, orgId]); const [connectOpen, setConnectOpen] = useState(false); const [draftOpen, setDraftOpen] = useState(false); @@ -149,6 +153,14 @@ export function SocialPage({ snapshot, actions }: PageProps) { + + setConnectOpen(false)} onSubmit={connect}> diff --git a/AgentCompany-New/src/renderer/src/pages/social/SocialActivity.tsx b/AgentCompany-New/src/renderer/src/pages/social/SocialActivity.tsx new file mode 100644 index 00000000..1d0388b9 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/social/SocialActivity.tsx @@ -0,0 +1,138 @@ +import { Pill } from "../../shared/ui"; +import type { + BrowserActionRecord, + EmployeeRecord, + SocialAccountGrantRecord, + SocialAccountRecord, + SocialAuditRecord, +} from "../../../../shared/domain"; +import { formatRelative } from "../types"; + +interface ActivityEntry { + id: string; + accountId: string | null; + label: string; + policyDecision: string; + actorId: string | null; + actorType: string; + createdAt: string; +} + +export function SocialActivity({ + accounts, + grants, + auditRecords, + browserActions, + employees, +}: { + accounts: SocialAccountRecord[]; + grants: SocialAccountGrantRecord[]; + auditRecords: SocialAuditRecord[]; + browserActions: BrowserActionRecord[]; + employees: EmployeeRecord[]; +}) { + const accountHandle = (id: string | null): string => { + if (!id) return "—"; + const account = accounts.find((item) => item.id === id); + return account ? account.handle : "—"; + }; + const employeeName = (id: string | null): string => { + if (!id) return "System"; + return employees.find((item) => item.id === id)?.displayName ?? id; + }; + + const entries: ActivityEntry[] = [ + ...auditRecords.map((record) => ({ + id: record.id, + accountId: record.socialAccountId, + label: humanize(record.eventType), + policyDecision: record.policyDecision, + actorId: record.actorId, + actorType: record.actorType, + createdAt: record.createdAt, + })), + ...browserActions.map((action) => ({ + id: action.id, + accountId: action.socialAccountId, + label: humanize(action.actionType), + policyDecision: action.policyDecision, + actorId: null, + actorType: "agent", + createdAt: action.createdAt, + })), + ] + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .slice(0, 12); + + if (grants.length === 0 && entries.length === 0) return null; + + return ( +
+
Access & activity
+ {grants.length > 0 && ( +
+ {grants.map((grant, index) => ( +
+ + {employeeName(grant.employeeId)} + + {accountHandle(grant.socialAccountId)} + {humanize(grant.role)} + {scopeSummary(grant.scopeJson)} +
+ ))} +
+ )} + {entries.length > 0 && ( +
+ {entries.map((entry, index) => ( +
+ {entry.label} + {accountHandle(entry.accountId)} + {humanize(entry.policyDecision)} + + {entry.actorType === "agent" ? "agent" : employeeName(entry.actorId)} · {formatRelative(entry.createdAt)} + +
+ ))} +
+ )} +
+ ); +} + +function humanize(value: string): string { + if (!value) return value; + const spaced = value.replace(/_/g, " "); + return spaced.charAt(0).toUpperCase() + spaced.slice(1); +} + +function policyTone(decision: string): "green" | "amber" | "red" | undefined { + if (decision.includes("denied") || decision.includes("blocked")) return "red"; + if (decision.includes("approval")) return "amber"; + if (decision.includes("allowed")) return "green"; + return undefined; +} + +function scopeSummary(scopeJson: string): string { + try { + const parsed: unknown = JSON.parse(scopeJson); + if (parsed && typeof parsed === "object" && Array.isArray((parsed as { actions?: unknown }).actions)) { + const actions = (parsed as { actions: unknown[] }).actions.filter((item): item is string => typeof item === "string"); + const ceiling = (parsed as { riskCeiling?: unknown }).riskCeiling; + const risk = typeof ceiling === "string" ? ` · ≤${ceiling}` : ""; + return `${actions.length} action${actions.length === 1 ? "" : "s"}${risk}`; + } + } catch { + return "scoped"; + } + return "scoped"; +}