Inherits the federation-editor spec; adds RemoteClientKind enum (catacomb | peertube), kind selector + conditional username field in both editors, kind in the API, kind-branched test-connection, and a phase-3 stopgap for browsing PeerTube remotes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.4 KiB
Kind-aware remote editor (phase 2)
Date: 2026-07-10 Status: Approved design, ready for implementation plan Scope: Desktop GUI + web server. Sub-project 2 of the PeerTube federation work.
Context
Phase 2 of three (see 2026-07-10-peertube-client-backend-design.md §Context).
Phase 1 (backend PeerTubeClient + RemoteKind/username config) is done.
This phase adds the in-UI editor to manage remotes of both kinds, applied
live.
Inherits 2026-07-10-federation-remote-editor-design.md wholesale for the
unchanged editor mechanics — read it first. Unchanged from that spec:
WebState.remotesbecomes astd::sync::RwLock<…>; browse endpoints read-lock, the editor save write-locks (live-apply).- Dedicated
/api/remotes/*endpoints (not folded intoSettingsPayload): extendedGET, newPUT(whole-list replace), newPOST /api/remotes/test. - Web passwords are masked / write-only; blank on save keeps the stored secret,
matched by URL (
merge_remote_passwords, pure + unit-tested). Desktop shows passwords in the clear (local). - Config source of truth is
state.config(state.config.lock_recover()); save viaconfig.save; on save error, don't swap the live list; drop the config lock before taking the remotes lock. - After any edit, both UIs refetch the list and clear the current selection.
This document specifies only the kind-aware deltas.
Delta 1 — Multi-kind live storage
New enum in src/remote.rs:
pub enum RemoteClientKind {
Catacomb(RemoteClient),
Peertube(crate::peertube::PeerTubeClient),
}
impl RemoteClientKind {
pub fn from_section(cfg: &crate::config::RemoteSection) -> Self {
match cfg.kind {
crate::config::RemoteKind::Catacomb => Self::Catacomb(RemoteClient::new(cfg)),
crate::config::RemoteKind::Peertube => Self::Peertube(crate::peertube::PeerTubeClient::new(cfg)),
}
}
pub fn name(&self) -> &str {
match self { Self::Catacomb(c) => &c.name, Self::Peertube(p) => &p.name }
}
pub fn kind(&self) -> crate::config::RemoteKind {
match self {
Self::Catacomb(_) => crate::config::RemoteKind::Catacomb,
Self::Peertube(_) => crate::config::RemoteKind::Peertube,
}
}
}
App.remotes: Vec<Arc<RemoteClientKind>>(desktop; mutable App, no lock).WebState.remotes: RwLock<Vec<Arc<RemoteClientKind>>>(web; per inherited spec). Built at startup withRemoteClientKind::from_sectionper config remote.- Both fronts rebuild the whole
Vecon editor save.
RemoteClient.name must be pub (it already is); PeerTubeClient.name is pub
(phase 1).
Delta 2 — Browse dispatch (phase-2 stopgap)
The existing browse call-sites match on the enum:
- Web
get_remote_library(positional:id): read-lock; match the entry:Catacomb(c)→c.library_json()(unchanged behavior).Peertube(_)→501 Not Implemented/ body"PeerTube browsing arrives in phase 3".
- Desktop
start_remote_fetch(idx): match the entry:Catacomb(c)→ spawn the existingc.library()fetch (unchanged).Peertube(_)→ setremote_status = "PeerTube browsing arrives in a later update", no fetch.
So a PeerTube remote is fully manageable (add/edit/test/save) in phase 2; browsing it is phase 3. Catacomb browsing is untouched.
Delta 3 — Editor UI
The inherited "Federation peers" Settings section, each row gaining:
- a kind selector — Catacomb / PeerTube (
egui::ComboBoxdesktop;<select>web); - a username field, rendered only when kind = PeerTube;
- (unchanged) name, url, password (masked web / clear desktop), Test, Remove.
Desktop edits self.config.remotes (each RemoteSection now carries
kind/username); on save rebuild self.remotes via from_section and reset
remote_selected/remote_library. Web edits a JS array and saves via PUT.
Delta 4 — Web API
On top of the inherited endpoints:
GET /api/remotesresponse gainskind(string,"catacomb"/"peertube"), keepshas_password—[{ id, name, url, kind, has_password }].PUT /api/remotesentry shape is{ name, url, kind, username, password }.merge_remote_passwords(URL-keyed) unchanged; the rebuilt live list usesRemoteClientKind::from_sectionper mergedRemoteSection.POST /api/remotes/testbody{ url, kind, username, password }. Build the matching client kind (resolving a blank password from the stored remote by URL, per the inherited merge) and check reachability onspawn_blocking:- catacomb →
RemoteClient::library_json()ok →{ ok: true }(optionally a channel count from the parsed library); - peertube →
PeerTubeClient::list_channels()→{ ok: true, channels: N }; - failure →
{ ok: false, error }.
- catacomb →
Delta 5 — Test-connection
Branches on kind as in Delta 4. Desktop runs it on a background thread
(mirroring start_remote_fetch), delivering { ok, detail } over an mpsc
channel drained in update(); the PeerTube arm calls list_channels().
Error handling
Inherited. Additionally: a PeerTube test against a bad instance/credentials
fails at list_channels()/OAuth with a descriptive message; the remote can
still be saved (peer may be down at edit time).
Testing
- Unit (
remote.rs):RemoteClientKind::from_sectionbuilds theCatacombvariant for a default section andPeertubeforkind = peertube;name()/kind()report correctly. - Unit (inherited):
merge_remote_passwords(URL-keyed keep-on-blank). - Integration (
tests/api.rs):PUT /api/remoteswith one catacomb and onekind:"peertube"entry →GET /api/remotesreflects both with correctkindandhas_password; a removed entry disappears; a blank-password edit of an existing entry keepshas_passwordtrue. (Live PeerTube reachability needs a real instance → manual.) - Manual: in both UIs, add a catacomb peer and a public PeerTube remote
(e.g.
https://framatube.org/c/<channel>); Test each; save; confirm catacomb browse still works and clicking the PeerTube remote shows the phase-3 stopgap; confirm the webGETnever returns plaintext passwords and blank-save preserves them.
What phase 3 consumes
The RemoteClientKind::Peertube arm's stopgap is replaced by the two-level
lazy browse (list_channels → channel_videos) + inline play (video_media) +
the per-video Archive action (watch_url → downloader).