mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
feat(analytics): refine timeline visualization and add print support
This commit is contained in:
@@ -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 ?? [],
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user