Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions services/api/src/services/admin/activity-repository.drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ interface ActivityRowSelect {
id: string
ts: Date
who: string | null
actorless: boolean
where_label: string | null
action: string | null
event_type: string | null
Expand All @@ -67,6 +68,7 @@ function toRecord(r: ActivityRowSelect): ActivitySourceRecord {
id: r.id,
ts: r.ts,
who: r.who ?? "",
actorless: r.actorless,
where: r.where_label ?? "",
action: r.action,
eventType: r.event_type,
Expand Down Expand Up @@ -147,7 +149,7 @@ export function makeDrizzleActivityRepository(sql: Sql): ActivityRepository {
if (wants("audit")) {
branches.push(sql`(
SELECT 'audit'::text AS source, a.id::text AS id, a.created_at AS ts,
u.display_name AS who, a.target AS where_label,
u.display_name AS who, a.actor_id IS NULL AS actorless, a.target AS where_label,
a.action AS action, NULL::text AS event_type, a.target AS subject
FROM audit_log a
LEFT JOIN users u ON u.id = a.actor_id
Expand All @@ -164,7 +166,7 @@ export function makeDrizzleActivityRepository(sql: Sql): ActivityRepository {
if (wants("report")) {
branches.push(sql`(
SELECT 'report'::text AS source, r.id::text AS id, r.created_at AS ts,
ru.display_name AS who, j.name AS where_label,
ru.display_name AS who, false AS actorless, j.name AS where_label,
NULL::text AS action, NULL::text AS event_type, r.category AS subject
FROM reports r
LEFT JOIN users ru ON ru.id = r.reporter_user_id
Expand All @@ -180,7 +182,7 @@ export function makeDrizzleActivityRepository(sql: Sql): ActivityRepository {
if (wants("cleanup")) {
branches.push(sql`(
SELECT 'cleanup'::text AS source, c.id::text AS id, c.created_at AS ts,
cu.display_name AS who, c.address AS where_label,
cu.display_name AS who, false AS actorless, c.address AS where_label,
NULL::text AS action, NULL::text AS event_type, c.title AS subject
FROM cleanups c
LEFT JOIN users cu ON cu.id = c.organizer_user_id
Expand All @@ -195,7 +197,7 @@ export function makeDrizzleActivityRepository(sql: Sql): ActivityRepository {
if (wants("mail_event")) {
branches.push(sql`(
SELECT 'mail_event'::text AS source, e.id::text AS id, e.created_at AS ts,
t.org AS who, COALESCE(t.org, t.jurisdiction_geoid) AS where_label,
t.org AS who, false AS actorless, COALESCE(t.org, t.jurisdiction_geoid) AS where_label,
NULL::text AS action, e.type AS event_type, t.subject AS subject
FROM mail_events e
LEFT JOIN mail_threads t ON t.id = e.thread_id
Expand Down
10 changes: 9 additions & 1 deletion services/api/src/services/admin/activity-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ActivitySourceRecord {
id: string
ts: Date
who: string
actorless?: boolean
where: string
action?: string | null
eventType?: string | null
Expand Down Expand Up @@ -220,6 +221,13 @@ export function describeAuditAction(action: string): string {
return map[action] ?? action
}

const PUBLIC_AUDIT_ACTIONS: ReadonlySet<string> = new Set(["discovery.contact_suggested"])

function auditActor(record: ActivitySourceRecord, action: string): string {
if (record.actorless !== true) return record.who || "Operator"
return PUBLIC_AUDIT_ACTIONS.has(action) ? "A neighbor" : "System"
}

export function classifyActivity(record: ActivitySourceRecord, ref: Date): ActivityItemDTO {
const ts = relativeAgo(record.ts, ref)
if (record.source === "report") {
Expand All @@ -245,7 +253,7 @@ export function classifyActivity(record: ActivitySourceRecord, ref: Date): Activ
}
const action = record.action ?? ""
const kind = classifyAuditAction(action)
return item(kind, record.who || "Operator", describeAuditAction(action), record.where, ts)
return item(kind, auditActor(record, action), describeAuditAction(action), record.where, ts)
}

function item(
Expand Down
15 changes: 15 additions & 0 deletions services/api/test/integration/admin-activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe.skipIf(!pg)("admin activity repository (integration: real schema)", ()
// The audit row carries the actor name + action.
const audit = records.find((r) => r.source === "audit")!
expect(audit.who).toBe("Operator")
expect(audit.actorless).toBe(false)
expect(audit.action).toBe("gov_claim.approved")
// The report carries the category subject + the jurisdiction place.
const report = records.find((r) => r.source === "report")!
Expand All @@ -115,6 +116,20 @@ describe.skipIf(!pg)("admin activity repository (integration: real schema)", ()
expect(mail.eventType).toBe("bounced")
})

it("marks only an audit row with no actor as actorless", async () => {
await seedOneOfEachSource()
await h.sql`
INSERT INTO audit_log (actor_id, action, target)
VALUES (NULL, 'mail.reply_published_without_text', 'mail:1')
`

const { records } = await repo.list(listArgs())
expect(records).toHaveLength(5)
expect(records.filter((r) => r.actorless).map((r) => r.action)).toEqual([
"mail.reply_published_without_text",
])
})

it("sort=oldest walks the same merged feed the other way", async () => {
await seedOneOfEachSource()

Expand Down
13 changes: 13 additions & 0 deletions services/api/test/unit/admin-activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@ describe("classifyActivity (per source)", () => {
.who,
).toBe("Operator")
})

it("labels an audit row with no actor as the system, or a neighbor for a public suggestion", () => {
const who = (action: string, actorless = true) =>
classifyActivity(
{ source: "audit", id: "a", ts: NOW, who: "", actorless, where: "", action },
NOW,
).who
expect(who("mail.reply_published_without_text")).toBe("System")
expect(who("mail.send_failed")).toBe("System")
expect(who("report.routed")).toBe("System")
expect(who("discovery.contact_suggested")).toBe("A neighbor")
expect(who("mail.sent", false)).toBe("Operator")
})
})

describe("activity service wiring", () => {
Expand Down
Loading