Files
soulseek/PLUGIN.md
T
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

5.2 KiB

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:

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.