ActivityStage: Sequential Activity Progression
Implement the following plan:
ActivityStage: Sequential Activity Progression
Overview
Add <ActivityStage> MDX component for guiding students through sequential activities during class meetings. Supports both individual student progression and instructor-controlled class-wide advancement.
Usage Example
<ActivityStages meetingSlug="2026-01-27-ethics">
<ActivityStage label="Opening Discussion" time="5m">
Discuss with your neighbor: What ethical concerns do you have about AI?
</ActivityStage>
<ActivityStage label="Group Formation" time="3m" autoAdvance>
<GroupActivity id="ethics-groups" group="ethics-meeting-1">
You're now in your group! Proceed to the case study.
</GroupActivity>
</ActivityStage>
<ActivityStage label="Case Study" time="15m">
Review and discuss the case study...
</ActivityStage>
</ActivityStages>
The "Donut" Pattern
Interactive MDX components use a layered server/client architecture:
Server Component (outer) ← Fetches data, injects props
└── Client Component ← Provides React context
└── Server Component ← Can still fetch data (passed as children)
└── Client Component ← Consumes context
Key principle: Children passed to a client component from a server component remain server-rendered. Only imported components become client components.
// ActivityStages.tsx (SERVER) - fetches session, injects index props
export async function ActivityStages({ children, meetingSlug }) {
const session = await fetchSession(meetingSlug);
const indexed = React.Children.map(children, (child, i) =>
React.cloneElement(child, { index: i })
);
return (
<ActivityStagesClient initialSession={session}>
{indexed}
</ActivityStagesClient>
);
}
// ActivityStage.tsx (SERVER) - receives index as prop, can fetch stage data
export async function ActivityStage({ children, index, label }) {
const stageData = await fetchStageData(index);
return (
<ActivityStageClient index={index} initialData={stageData}>
{children} {/* MDX content - still server rendered */}
</ActivityStageClient>
);
}
// ActivityStageClient.tsx (CLIENT) - consumes context
"use client";
export function ActivityStageClient({ children, index, initialData }) {
const { currentIndex } = useActivityStages(); // Context works here
return <div>{children}</div>;
}
Core Features
Student View
- See one stage at a time (or all unlocked stages)
- Complete stages via content triggers (e.g., GroupActivity completion) or "Mark Complete" button
autoAdvancestages automatically show next stage on completion- Non-autoAdvance stages show "Waiting for instructor..." after completion
Instructor View
- See ALL stages simultaneously with visibility badges
- Per-stage controls: "Advance All" button to unlock stage for entire class
- Count-up timer showing elapsed time vs budgeted time
- Completion tracker: "15/32 students completed"
- Global controls: Reset session, jump to specific stage
Files to Create
| File | Type | Purpose |
|---|---|---|
packages/db/src/schema/activity-stages.ts | Schema | Database tables |
apps/api/src/routes/activity-stages.ts | API | Student endpoints |
apps/api/src/routes/admin/activity-stages.ts | API | Instructor endpoints |
apps/web/components/mdx/activity-stages.tsx | Server | Container component |
apps/web/components/mdx/activity-stage.tsx | Server | Individual stage wrapper |
apps/web/components/mdx/activity-stage-client.tsx | Client | Interactive stage UI |
apps/web/components/mdx/activity-stage-context.tsx | Client | React context provider |
apps/web/components/mdx/activity-stage-instructor.tsx | Client | Instructor controls |
testing/e2e/tests/activity-stage.spec.ts | Test | E2E tests |
Files to Modify
| File | Changes |
|---|---|
CLAUDE.md | Document the donut pattern for MDX components |
apps/web/mdx-components.tsx | Register ActivityStages, ActivityStage |
apps/web/components/mdx/group-activity-client.tsx | Add optional stage completion hook |
packages/db/src/schema/index.ts | Export new schema |
apps/api/src/index.ts | Register new routes |
Database Schema
// activity_sessions - tracks class-wide session state
export const activitySessions = pgTable("activity_sessions", {
id: text("id").primaryKey(),
meetingSlug: text("meeting_slug").notNull(),
semester: text("semester").notNull(),
currentStageIndex: integer("current_stage_index").notNull().default(0),
totalStages: integer("total_stages").notNull(),
startedAt: timestamp("started_at", { mode: "date" }),
createdAt: timestamp("created_at", { mode: "date" }).notNull().defaultNow(),
}, (t) => [
unique().on(t.meetingSlug, t.semester),
]);
// activity_stage_timings - timing data per stage
export const activityStageTimings = pgTable("activity_stage_timings", {
id: text("id").primaryKey(),
sessionId: text("session_id").notNull().references(() => activitySessions.id),
stageIndex: integer("stage_index").notNull(),
label: text("label").notNull(),
budgetedSeconds: integer("budgeted_seconds"),
startedAt: timestamp("started_at", { mode: "date" }),
}, (t) => [
unique().on(t.sessionId, t.stageIndex),
]);
// activity_stage_completions - individual student completions
export const activityStageCompletions = pgTable("activity_stage_completions", {
id: text("id").primaryKey(),
sessionId: text("session_id").notNull().references(() => activitySessions.id),
stageIndex: integer("stage_index").notNull(),
userId: text("user_id").notNull(),
completedAt: timestamp("completed_at", { mode: "date" }).notNull().defaultNow(),
}, (t) => [
unique().on(t.sessionId, t.stageIndex, t.userId),
]);
API Endpoints
Student Endpoints (/api/activity-stages/:meetingSlug)
| Method | Path | Purpose |
|---|---|---|
| GET | / | Get session status, stages, user completions |
| POST | /complete/:stageIndex | Mark stage complete for current user |
Instructor Endpoints (/api/admin/activity-stages/:meetingSlug)
| Method | Path | Purpose |
|---|---|---|
| POST | /start | Initialize session with stage metadata |
| POST | /advance | Advance class to next/specific stage |
| GET | /status | Get full status with completion counts |
| POST | /reset | Reset session |
Component Props
interface ActivityStagesProps {
children: React.ReactNode;
meetingSlug: string;
}
interface ActivityStageProps {
children: React.ReactNode;
label: string;
time?: string; // "5m", "10m", "1h30m"
autoAdvance?: boolean; // Auto-show next stage on completion
}
Implementation Order
- Document donut pattern in CLAUDE.md
- Database schema - Create tables, run
bun run db:push - API routes - Student endpoints, then admin endpoints
- Context provider - State management with SWR
- Client components - Stage display and instructor controls
- Server components - Container and MDX wrappers
- MDX registration - Add to mdx-components.tsx
- GroupActivity integration - Add optional completion hook
- Test page - Create
/testing/components/activity-stages - E2E tests - Test student and instructor flows
Verification
- Run
bun run dev - Create test page at
apps/web/app/testing/components/activity-stages/page.mdx - Test as student:
- See only first stage initially
- Complete stage, verify autoAdvance vs waiting behavior
- Test as instructor:
- See all stages with timers and completion counts
- Advance to next stage, verify students see new content
- Test GroupActivity nested inside stage triggers completion
- Run
CI=true bun run test:e2e- all tests pass
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ~/.claude/projects/-Users-challen-www-usingandunderstanding-ai/ac4a9609-8149-46ba-9140-4093bd53a17a.jsonl
I'll implement the ActivityStage sequential activity progression system. Let me start by creating a todo list to track the implementation.
Let me first read the relevant existing files to understand the patterns used.
Let me also read the group-activity server component and an existing API route to understand the patterns better.
Now I understand the patterns. Let me start implementing. I'll begin with documenting the donut pattern in CLAUDE.md.
Now let me create the database schema for activity stages.
Now let me add the export to the schema index.
Let me check if the dev server is running, then push the schema changes.
Let me read the auth middleware to understand how to use it properly.
Now let me create the student API routes for activity stages.
Now let me register these routes in the API index.ts.
Let me read the SWR fetcher and env patterns used in the web app.
Now let me create the context provider for activity stages.
Let me check how server-side API fetching works.
Now let me create the server components. First the container component.
Now let me create the ActivityStage server component.
Now let me add the GroupActivity integration. I'll create a hook that GroupActivityClient can use to notify when a group completes.
Now let me update GroupActivityClient to trigger stage completion when the group becomes complete.
Let me check the testing directory structure.
Let me run the type checker to make sure everything compiles.
There's a TypeScript error. Let me fix it.
Let me fix these TypeScript errors.
TypeScript checks pass. Let me also run the linter.
Let me fix the lint errors.
I see that activityGroups is unused but also I should add cleanup for the new activity stages tables. Let me update the e2e routes.
Let me read an existing E2E test to understand the pattern.
Let me run the E2E tests.
The test is failing because the session from the previous test persists. Let me update the tests to reset the session at the beginning.
The test is failing because the session persists from previous tests. Let me update the test to handle this more gracefully.
Let me look at the reset function more carefully and fix it.
All tests pass. Let me run the full E2E test suite to make sure nothing is broken.
Sorry, continue.
Let me update the todo list and provide a summary of the implementation.
The ActivityStage implementation is complete. Here's a summary of what was created:
Files Created
Database Schema:
packages/db/src/schema/activity-stages.ts- Three tables:activity_sessions,activity_stage_timings,activity_stage_completions
API Routes:
apps/api/src/routes/activity-stages.ts- Student endpoints for getting session status and completing stagesapps/api/src/routes/admin/activity-stages.ts- Instructor endpoints for starting, advancing, resetting sessions and viewing completion counts
React Components:
apps/web/components/mdx/activity-stages.tsx- Server component container that fetches session dataapps/web/components/mdx/activity-stage.tsx- Server component wrapper for individual stagesapps/web/components/mdx/activity-stage-client.tsx- Client component for stage UI and completionapps/web/components/mdx/activity-stage-context.tsx- React context provider with SWR for state managementapps/web/components/mdx/activity-stage-instructor.tsx- Instructor control panel with timers and completion tracking
Test Page:
apps/web/app/testing/components/activity-stages/page.tsx- Test page at/testing/components/activity-stages
E2E Tests:
testing/e2e/tests/activity-stages.spec.ts- 15 tests covering student and instructor flows
Files Modified
CLAUDE.md- Added documentation for the "donut" patternpackages/db/src/schema/index.ts- Added export for activity stages schemaapps/api/src/index.ts- Registered new routesapps/web/mdx-components.tsx- RegisteredActivityStagesandActivityStagecomponentsapps/web/components/mdx/group-activity-client.tsx- Added integration to trigger stage completion when group formsapps/api/src/routes/e2e.ts- Added cleanup foractivityStageCompletionstesting/e2e/playwright.config.ts- Added activity-stages test project
Usage Example
<ActivityStages meetingSlug="2026-01-27-ethics">
<ActivityStage label="Opening Discussion" time="5m">
Discuss with your neighbor...
</ActivityStage>
<ActivityStage label="Group Formation" time="3m" autoAdvance>
<GroupActivity id="ethics-groups" group="ethics-meeting-1">
Partner content here...
</GroupActivity>
</ActivityStage>
<ActivityStage label="Case Study" time="15m">
Review and discuss...
</ActivityStage>
</ActivityStages>
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
Let me check the new and modified files for any debugging statements or unnecessary logging.
<thinking> I found two console.error statements: 1. `apps/web/components/mdx/group-activity.tsx:68` - This is a pre-existing one (not in my changes) 2. `apps/web/components/mdx/activity-stages.tsx:118` - This is in my new fileThe one in activity-stages.tsx is similar to the existing one in group-activity.tsx - both are server-side error logging for API fetch failures, which is appropriate for debugging server-side issues. Since the existing codebase has this pattern, it's consistent.
Let me confirm there are no other debugging statements in my new files and then proceed with the commit. </thinking>
The console.error in activity-stages.tsx:118 is for server-side error logging, matching the existing pattern in group-activity.tsx:68. This is appropriate for debugging server-side fetch failures.
Here's a summary of changes:
Modified Files (7):
CLAUDE.md- Added documentation for the donut patternapps/api/src/index.ts- Registered new API routesapps/api/src/routes/e2e.ts- Added cleanup for activity stage completionsapps/web/components/mdx/group-activity-client.tsx- Added activity stage integrationapps/web/mdx-components.tsx- Registered ActivityStages and ActivityStage componentspackages/db/src/schema/index.ts- Added export for activity stages schematesting/e2e/playwright.config.ts- Added activity-stages test project
Untracked Files (10 new files):
packages/db/src/schema/activity-stages.ts- Database schemaapps/api/src/routes/activity-stages.ts- Student API routesapps/api/src/routes/admin/activity-stages.ts- Admin API routesapps/web/components/mdx/activity-stage.tsx- Server componentapps/web/components/mdx/activity-stages.tsx- Container server componentapps/web/components/mdx/activity-stage-client.tsx- Client componentapps/web/components/mdx/activity-stage-context.tsx- React context providerapps/web/components/mdx/activity-stage-instructor.tsx- Instructor controlsapps/web/app/testing/components/activity-stages/page.tsx- Test pagetesting/e2e/tests/activity-stages.spec.ts- E2E tests
Committed successfully: b1b4761 - Add ActivityStages MDX component for sequential activity progression
17 files changed with 1958 insertions. The working tree is clean.
OK! Next, I'd like to be able to compose the group and activity stages around the idea of roles. For example, a group might establish two roles: driver and observer. Then, for each phase inside that group, we might have students switch roles. We might want to only render the stage content if the student is a driver for that stage, and instruct the observer to observe. We'd have roles rotate within the group at each stage. Does that make sense? Can you provide an example of how the markup for that might look?
Let me first read the existing plan file and then understand the current implementation to propose a good design for roles.
This is a great idea! Let me think through how the markup might look. Here are a few approaches to consider:
Option A: Roles on GroupActivity, rotation on stages
<ActivityStages meetingSlug="2026-01-27-pair-programming">
<ActivityStage label="Find a Partner" autoAdvance>
<GroupActivity id="pp-groups" group="pair-programming-1" roles={["driver", "observer"]}>
You found a partner!
</GroupActivity>
</ActivityStage>
<ActivityStage label="Exercise 1" time="10m">
<RoleContent role="driver">
**You are the driver.** Open the editor and implement the `factorial` function.
</RoleContent>
<RoleContent role="observer">
**You are the observer.** Watch your partner code and offer suggestions.
</RoleContent>
</ActivityStage>
<ActivityStage label="Exercise 2" time="10m" rotateRoles>
<!-- rotateRoles swaps driver/observer from previous stage -->
<RoleContent role="driver">
**Your turn to drive!** Now implement the `fibonacci` function.
</RoleContent>
<RoleContent role="observer">
**Now you observe.** Help your partner think through the approach.
</RoleContent>
</ActivityStage>
</ActivityStages>
Option B: Explicit role assignment per stage
<ActivityStages meetingSlug="2026-01-27-pair-programming">
<ActivityStage label="Find a Partner" autoAdvance>
<GroupActivity id="pp-groups" group="pair-programming-1" roles={["driver", "observer"]}>
You found a partner!
</GroupActivity>
</ActivityStage>
<ActivityStage label="Exercise 1" time="10m" roles={{ driver: 0, observer: 1 }}>
<!-- Member 0 = driver, Member 1 = observer -->
<RoleContent role="driver">...</RoleContent>
<RoleContent role="observer">...</RoleContent>
</ActivityStage>
<ActivityStage label="Exercise 2" time="10m" roles={{ driver: 1, observer: 0 }}>
<!-- Swapped: Member 1 = driver, Member 0 = observer -->
<RoleContent role="driver">...</RoleContent>
<RoleContent role="observer">...</RoleContent>
</ActivityStage>
</ActivityStages>
Key design questions:
-
Role assignment strategy - When a group forms, how are roles initially assigned?
- First to join = first role (driver), second = second role (observer)?
- Random assignment?
- Student chooses?
-
Rotation behavior - How should
rotateRoleswork?- Simple swap for pairs (driver↔observer)?
- Round-robin for larger groups?
-
Content visibility - What does the other role see?
- Nothing (content hidden)?
- Their own role-specific instructions?
- Both, but their role highlighted?
Which approach feels more natural for your use case? And any preferences on the design questions above?
Wait, I think that the GroupActivity should have the activity stages inside it, no? That way that can obtain the GroupActivity state from their parent via context.
You're right! That's a much cleaner design. The GroupActivity provides the group context (members, roles), and the ActivityStages operate within that context:
<GroupActivity id="pp-groups" group="pair-programming-1" roles={["driver", "observer"]}>
<!-- This content only shows after group forms -->
<ActivityStages meetingSlug="2026-01-27-pair-programming">
<ActivityStage label="Exercise 1" time="10m">
<RoleContent role="driver">
**You are the driver.** Open the editor and implement `factorial`.
</RoleContent>
<RoleContent role="observer">
**You are the observer.** Watch and offer suggestions.
</RoleContent>
</ActivityStage>
<ActivityStage label="Exercise 2" time="10m" rotateRoles>
<RoleContent role="driver">
**Your turn to drive!** Now implement `fibonacci`.
</RoleContent>
<RoleContent role="observer">
**Now you observe.** Help your partner think through it.
</RoleContent>
</ActivityStage>
</ActivityStages>
</GroupActivity>
This way:
- GroupActivity forms the group, assigns initial roles, provides context
- ActivityStages (children) only render after group is complete
- ActivityStage can read group context and optionally
rotateRoles - RoleContent checks the user's current role from context and conditionally renders
The context chain would be: GroupActivity → ActivityStages → ActivityStage → RoleContent
Does this structure match what you had in mind?
I think that makes more sense. What do you think?
I agree, this design is better for several reasons:
-
Natural data flow - Group context (members, roles) flows down to stages and role content. No need to wire things up manually.
-
Automatic gating - GroupActivity already hides children until the group forms. No changes needed there.
-
Compositional - Each component does one thing:
GroupActivity- forms group, assigns roles, provides contextActivityStages- manages stage progression (works with or without group)RoleContent- conditionally renders based on role context
-
Clean mental model - "Within this group, do these stages, with these role-specific instructions"
-
Instructor view - Instructors could see all role content labeled, so they understand what each student sees
The only thing I'd add: RoleContent should probably still render its content (maybe dimmed or labeled) if there's no group context, so the MDX is previewable and instructors can see everything.
Want me to plan the implementation? The main pieces would be:
- Add
rolesprop to GroupActivity and store role assignments in the database - Create a group context that exposes current user's role
- Add
rotateRolesprop to ActivityStage that cycles role assignments - Create
RoleContentcomponent that conditionally renders based on role
Well RoleContent is inside GroupActivity, which won't render children until the group forms. But this is fine. We'll need different visibility for instructors anyway, and we can work on that later. From the previous example, I liked the explicit rotateRoles on the activityStage, although that should only work if it's in a GroupContext. I guess one thing to keep in mind is that stages should work without the group wrapper. Actually, can we be more clear about the composition rules here for both activity and group components?
Great question. Let me lay out the composition rules clearly:
Composition Rules
ActivityStages (standalone)
<ActivityStages meetingSlug="...">
<ActivityStage label="Step 1">...</ActivityStage>
<ActivityStage label="Step 2">...</ActivityStage>
</ActivityStages>
- Class-wide stage progression
- Instructor controls advancement
- No roles, no
rotateRoles, noRoleContent
GroupActivity (standalone)
<GroupActivity id="..." group="...">
Content shown after group forms
</GroupActivity>
- Group formation only
- No roles (or roles could be assigned but never used)
- No stages, no rotation
GroupActivity wrapping ActivityStages
<GroupActivity id="..." group="..." roles={["driver", "observer"]}>
<ActivityStages meetingSlug="...">
<ActivityStage label="Exercise 1">
<RoleContent role="driver">...</RoleContent>
<RoleContent role="observer">...</RoleContent>
</ActivityStage>
<ActivityStage label="Exercise 2" rotateRoles>
<RoleContent role="driver">...</RoleContent>
</ActivityStage>
</ActivityStages>
</GroupActivity>
- Full feature set: group + roles + stages + rotation
rotateRolescycles role assignments for that stageRoleContentrenders based on user's current role
ActivityStages wrapping GroupActivity (existing pattern)
<ActivityStages meetingSlug="...">
<ActivityStage label="Find Partner" autoAdvance>
<GroupActivity id="..." group="...">
Partner found!
</GroupActivity>
</ActivityStage>
<ActivityStage label="Discussion">
<!-- No group context here - GroupActivity ended -->
</ActivityStage>
</ActivityStages>
- Stages control class progression
- Group forms within a stage but context doesn't persist to other stages
- No roles - use the other pattern for roles
Invalid/Warning cases
rotateRoleson ActivityStage without GroupActivity ancestor → ignored (or console warning)RoleContentwithout GroupActivity ancestor → render nothing? render all? warning?
Does this capture the composition rules correctly? And for RoleContent outside a group context - should it render nothing, render its content anyway (for testing/previewing), or show a warning message?
I guess I'm more thinking: It looks like ActivityStage has to be in ActivityStages, correct? And RoleContent must be inside GroupActivity, correct?
Yes, exactly. The strict containment rules are:
| Component | Must be inside |
|---|---|
ActivityStage | ActivityStages |
RoleContent | GroupActivity |
And the flexible parts:
| Component | Can be... |
|---|---|
ActivityStages | Standalone, OR inside GroupActivity |
GroupActivity | Standalone, OR inside ActivityStage, OR wrapping ActivityStages |
So there are really two valid patterns for roles:
Pattern 1: Group wraps Stages (roles + rotation)
<GroupActivity roles={["driver", "observer"]}>
<ActivityStages>
<ActivityStage rotateRoles>
<RoleContent role="driver">...</RoleContent>
</ActivityStage>
</ActivityStages>
</GroupActivity>
Pattern 2: Stage contains Group (no roles outside group)
<ActivityStages>
<ActivityStage>
<GroupActivity>
<!-- RoleContent only valid here, inside GroupActivity -->
</GroupActivity>
</ActivityStage>
<ActivityStage>
<!-- No RoleContent here - outside GroupActivity -->
</ActivityStage>
</ActivityStages>
Does that capture the rules correctly?
Gotcha. There is some crossover here, however, since rotateRoles communicates with GroupActivity, correct? That's fine, just want to be clear.
Right. ActivityStage consumes from two contexts:
- ActivityStages context (required) - stage index, visibility, completion state
- GroupActivity context (optional) - for
rotateRolesto actually rotate roles
So when you have:
<GroupActivity roles={["driver", "observer"]}>
<ActivityStages>
<ActivityStage rotateRoles>
...
</ActivityStage>
</ActivityStages>
</GroupActivity>
The ActivityStage reaches "up" to both:
- Its parent
ActivityStagesfor stage mechanics - Its ancestor
GroupActivityfor role rotation
And rotateRoles without a GroupActivity ancestor would just be ignored (no roles to rotate).
This cross-context communication is fine - React contexts are designed for this. The ActivityStage client component would do something like:
const stagesContext = useActivityStages(); // required
const groupContext = useOptionalGroupActivity(); // optional
if (rotateRoles && groupContext) {
// trigger role rotation
}
Is that the interaction model you had in mind?
Yes! And I like the RoleContent wrapper. That makes things very explicit. And let's make sure that rotateRoles handles cases where the number of students exceeds the number of roles. Actually, this probably requires configuration. Maybe roles={{driver: 1, observer: Inf}} or something like that, to express that there should be only one driver but can be many observers? That should be configured separately from the target group size.
Good catch. The role configuration needs to express cardinality. A few syntax options:
Option A: Object with counts
<GroupActivity
size={3}
roles={{ driver: 1, observer: "*" }}
>
1= exactly one"*"= fill remaining slots
Option B: More explicit
<GroupActivity
size={3}
roles={{ driver: { count: 1 }, observer: { count: "rest" } }}
>
Rotation behavior for 3-person group with 1 driver:
| Stage | Alice | Bob | Carol |
|---|---|---|---|
| 1 | driver | observer | observer |
| 2 (rotate) | observer | driver | observer |
| 3 (rotate) | observer | observer | driver |
The single-count role (driver) rotates through members; the "*" role (observer) fills the rest.
Edge cases to handle:
- What if
driver: 2, observer: "*"and group has only 2 people? Both are drivers, no observers? - What if total role counts exceed group size? Error?
I lean toward Option A for simplicity:
<GroupActivity size={3} roles={{ driver: 1, observer: "*" }}>
Does that syntax work for you? And how strict should validation be for mismatched sizes?
Yeah, I like that.
Let me write up the implementation plan.
I've written up the implementation plan. The key points:
- Role syntax:
roles={{ driver: 1, observer: "*" }} - Composition:
GroupActivitywrapsActivityStagesfor full role support - Rotation:
rotateRolesonActivityStagecycles the limited-count role through members - RoleContent: Simple conditional rendering based on user's current role
- Database: Store role config on groups, track rotation offset per stage