metatroncubeswdev ce75d45141
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
Phase 4: activity log — who did what in the workspace
- 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>
2026-09-09 16:00:09 -04:00

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 };
});