mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
chore: commit full workspace changes (designer modularization, diagnostics fixes, docs updates, seed script cleanup)
This commit is contained in:
@@ -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
|
||||
|
||||
312
src/server/api/routers/dashboard.ts
Normal file
312
src/server/api/routers/dashboard.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user