Files
officer c11f45c033 soulseek, extracted from the platform as a plugin
Seventh plugin out, and the first that was already dead. Soulseek had been
switched off since 2026-08-13: hono.ts had the router import and its mount
commented out, so createSidecarProxy never ran, registerProxiedPrefix was never
called, the slskd:server announcement fired into a void, and officer-slskd was
not in the ecosystem at all. The permission entry had been reduced to api: [],
which is why assertPermissionTotality was satisfied and the server booted
clean. The UI rendered perfectly and every request 404'd.

So this is a resurrection as well as a move.

The mount is now /api/soulseek, derived via mountPrefix from the appName — all
47 frontend call sites repointed from /slskd/. The credential key in
service_connections stays 'slskd': it names the daemon, not the route. Those
two strings differ on purpose and conflating them reads the wrong row.

The connection stays in core service_connections, as transmission does, so
'slskd' remains in a platform union this plugin cannot extend. Recorded in
PLUGIN.md. Unlike transmission this plugin does own a schema: favourites and
the two halves of the cached browse tree, which slskd has no concept of. Those
three tables had been dropped from the drizzle barrel, so bun db:push would
have deleted them on any server holding data.

313 palette classes converted to the semantic tone set, including a shared
PHASE_TONE the three transfer views previously each copied.
2026-08-19 02:55:02 +00:00

95 lines
5.2 KiB
Markdown

# Soulseek — the extraction record
Extracted from the platform on **2026-08-19**, the seventh plugin out, after transmission.
`plugins/EXTRACTING-A-PLUGIN.md` is the runbook.
## This one was a resurrection, not a move
Every other extraction so far moved working code. **Soulseek was already dead when it left**, and had
been since 2026-08-13:
- `src/servers/hono.ts` had both the import and the `PROTECTED_MOUNTS` entry commented out
(`// plugin — switched off 2026-08-13`), so nothing mounted `/api/slskd`.
- Because `api/slskd/router.ts` was therefore imported by nobody, `createSidecarProxy` never ran — so
`registerProxiedPrefix('/api/slskd')` was never called and the `slskd:server` port announcement fired
into a void.
- `officer-slskd` was not in `ecosystem.config.cjs` at all, so the sidecar was not even started.
- The permission entry had been reduced to `api: []`, which is why `assertPermissionTotality` was
satisfied and the server booted cleanly. **The UI rendered perfectly and every request 404'd.**
That last point is the one worth keeping. The boot check is a totality check, not a liveness check: it
proves every mounted prefix has a permission, and is silent about a feature that mounts nothing at all.
A screen whose panels all fail is invisible to it.
There was also a live data hazard, unrelated to the routing: `src/databases/officer_db/src/schema.ts`
had `export * from './soulseek/schema'` commented out, so the three tables were **not in the drizzle
barrel** — `bun db:push` would have dropped `soulseek_favorites`, `soulseek_browse_snapshots` and
`soulseek_browse_dirs` on any server holding them. `scripts/rebuild-soulseek-tree.ts` documented the
workaround rather than the problem. Moving the schema into the plugin closes that.
## What moved
| Was | Is now |
| ------------------------------------------------------- | --------------- |
| `src/servers/sidecar/slskd/*` (6 files) | `sidecar/*` |
| `src/servers/api/slskd/router.ts` | `api/router.ts` |
| `src/databases/officer_db/src/soulseek/*` (3 files) | `db/*` |
| `src/workspaces/officerdev/src/apps/Soulseek/*` (20 files) | `web/*` |
| `src/apps/officer-web/Screens/Dashboard/Soulseek/*` | `web/layout.ts` |
| `public/slskd.png` | `icon.png` |
| `scripts/rebuild-soulseek-tree.ts` | deleted |
## The prefix changed, and that is not cosmetic
The mount moved from `/api/slskd` to `/api/soulseek`, because `mountPrefix` derives it from the
**appName** and this plugin is `soulseek`. Three consequences, all handled here:
1. All 47 frontend call sites were repointed from `/slskd/...` to `/soulseek/...`.
2. `useServiceConnection('slskd')` became `useServiceConnection('soulseek')` — that hook builds
`` `/${service}/_config` ``, so its argument is the **mount name**, not the credential key.
3. The credential key in `service_connections` stays **`'slskd'`**. It names the daemon, not the route.
Point 3 is the one that will trip somebody. `service` in the database and the URL segment are now
different strings on purpose, and conflating them silently reads the wrong row.
## The part that is not self-contained
**The connection lives in `service_connections`, a CORE table** — the same one transmission uses.
`'slskd'` is a member of the platform's `ServiceName` union, which a plugin cannot extend.
Deliberate, and the table says so in its own comment: it exists for *"services the owner has exactly ONE
of"*. Nobody has two Soulseek daemons, so there is no label, no `is_active` and no switcher. It also
gives encryption at rest for free, which a private table would have to reimplement.
The cost, plainly: **this plugin depends on a core facility it does not own.** If `service_connections`
changes shape, this breaks with it.
Unlike transmission, this plugin **does** declare a schema — the connection is core, but favourites and
the cached browse tree are its own.
## Fixtures this broke, and what replaced them
`slskd` was a canonical app-store test fixture in three files, the third extraction in a row to break
them. `installer.test.ts` now builds a **literal** fixture rather than reaching into the catalogue:
```ts
const noMemberConcept = { ...byId('jellyfin')!, id: 'test-single-tenant', members: 'none' as const };
```
That test pins `planSteps`, which is installer logic. Binding it to catalogue contents is why it broke
three times; no catalogue entry is both `provisioned` and `members: 'none'` any more, so there was
nothing left to borrow.
`catalogue.test.ts` and `members.test.ts` were re-anchored to `vnc`/`notify`/`gitea`, which are real
remaining single-tenant entries.
## Not carried over
`composeTemplate: 'slskd'` pointed at a template directory containing **only `assets/icon.png` and no
`docker-compose.yaml`**, so the catalogue's `modes: ['existing', 'provisioned']` was already unbackable.
Like photos and transmission, this plugin is **point-at-your-own-daemon only** until plugin Docker
provisioning exists.
The SignalR hub proxy (`/hub/*`) was a TODO in the old sidecar and never built. Live search results
still arrive by polling.