feat(timeline): add pulse animation for in-flight drag mutations

Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:28:46 +02:00
parent 7a5e98e2e9
commit 1df208dbcc
386 changed files with 657 additions and 81650 deletions
@@ -1,7 +1,7 @@
import { BlueprintTarget, CreateBlueprintSchema, UpdateBlueprintSchema } from "@capakraken/shared";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createAuditEntry } from "../lib/audit.js";
import { makeAuditLogger } from "../lib/audit-helpers.js";
import type { TRPCContext } from "../trpc.js";
import {
buildBlueprintCreateData,
@@ -32,10 +32,6 @@ type BlueprintDetailReadModel = {
isActive: boolean;
};
function withAuditUser(userId: string | undefined) {
return userId ? { userId } : {};
}
async function getBlueprintOrThrow(ctx: BlueprintProcedureContext, id: string) {
return findUniqueOrThrow(ctx.db.blueprint.findUnique({ where: { id } }), "Blueprint");
}
@@ -131,25 +127,24 @@ export async function getBlueprintDetailByIdentifier(
}
export async function createBlueprint(ctx: BlueprintProcedureContext, input: BlueprintCreateInput) {
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
const blueprint = await ctx.db.blueprint.create({
data: buildBlueprintCreateData(input),
});
void createAuditEntry({
db: ctx.db,
audit({
entityType: "Blueprint",
entityId: blueprint.id,
entityName: blueprint.name,
action: "CREATE",
...withAuditUser(ctx.dbUser?.id),
after: { name: input.name, target: input.target, description: input.description },
source: "ui",
});
return blueprint;
}
export async function updateBlueprint(ctx: BlueprintProcedureContext, input: BlueprintUpdateInput) {
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
const before = await getBlueprintOrThrow(ctx, input.id);
const updated = await ctx.db.blueprint.update({
@@ -157,16 +152,13 @@ export async function updateBlueprint(ctx: BlueprintProcedureContext, input: Blu
data: buildBlueprintUpdateData(input.data),
});
void createAuditEntry({
db: ctx.db,
audit({
entityType: "Blueprint",
entityId: input.id,
entityName: updated.name,
action: "UPDATE",
...withAuditUser(ctx.dbUser?.id),
before: before as unknown as Record<string, unknown>,
after: updated as unknown as Record<string, unknown>,
source: "ui",
});
return updated;
@@ -176,22 +168,20 @@ export async function updateBlueprintRolePresets(
ctx: BlueprintProcedureContext,
input: BlueprintRolePresetsInput,
) {
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
const before = await getBlueprintOrThrow(ctx, input.id);
const updated = await ctx.db.blueprint.update({
where: { id: input.id },
data: buildBlueprintRolePresetsUpdateData(input.rolePresets),
});
void createAuditEntry({
db: ctx.db,
audit({
entityType: "Blueprint",
entityId: input.id,
entityName: updated.name,
action: "UPDATE",
...withAuditUser(ctx.dbUser?.id),
before: { rolePresets: before.rolePresets },
after: { rolePresets: input.rolePresets },
source: "ui",
summary: "Updated role presets",
});
@@ -199,19 +189,17 @@ export async function updateBlueprintRolePresets(
}
export async function deleteBlueprint(ctx: BlueprintProcedureContext, input: BlueprintIdInput) {
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
const deleted = await ctx.db.blueprint.update({
where: { id: input.id },
data: { isActive: false },
});
void createAuditEntry({
db: ctx.db,
audit({
entityType: "Blueprint",
entityId: input.id,
entityName: deleted.name,
action: "DELETE",
...withAuditUser(ctx.dbUser?.id),
source: "ui",
});
return deleted;
@@ -221,6 +209,7 @@ export async function batchDeleteBlueprints(
ctx: BlueprintProcedureContext,
input: BlueprintBatchDeleteInput,
) {
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
const updated = await ctx.db.$transaction(
input.ids.map((id) =>
ctx.db.blueprint.update({
@@ -231,14 +220,11 @@ export async function batchDeleteBlueprints(
);
for (const blueprint of updated) {
void createAuditEntry({
db: ctx.db,
audit({
entityType: "Blueprint",
entityId: blueprint.id,
entityName: blueprint.name,
action: "DELETE",
...withAuditUser(ctx.dbUser?.id),
source: "ui",
});
}
@@ -261,20 +247,18 @@ export async function setBlueprintGlobal(
ctx: BlueprintProcedureContext,
input: BlueprintSetGlobalInput,
) {
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
const updated = await ctx.db.blueprint.update({
where: { id: input.id },
data: { isGlobal: input.isGlobal },
});
void createAuditEntry({
db: ctx.db,
audit({
entityType: "Blueprint",
entityId: input.id,
entityName: updated.name,
action: "UPDATE",
...withAuditUser(ctx.dbUser?.id),
after: { isGlobal: input.isGlobal },
source: "ui",
summary: input.isGlobal ? "Set blueprint as global" : "Removed global flag from blueprint",
});