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.
95 lines
5.2 KiB
Markdown
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.
|