---
title: What's wired
description: 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

<Accordion>
  <AccordionItem title="Work-item CRUD — 9 routes" icon="alert-triangle" defaultOpen>

```text
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/:commentId
```

**This 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.

  </AccordionItem>
  <AccordionItem title="Audit timeline — 4 routes">

```text
GET /api/timeline/user
GET /api/timeline                        ADMIN
GET /api/timeline/resource/:type/:id     ADMIN
GET /api/timeline/summary                ADMIN
```

The 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.

  </AccordionItem>
  <AccordionItem title="Organization read and rename — 2 routes">

```text
GET   /api/organizations/:id
PATCH /api/organizations/:id
```

Org 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`.

  </AccordionItem>
  <AccordionItem title="Superseded and infrastructure — 4 routes">

```text
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 instead
```

Expected. 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.

  </AccordionItem>
</Accordion>

## 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 |

:::success[Mock data is gone; what is left is unbuilt]
Page components went from roughly 4,800 lines, most of it mock-driven, to 2,638 lines in total. What remains is genuinely unbuilt rather than faked, and `noSyntheticData.test.ts` guards against a regression.
:::

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](/status/drift) for the full list of corrections.
