chore: commit full workspace changes (designer modularization, diagnostics fixes, docs updates, seed script cleanup)

This commit is contained in:
2025-08-08 00:37:35 -04:00
parent c071d33624
commit 1ac8296ab7
37 changed files with 5378 additions and 5758 deletions

View File

@@ -2,6 +2,7 @@ import { adminRouter } from "~/server/api/routers/admin";
import { analyticsRouter } from "~/server/api/routers/analytics";
import { authRouter } from "~/server/api/routers/auth";
import { collaborationRouter } from "~/server/api/routers/collaboration";
import { dashboardRouter } from "~/server/api/routers/dashboard";
import { experimentsRouter } from "~/server/api/routers/experiments";
import { mediaRouter } from "~/server/api/routers/media";
import { participantsRouter } from "~/server/api/routers/participants";
@@ -28,6 +29,7 @@ export const appRouter = createTRPCRouter({
analytics: analyticsRouter,
collaboration: collaborationRouter,
admin: adminRouter,
dashboard: dashboardRouter,
});
// export type definition of API

View File

@@ -0,0 +1,312 @@
import { and, count, desc, eq, gte, inArray, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import {
activityLogs,
experiments,
participants,
studies,
studyMembers,
trials,
users,
userSystemRoles,
} from "~/server/db/schema";
export const dashboardRouter = createTRPCRouter({
getRecentActivity: protectedProcedure
.input(
z.object({
limit: z.number().min(1).max(20).default(10),
studyId: z.string().uuid().optional(),
}),
)
.query(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
// Get studies the user has access to
const accessibleStudies = await ctx.db
.select({ studyId: studyMembers.studyId })
.from(studyMembers)
.where(eq(studyMembers.userId, userId));
const studyIds = accessibleStudies.map((s) => s.studyId);
// If no accessible studies, return empty
if (studyIds.length === 0) {
return [];
}
// Build where conditions
const whereConditions = input.studyId
? eq(activityLogs.studyId, input.studyId)
: inArray(activityLogs.studyId, studyIds);
// Get recent activity logs
const activities = await ctx.db
.select({
id: activityLogs.id,
action: activityLogs.action,
description: activityLogs.description,
createdAt: activityLogs.createdAt,
user: {
name: users.name,
email: users.email,
},
study: {
name: studies.name,
},
})
.from(activityLogs)
.innerJoin(users, eq(activityLogs.userId, users.id))
.innerJoin(studies, eq(activityLogs.studyId, studies.id))
.where(whereConditions)
.orderBy(desc(activityLogs.createdAt))
.limit(input.limit);
return activities.map((activity) => ({
id: activity.id,
type: activity.action,
title: activity.description,
description: `${activity.study.name} - ${activity.user.name}`,
time: activity.createdAt,
status: "info" as const,
}));
}),
getStudyProgress: protectedProcedure
.input(
z.object({
limit: z.number().min(1).max(10).default(5),
}),
)
.query(async ({ ctx, input }) => {
const userId = ctx.session.user.id;
// Get studies the user has access to with participant counts
const studyProgress = await ctx.db
.select({
id: studies.id,
name: studies.name,
status: studies.status,
createdAt: studies.createdAt,
totalParticipants: count(participants.id),
})
.from(studies)
.innerJoin(studyMembers, eq(studies.id, studyMembers.studyId))
.leftJoin(participants, eq(studies.id, participants.studyId))
.where(
and(eq(studyMembers.userId, userId), eq(studies.status, "active")),
)
.groupBy(studies.id, studies.name, studies.status, studies.createdAt)
.orderBy(desc(studies.createdAt))
.limit(input.limit);
// Get trial completion counts for each study
const studyIds = studyProgress.map((s) => s.id);
const trialCounts =
studyIds.length > 0
? await ctx.db
.select({
studyId: experiments.studyId,
completedTrials: count(trials.id),
})
.from(experiments)
.innerJoin(trials, eq(experiments.id, trials.experimentId))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "completed"),
),
)
.groupBy(experiments.studyId)
: [];
const trialCountMap = new Map(
trialCounts.map((tc) => [tc.studyId, tc.completedTrials]),
);
return studyProgress.map((study) => {
const completedTrials = trialCountMap.get(study.id) ?? 0;
const totalParticipants = study.totalParticipants;
// Calculate progress based on completed trials vs participants
// If no participants, progress is 0; if trials >= participants, progress is 100%
const progress =
totalParticipants > 0
? Math.min(
100,
Math.round((completedTrials / totalParticipants) * 100),
)
: 0;
return {
id: study.id,
name: study.name,
progress,
participants: completedTrials, // Using completed trials as active participants
totalParticipants,
status: study.status,
};
});
}),
getStats: protectedProcedure.query(async ({ ctx }) => {
const userId = ctx.session.user.id;
// Get studies the user has access to
const accessibleStudies = await ctx.db
.select({ studyId: studyMembers.studyId })
.from(studyMembers)
.where(eq(studyMembers.userId, userId));
const studyIds = accessibleStudies.map((s) => s.studyId);
if (studyIds.length === 0) {
return {
totalStudies: 0,
totalExperiments: 0,
totalParticipants: 0,
totalTrials: 0,
activeTrials: 0,
scheduledTrials: 0,
completedToday: 0,
};
}
// Get total counts
const [studyCount] = await ctx.db
.select({ count: count() })
.from(studies)
.where(inArray(studies.id, studyIds));
const [experimentCount] = await ctx.db
.select({ count: count() })
.from(experiments)
.where(inArray(experiments.studyId, studyIds));
const [participantCount] = await ctx.db
.select({ count: count() })
.from(participants)
.where(inArray(participants.studyId, studyIds));
const [trialCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(inArray(experiments.studyId, studyIds));
// Get active trials count
const [activeTrialsCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "in_progress"),
),
);
// Get scheduled trials count
const [scheduledTrialsCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "scheduled"),
),
);
// Get today's completed trials
const today = new Date();
today.setHours(0, 0, 0, 0);
const [completedTodayCount] = await ctx.db
.select({ count: count() })
.from(trials)
.innerJoin(experiments, eq(trials.experimentId, experiments.id))
.where(
and(
inArray(experiments.studyId, studyIds),
eq(trials.status, "completed"),
gte(trials.completedAt, today),
),
);
return {
totalStudies: studyCount?.count ?? 0,
totalExperiments: experimentCount?.count ?? 0,
totalParticipants: participantCount?.count ?? 0,
totalTrials: trialCount?.count ?? 0,
activeTrials: activeTrialsCount?.count ?? 0,
scheduledTrials: scheduledTrialsCount?.count ?? 0,
completedToday: completedTodayCount?.count ?? 0,
};
}),
debug: protectedProcedure.query(async ({ ctx }) => {
const userId = ctx.session.user.id;
// Get user info
const user = await ctx.db.query.users.findFirst({
where: eq(users.id, userId),
});
// Get user system roles
const systemRoles = await ctx.db.query.userSystemRoles.findMany({
where: eq(userSystemRoles.userId, userId),
});
// Get user study memberships
const studyMemberships = await ctx.db.query.studyMembers.findMany({
where: eq(studyMembers.userId, userId),
with: {
study: {
columns: {
id: true,
name: true,
status: true,
},
},
},
});
// Get all studies (admin view)
const allStudies = await ctx.db.query.studies.findMany({
columns: {
id: true,
name: true,
status: true,
createdBy: true,
},
where: sql`deleted_at IS NULL`,
limit: 10,
});
return {
user: user
? {
id: user.id,
email: user.email,
name: user.name,
}
: null,
systemRoles: systemRoles.map((r) => r.role),
studyMemberships: studyMemberships.map((m) => ({
studyId: m.studyId,
role: m.role,
study: m.study,
})),
allStudies,
session: {
userId: ctx.session.user.id,
userEmail: ctx.session.user.email,
userRole: ctx.session.user.roles?.[0]?.role ?? null,
},
};
}),
});

