What's wired
55 of 74 backend routes have a frontend caller. The 19 that do not, which of them actually matter, and the contract gaps found alongside them.
Read from source on 2026-09-09 at backend@a164684 and frontend@ed55491, both on staging. backend@a164684 is still the tip; the frontend has since advanced to f2c1ead, which merges main into staging — recount before treating these numbers as current.
The one finding that matters: from the UI, every work item in the app is create-only. Nine of the 19 uncalled routes are the edit-and-delete half of projects, tasks, and comments. The backend has them; no frontend code path calls them.
| Routes declared | 74 |
| Routes with a frontend caller | 55 |
| Routes with none | 19 |
The 19
Work-item CRUD — 9 routes
GET /api/projects/:id
PATCH /api/projects/:id
DELETE /api/projects/:id
GET /api/tasks/:id
PATCH /api/tasks/:id
PATCH /api/tasks/:id/position
DELETE /api/tasks/:id
PATCH /api/tasks/:taskId/comments/:commentId
DELETE /api/tasks/:taskId/comments/:commentIdThis is the real gap. From the UI, every work item in the app is create-only:
- Tasks can be created and assigned, but never moved between columns, edited, or deleted.
- Projects can be created and reordered, but not renamed or removed.
- Comments can be posted, but not edited or removed.
All nine routes exist on the backend and carry guards. This is missing UI, not missing API.
Audit timeline — 4 routes
GET /api/timeline/user
GET /api/timeline ADMIN
GET /api/timeline/resource/:type/:id ADMIN
GET /api/timeline/summary ADMINThe audit timeline is fully built — indexed on (action, createdAt) and (user, createdAt), with a retention cron — and entirely unsurfaced. The SPA’s /timeline route renders tasks instead.
Organization read and rename — 2 routes
GET /api/organizations/:id
PATCH /api/organizations/:idOrg detail is read out of the list response instead. The consequence is that renaming an organization is unreachable from the UI, even for an OWNER.
Superseded and infrastructure — 4 routes
GET /health probes only, outside the /api prefix
GET /api/seeder superseded by the SeedPlans migration
GET /api/assets/logs per-user variant of a route that is wired
GET /api/assets/:id asset detail; the SPA reads assets out of the
collection listing insteadExpected. None of these need a frontend caller today — though GET /api/assets/:id is the natural backing for an asset detail view, and its absence is why the SPA has no way to open a single asset by ID.
Wired but unreachable
Two things pass the “has a caller” test and still do not work end to end.
getAssetVersions is never called
api/tala.ts exports it; nothing imports it. restoreAssetVersion is called from LibraryPage. So version restore ships without version listing — the UI restores a version number the user has no way to see.
Collection favourites never reach the server
useCollections.toggleFavorite writes to localStorage and returns. PATCH /api/collections/:id/favorite exists, and api/tala.ts exports favoriteCollection(). Neither is called.
Asset favourites do hit the server, so the two behave differently on a second device.
Contract inconsistencies
| Where | Issue |
|---|---|
PATCH /api/assets/:id |
Requires collectionId in the body even though assets no longer require a collection. The SPA always sends one to work around it. |
| Paginated responses | Carry no total or pageCount. Page numbers, a result count, and a last-page control cannot be built against the current envelope. |
POST /api/upload/complete |
No idempotency key; a retry can duplicate an asset. |
Page coverage
| Page | LOC | State |
|---|---|---|
LibraryPage |
475 | live |
WorkspacePage |
454 | live |
HomePage |
283 | live |
ResetPasswordPage |
246 | live |
LoginPage |
233 | live |
SignUpPage |
230 | live |
TimelinePage |
211 | live |
VerifyEmailPage |
129 | live |
LoadingPage |
118 | live |
TeamsPage |
73 | live |
ForgotPasswordPage |
66 | live |
BillingPage |
49 | plans only, no billing |
InvitationPage |
40 | live |
WorkspaceDetailPage |
18 | thin wrapper |
ProfilePage |
10 | stub |
SettingsPage |
3 | placeholder copy |
The counts above were read from source rather than from an existing document, because the existing documents disagree with the code in about ten places — see Documentation drift for the full list of corrections.
