feat(alerts): always-visible dismiss/ack actions + Dismiss all (service, endpoint, UI)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 17:53:12 +02:00
parent 13efe177a5
commit 6c4490f653
7 changed files with 180 additions and 10 deletions

View File

@@ -0,0 +1,70 @@
/**
* Bulk-dismiss service: dismissAllForPort must respect the optional rule/
* severity filter and never touch another port's alerts.
*/
import { describe, it, expect, beforeAll, vi } from 'vitest';
import { and, eq, isNull } from 'drizzle-orm';
vi.mock('@/lib/socket/server', () => ({
emitToRoom: vi.fn(),
}));
import { db } from '@/lib/db';
import { alerts } from '@/lib/db/schema/insights';
import { user } from '@/lib/db/schema/users';
import { dismissAllForPort } from '@/lib/services/alerts.service';
import { makePort } from '../helpers/factories';
let USER_ID = '';
beforeAll(async () => {
const [u] = await db.select({ id: user.id }).from(user).limit(1);
if (!u) throw new Error('No user available; run pnpm db:seed first');
USER_ID = u.id;
});
async function seedAlert(portId: string, ruleId: string, severity = 'info') {
const [row] = await db
.insert(alerts)
.values({
portId,
ruleId,
severity,
title: `t-${ruleId}`,
link: '/x',
fingerprint: `fp-${Math.random().toString(36).slice(2)}`,
metadata: {},
})
.returning({ id: alerts.id });
return row!.id;
}
async function openCount(portId: string) {
const rows = await db
.select()
.from(alerts)
.where(and(eq(alerts.portId, portId), isNull(alerts.dismissedAt), isNull(alerts.resolvedAt)));
return rows.length;
}
describe('dismissAllForPort', () => {
it('dismisses only the filtered rule, scoped to the port, then all', async () => {
const portA = await makePort();
const portB = await makePort();
await seedAlert(portA.id, 'interest.stale');
await seedAlert(portA.id, 'interest.stale');
await seedAlert(portA.id, 'document.signer_overdue', 'warning');
await seedAlert(portB.id, 'interest.stale');
const filtered = await dismissAllForPort(portA.id, USER_ID, { ruleId: 'interest.stale' });
expect(filtered).toBe(2);
expect(await openCount(portA.id)).toBe(1); // signer_overdue remains
expect(await openCount(portB.id)).toBe(1); // other port untouched
const rest = await dismissAllForPort(portA.id, USER_ID);
expect(rest).toBe(1);
expect(await openCount(portA.id)).toBe(0);
expect(await openCount(portB.id)).toBe(1);
});
});