View File

@@ -468,6 +468,7 @@ export const robotsRouter = createTRPCRouter({
repositoryUrl: plugins.repositoryUrl,
trustLevel: plugins.trustLevel,
status: plugins.status,
actionDefinitions: plugins.actionDefinitions,
createdAt: plugins.createdAt,
updatedAt: plugins.updatedAt,
},

View File

@@ -4,8 +4,15 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import {
activityLogs, studies, studyMemberRoleEnum, studyMembers,
studyStatusEnum, users, userSystemRoles
activityLogs,
plugins,
studies,
studyMemberRoleEnum,
studyMembers,
studyPlugins,
studyStatusEnum,
users,
userSystemRoles,
} from "~/server/db/schema";
export const studiesRouter = createTRPCRouter({
@@ -274,6 +281,20 @@ export const studiesRouter = createTRPCRouter({
role: "owner",
});
// Auto-install core plugin in new study
const corePlugin = await ctx.db.query.plugins.findFirst({
where: eq(plugins.name, "HRIStudio Core System"),
});
if (corePlugin) {
await ctx.db.insert(studyPlugins).values({
studyId: newStudy.id,
pluginId: corePlugin.id,
configuration: {},
installedBy: userId,
});
}
// Log activity
await ctx.db.insert(activityLogs).values({
studyId: newStudy.id,
@@ -534,7 +555,7 @@ export const studiesRouter = createTRPCRouter({
studyId,
userId,
action: "member_removed",
description: `Removed ${memberToRemove.user?.name ?? memberToRemove.user?.email ?? 'Unknown user'}`,
description: `Removed ${memberToRemove.user?.name ?? memberToRemove.user?.email ?? "Unknown user"}`,
});
return { success: true };

View File

@@ -393,6 +393,7 @@ export const experiments = createTable(
visualDesign: jsonb("visual_design"),
executionGraph: jsonb("execution_graph"),
pluginDependencies: text("plugin_dependencies").array(),
integrityHash: varchar("integrity_hash", { length: 128 }),
deletedAt: timestamp("deleted_at", { withTimezone: true }),
},
(table) => ({
@@ -496,12 +497,24 @@ export const actions = createTable(
.references(() => steps.id, { onDelete: "cascade" }),
name: varchar("name", { length: 255 }).notNull(),
description: text("description"),
type: varchar("type", { length: 100 }).notNull(), // e.g., 'speak', 'move', 'wait', 'collect_data'
type: varchar("type", { length: 100 }).notNull(), // e.g., 'speak', 'move', 'wait', 'collect_data' or pluginId.actionId
orderIndex: integer("order_index").notNull(),
parameters: jsonb("parameters").default({}),
validationSchema: jsonb("validation_schema"),
timeout: integer("timeout"), // in seconds
retryCount: integer("retry_count").default(0).notNull(),
// Provenance & execution metadata
sourceKind: varchar("source_kind", { length: 20 }), // 'core' | 'plugin'
pluginId: varchar("plugin_id", { length: 255 }),
pluginVersion: varchar("plugin_version", { length: 50 }),
robotId: varchar("robot_id", { length: 255 }),
baseActionId: varchar("base_action_id", { length: 255 }),
category: varchar("category", { length: 50 }),
transport: varchar("transport", { length: 20 }), // 'ros2' | 'rest' | 'internal'
ros2: jsonb("ros2_config"),
rest: jsonb("rest_config"),
retryable: boolean("retryable"),
parameterSchemaRaw: jsonb("parameter_schema_raw"),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),