- activity_log table (sqlite + pg, structurally identical; schema-parity covers it). Plain-text columns, no FKs — an append-only trail that must outlive the projects/users it references, so target_label snapshots a human-readable name at write time. - ActivityRepository: record() (fire-and-forget, never breaks the caller) + list() (org-scoped, actor/action filters, keyset pagination) + listActors(). - Recording wired into the mutations worth tracking: project create/archive/restore/domain, audit start, team user create/remove/ password-reset, invitation sent. - getActivityLog / getActivityActors server functions (owner/admin gated) + Settings → Activity tab (ActivityLogView: filter by user & action, load more). - Migration: drizzle/0045_*, drizzle-pg/0023_*. The pipeline does not run migrations — see docs/SELF_HOSTING_TEAM_MODE.md step 5 for the one-time `drizzle-kit migrate` on the server. Writes fail silently until the table exists. tsc / oxlint / knip clean. New ActivityRepository.test.ts (4) + schema-parity picks up the new table; suite otherwise unchanged (pre-existing samSkills CRLF failure only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import { createServerFn } from "@tanstack/react-start";
|
|
import { waitUntil } from "cloudflare:workers";
|
|
import { requireOrgPermission } from "@/server/auth/org-gate";
|
|
import { ActivityRepository } from "@/server/features/activity/ActivityRepository";
|
|
import { AuditService } from "@/server/features/audit/services/AuditService";
|
|
import { captureServerEvent } from "@/server/lib/posthog";
|
|
import { requireProjectContext } from "@/serverFunctions/middleware";
|
|
import {
|
|
deleteAuditSchema,
|
|
getAuditHistorySchema,
|
|
getAuditResultsSchema,
|
|
getAuditStatusSchema,
|
|
getCrawlProgressSchema,
|
|
startAuditSchema,
|
|
} from "@/types/schemas/audit";
|
|
|
|
export const startAudit = createServerFn({ method: "POST" })
|
|
.middleware(requireProjectContext)
|
|
.validator(startAuditSchema)
|
|
.handler(async ({ data, context }) => {
|
|
const limitTier = await AuditService.resolveAuditLimitTier(context);
|
|
|
|
const result = await AuditService.startAudit({
|
|
actorUserId: context.userId,
|
|
billingCustomer: context,
|
|
projectId: context.projectId,
|
|
startUrl: data.startUrl,
|
|
maxPages: data.maxPages,
|
|
lighthouseStrategy: data.lighthouseStrategy,
|
|
limitTier,
|
|
});
|
|
|
|
await ActivityRepository.record({
|
|
context,
|
|
action: "audit.start",
|
|
targetType: "project",
|
|
targetId: context.projectId,
|
|
targetLabel: data.startUrl,
|
|
metadata: { maxPages: data.maxPages ?? 50 },
|
|
});
|
|
|
|
waitUntil(
|
|
captureServerEvent({
|
|
distinctId: context.userId,
|
|
event: "site_audit:start",
|
|
organizationId: context.organizationId,
|
|
properties: {
|
|
project_id: context.projectId,
|
|
max_pages: data.maxPages ?? 50,
|
|
run_lighthouse: data.lighthouseStrategy !== "none",
|
|
plan_tier: limitTier,
|
|
},
|
|
}),
|
|
);
|
|
|
|
return result;
|
|
});
|
|
|
|
export const getAuditStatus = createServerFn({ method: "POST" })
|
|
.middleware(requireProjectContext)
|
|
.validator(getAuditStatusSchema)
|
|
.handler(async ({ data, context }) => {
|
|
return AuditService.getStatus(data.auditId, context.projectId);
|
|
});
|
|
|
|
export const getAuditResults = createServerFn({ method: "POST" })
|
|
.middleware(requireProjectContext)
|
|
.validator(getAuditResultsSchema)
|
|
.handler(async ({ data, context }) => {
|
|
return AuditService.getResults(data.auditId, context.projectId);
|
|
});
|
|
|
|
export const getAuditHistory = createServerFn({ method: "POST" })
|
|
.middleware(requireProjectContext)
|
|
.validator(getAuditHistorySchema)
|
|
.handler(async ({ context }) => {
|
|
return AuditService.getHistory(context.projectId);
|
|
});
|
|
|
|
export const getCrawlProgress = createServerFn({ method: "POST" })
|
|
.middleware(requireProjectContext)
|
|
.validator(getCrawlProgressSchema)
|
|
.handler(async ({ data, context }) => {
|
|
return AuditService.getCrawlProgress(data.auditId, context.projectId);
|
|
});
|
|
|
|
export const deleteAudit = createServerFn({ method: "POST" })
|
|
.middleware(requireProjectContext)
|
|
.validator(deleteAuditSchema)
|
|
.handler(async ({ data, context }) => {
|
|
// Deleting audits frees the org's free-plan capacity ceiling (a SUM over
|
|
// audit rows), so it gets the same destructive-action gate as archiving.
|
|
requireOrgPermission(context, { project: ["delete"] });
|
|
await AuditService.remove(data.auditId, context.projectId);
|
|
return { success: true };
|
|
});
|