feat(analytics): refine timeline visualization and add print support

This commit is contained in:
2026-02-17 21:17:11 -05:00
parent 568d408587
commit 72971a4b49
82 changed files with 6670 additions and 2448 deletions

View File

@@ -389,9 +389,11 @@ export const experimentsRouter = createTRPCRouter({
}
: null;
const convertedSteps = convertDatabaseToSteps(experiment.steps);
return {
...experiment,
steps: convertDatabaseToSteps(experiment.steps),
steps: convertedSteps,
integrityHash: experiment.integrityHash,
executionGraphSummary,
pluginDependencies: experiment.pluginDependencies ?? [],

View File

@@ -665,4 +665,108 @@ export const studiesRouter = createTRPCRouter({
},
};
}),
// Plugin configuration management
getPluginConfiguration: protectedProcedure
.input(
z.object({
studyId: z.string().uuid(),
pluginId: z.string().uuid(),
}),
)
.query(async ({ ctx, input }) => {
const { studyId, pluginId } = input;
const userId = ctx.session.user.id;
// Check if user has access to this study
const membership = await ctx.db.query.studyMembers.findFirst({
where: and(
eq(studyMembers.studyId, studyId),
eq(studyMembers.userId, userId),
),
});
if (!membership) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have access to this study",
});
}
// Get the study plugin configuration
const studyPlugin = await ctx.db.query.studyPlugins.findFirst({
where: and(
eq(studyPlugins.studyId, studyId),
eq(studyPlugins.pluginId, pluginId),
),
});
if (!studyPlugin) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Plugin not installed in this study",
});
}
return studyPlugin.configuration ?? {};
}),
updatePluginConfiguration: protectedProcedure
.input(
z.object({
studyId: z.string().uuid(),
pluginId: z.string().uuid(),
configuration: z.any(),
}),
)
.mutation(async ({ ctx, input }) => {
const { studyId, pluginId, configuration } = input;
const userId = ctx.session.user.id;
// Check if user has permission to update plugin configuration
const membership = await ctx.db.query.studyMembers.findFirst({
where: and(
eq(studyMembers.studyId, studyId),
eq(studyMembers.userId, userId),
),
});
if (!membership || !["owner", "researcher"].includes(membership.role)) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to update plugin configuration",
});
}
// Update the plugin configuration
const [updatedPlugin] = await ctx.db
.update(studyPlugins)
.set({
configuration,
})
.where(
and(
eq(studyPlugins.studyId, studyId),
eq(studyPlugins.pluginId, pluginId),
),
)
.returning();
if (!updatedPlugin) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Plugin not found in this study",
});
}
// Log activity
await ctx.db.insert(activityLogs).values({
studyId,
userId,
action: "plugin_configured",
description: `Updated plugin configuration`,
});
return updatedPlugin;
}),
});

View File

@@ -306,6 +306,32 @@ export const trialsRouter = createTRPCRouter({
};
}),
getLatestSession: protectedProcedure
.input(
z.object({
participantId: z.string(),
experimentId: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
const { db } = ctx;
const { participantId, experimentId } = input;
const conditions: SQL[] = [eq(trials.participantId, participantId)];
if (experimentId) {
conditions.push(eq(trials.experimentId, experimentId));
}
const result = await db
.select({ sessionNumber: trials.sessionNumber })
.from(trials)
.where(and(...conditions))
.orderBy(desc(trials.sessionNumber))
.limit(1);
return result[0]?.sessionNumber ?? 0;
}),
create: protectedProcedure
.input(
z.object({
@@ -756,11 +782,10 @@ export const trialsRouter = createTRPCRouter({
return { success: true, url: uploadResult.url };
} catch (error) {
console.error("Failed to archive trial:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to upload archive to storage",
});
console.error("Failed to archive trial (non-fatal):", error);
// Do not throw error to client, as archiving is a background task
// and shouldn't block the user flow or show alarming errors
return { success: false, error: "Failed to upload archive to storage" };
}
}),
@@ -1248,7 +1273,7 @@ export const trialsRouter = createTRPCRouter({
await db.insert(trialEvents).values({
trialId: input.trialId,
eventType: "manual_robot_action",
actionId: actionDefinition.id,
actionId: null, // Ad-hoc action, not linked to a protocol action definition
data: {
userId,
pluginName: input.pluginName,