The two DETAIL reads get public routes — one work item, and one feature request
Opened by Zhu Yue ·
/p/* is not only tabs: it has two detail pages, and neither has a public endpoint.
app/(public)/p/[identifier]/items/[key]/page.tsx— one work item as the public surface shows it.publicProjectsService.getWorkItemDetail(:993) exists, is tested, and no route reaches it.app/(public)/p/[identifier]/requests/[requestKey]/page.tsx— one feature request with its public comment thread and its vote count.publicProjectsService.getRequestDetail(:899) exists, is tested, and no route reaches it.
Both were deleted by MOTIR-3951, whose service sweep kept the two methods precisely for this card. Add the two GET routes so the renderer on motir.co can fetch them.
Same shape as the reads beside them, and no deviation from it: capability gate first (publicSurfaceUnavailable()), getSession() for an optional actorUserId ?? null, one service call, ProjectNotFoundError / PublicRequestNotFoundError → 404 with no existence leak (the 404-not-403 posture the request routes already keep). HTTP layer only.
Read the two methods before choosing the paths and the parameters — the request detail is keyed by a request KEY on the page and the existing write routes are keyed by projectId + request id, so the route's identifier must match what the SERVICE actually takes rather than what the old page's URL happened to carry. State the mapping in the route's own comment.
The comment thread that hangs off the request detail is a READ here and a WRITE elsewhere. This card exposes what the page displays; POST /api/public-requests/{id}/comments and …/upvote already exist and are session-gated, and what becomes of them cross-origin is the amendment's and the act path's, not this card's. Do not touch them.
Contract entries are part of the deliverable, exactly as for the tab reads: two operations in lib/api/public/openapi/operations.ts, and both routes present to the cloud-gate and anonymous-posture enumerations without an exemption.
Acceptance criteria
- A public GET route returns the work-item detail projection for an anonymous caller on a public project, and
404 { code }for an unknown item, a non-public project, or an item the epic-privacy exclusion hides from a non-member. - A public GET route returns the feature-request detail projection — including its public comment thread and vote count as the service already returns them — with the same 404 posture.
- Both routes call
publicSurfaceUnavailable()before any session read and answer the gate's response withMOTIR_CLOUDunset. - Each route's comment records the identifier it takes and why (key vs id), read from the service signature rather than from the deleted page's URL.
lib/api/public/openapi/operations.tsdeclares both;contract-coverage,contract-drift,cloud-gate-totalityandanonymous-postureare green with no new exemption.- Neither route writes anything, and neither
POST /api/public-requests/{id}/upvotenor…/commentsis modified by this card. - No file outside
motir-coreis touched.
Context refs
motir-core/lib/services/publicProjectsService.ts—getRequestDetail(:899),getWorkItemDetail(:993)motir-core/lib/dto/publicProjects.ts·lib/dto/publicRequests.ts— the projections these returnmotir-core/app/api/public/p/[identifier]/items/route.ts— the sibling list read and its posture commentmotir-core/app/api/public/projects/[projectId]/requests/route.ts— how the request surface is keyed on the write sidemotir-core/tests/publicProjects/publicWorkItemDetail.test.ts·publicRequestDetail.test.ts— the service tests kept for this cardmotir-core/lib/api/public/openapi/operations.ts·motir-core/tests/api/public/- the two detail pages on motir.co — the consumer
Discussion
No comments yet.
Adding to this discussion signs you in on app.motir.co and brings you back to this request.