Skip to content

Add project pod workspace#219

Open
MubaiHua wants to merge 1 commit intomainfrom
feature/project-pod-workspace
Open

Add project pod workspace#219
MubaiHua wants to merge 1 commit intomainfrom
feature/project-pod-workspace

Conversation

@MubaiHua
Copy link
Copy Markdown

This PR introduces a first-pass project pod workspace.

What is included:

  • new project pod type in backend and shared types
  • dedicated project pod room with Chat and Tasks tabs
  • project metadata on pods (projectMeta)
  • richer task fields and structured updates/blockers
  • project pod creation/category entry points in the UI
  • dev-container self-healing for missing frontend/backend dev dependencies

Still pending:

  • dedicated Create Project Pod wizard
  • milestone model and milestone sidebar UI
  • task workflow cleanup to user-facing states like todo, in_progress, blocked, in_review, done
  • richer task detail actions for handoff/review/blockers
  • agent tool/runtime expansion for task progress and blocker updates
  • project timeline/activity view and stronger sidebar context
  • DB/index tuning once task volume and query patterns settle
  • documentation follow-up for the remaining rollout items

Verification:

  • targeted frontend tests for pod routing and pod creation UI
  • frontend test run for Pod.test.tsx after the dropdown fix

Comment thread backend/routes/pods.ts

router.get('/', auth, getAllPods);
router.post('/', auth, createPod);
router.patch('/:id', auth, updatePod);
Comment thread backend/routes/pods.ts

router.get('/', auth, getAllPods);
router.post('/', auth, createPod);
router.patch('/:id', auth, updatePod);
|| slugify(`${item.agentName}-${item.instanceId}`) === normalizedLabel
)) || null;
}

Comment on lines 241 to 264
@@ -153,6 +263,10 @@ router.post('/:podId', auth, async (req: AuthReq, res: Res) => {
}
}
Copy link
Copy Markdown
Contributor

@samxu01 samxu01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big PR, well-scoped feature. The data model for project + task is reasonable and the room/tab split into PodRoomRouter is a clean extension point. Main concerns are one real security bug and a few data-loss / consistency issues worth fixing before merge.

Please fix before merge:

  1. Stored XSS via keyLinks[].url (ProjectPodRoom.tsx:750, podController.ts updatePod). javascript: URIs render into <a href>. Needs scheme validation server-side and guard on render, plus noopener.
  2. Complete clobbers blocker history (tasksApi.ts:313). Only flip open / stamp resolvedAt; preserve reason/openedAt/openedBy.

Consistency / tech debt:

  1. updatePod ignores projectMeta.ownerIds (podController.ts:336). Schema advertises multi-owner, auth check only honors createdBy. Either wire ownerIds into the auth check or drop it from the schema.
  2. // @ts-nocheck on 1108-line ProjectPodRoom. Minimum: type room, task, podAgents from packages/types.
  3. projectMeta stored on every pod (Pod.ts:75). Schema defaults populate the subdoc on chat/study/games/dm too. Gate on type === 'project'.

Not posting inline but worth tracking:

  • Optimistic-message coalescing in the socket handler matches on content only — two identical messages in quick succession collapse. Pre-existing pattern elsewhere, not new here.
  • PATCH /:podId/:taskId accepts blocker as an opaque object in the allowed list. Mongoose enum validation will reject invalid severity, but nothing prevents callers from flipping openedAt/openedBy directly. Consider a dedicated /blocker endpoint that stamps those server-side.
  • frontend/start-dev.sh uses npm ci --only=production=false — the --only flag was deprecated in npm 7+ and this syntax is malformed. npm ci installs devDeps by default; just drop the flag.
  • enqueueTaskAssignedIfNeeded fallback lookup does a find({podId, status:'active'}) + in-memory slug match. Fine at current pod sizes; worth an index check if pods grow.
  • PodRoomRouter.tsx:14 — 14-line router is a good abstraction; future work should keep this stable when adding more pod types.

Marking as COMMENT; the XSS and blocker-clobber are real defects but contained.


Generated by Claude Code

{(room?.projectMeta?.keyLinks || []).length ? (
room.projectMeta.keyLinks.map((link, index) => (
<a key={`${link.url}-${index}`} href={link.url} target="_blank" rel="noreferrer" style={{ color: '#f8fafc', textDecoration: 'none' }}>
{link.label || link.url}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stored XSS. link.url is rendered into href= with zero scheme validation, and the backend updatePod stores the string as-is (only trims whitespace). Any pod editor can save javascript:alert(1) — every member who clicks gets XSS.

Fix: validate scheme server-side (allow http:/https:/mailto: only) and guard in the render:

const safeUrl = /^(https?|mailto):/i.test(link.url) ? link.url : '#';
<a href={safeUrl} target="_blank" rel="noopener noreferrer">...

Also add noopenernoreferrer alone doesn't cover older browsers for target="_blank".


Generated by Claude Code

$set: {
status: 'done',
completedAt: new Date(),
progressPercent: 100,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completing a blocked task $sets the entire blocker object to a fresh default — reason: null, waitingOn: null, openedAt: null, openedBy: null. That erases the audit trail of why/when the task was blocked.

Just flip open and stamp resolvedAt:

'blocker.open': false,
'blocker.resolvedAt': new Date(),

Keep reason, waitingOn, openedAt, openedBy for history.


Generated by Claude Code

}

const requesterId = req.userId || req.user?.id || req.user?._id;
const isCreator = pod.createdBy.toString() === requesterId?.toString();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema introduces projectMeta.ownerIds (plural) but the auth check here only honors the original createdBy. A co-owner added via ownerIds gets 403 when they try to edit project metadata — they paid for the field but can't use it.

Either:

const isOwner = (pod.projectMeta?.ownerIds || []).some(
  (id) => id.toString() === requesterId?.toString()
);
if (!isCreator && !isOwner && !isGlobalAdmin) { ... }

or drop ownerIds from the schema until co-ownership is actually wired.


Generated by Claude Code

@@ -0,0 +1,1108 @@
// @ts-nocheck
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// @ts-nocheck on a 1108-line component that's the primary surface for a new feature is expensive tech debt from day one. The rest of the codebase is typed; this file will silently accept shape changes to room.projectMeta, task.blocker, task.assigneeType, etc., and the first signal of a break will be runtime.

Minimum ask: drop @ts-nocheck and at least type the props + the three main state shapes (room, task, podAgents) from packages/types/src/pod.ts. The any-tolerant handlers can stay loose for now; the data surface is what actually needs coverage.


Generated by Claude Code

Comment thread backend/models/Pod.ts
projectMeta: {
goal: { type: String, trim: true, default: '' },
scope: { type: String, trim: true, default: '' },
successCriteria: { type: [String], default: [] },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectMeta has field-level defaults but no conditional on type === 'project', so every pod (chat/study/games/dm/…) gets a populated projectMeta: { goal:'', scope:'', successCriteria:[], status:'planning', dueDate:null, ownerIds:[], keyLinks:[] } persisted.

Two small wins: save storage on non-project pods, and make the "is this a project?" check a simple field-presence test. Either mark the subdoc default: undefined and set it explicitly in the project branch of createPod, or gate with required: function() { return this.type === 'project'; }.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants