feat(autoscan): make source setup descriptor-driven and self-contained

Autoscan setup asked operators to hold five concepts — plugin, capability,
source, connection, delivery mode — before anything scanned, spread across
four tabs. Sources never stated which library they fed, so a misconfigured
one ran cleanly and silently did nothing.

The admin UI also hardcoded two plugin identities: SourcesPanel.tsx carried
33 references to silo.autoscan.cephfs / silo.autoscan.arr-webhook plus a
bespoke CephFS config editor. A third-party scan-source plugin could render
no configuration UI at all without patching silo-server.

Host side, add a ScanSourceDescriptor read from capability manifest metadata:
delivery modes, connection requirement, connection kinds, and the per-source
config form. Capabilities that declare nothing resolve to poll + optional
connection — exactly the pre-descriptor behavior, so existing installs are
unaffected. Compatibility descriptors for the two first-party plugins live in
one file with a clear exit: the manifest always wins, so a plugin takes
ownership by publishing its own.

UI side, the Add-source flow builds its steps from the descriptor rather than
from plugin ids, so a single-mode source is never asked how changes arrive and
a credential-free one never sees the connection step. Connections are created
inline (previously a dead end that forced cancelling out of the dialog), and
webhook setup finishes in one place: mappings, then the URL with the exact
Sonarr/Radarr triggers the host actually parses.

Mapping rows seed from library paths collapsed to their common ancestor per
mount point. Verified against a real install: 96 library paths become 2 rows.
Rewrites match by longest prefix at a segment boundary, so one row per mount
covers everything beneath it.

Fold the Connections and Settings tabs into an Advanced section (4 tabs -> 2);
old ?tab= links land on Sources with it expanded. Source rows now name the
libraries they feed and warn when a source can never resolve one.

API changes are additive within /api/v1: new optional fields on
/autoscan/scan-source-plugins only.
This commit is contained in:
Quick
2026-07-29 14:52:16 -04:00
parent a7601028d9
commit 251282186e
22 changed files with 3055 additions and 396 deletions
+32
View File
@@ -2107,10 +2107,35 @@ export interface AutoscanSourcesResponse {
sources: AutoscanSource[];
}
/** Whether a scan source needs upstream credentials to reach its provider. */
export type AutoscanConnectionRequirement = "none" | "optional" | "required";
/**
* The setup contract for one scan source, declared by its plugin manifest and
* resolved host-side. The Add-source flow builds its steps from this rather
* than branching on plugin ids, so a new plugin configures itself.
*
* The host always populates this, substituting defaults (poll + optional
* connection) for capabilities that declare nothing.
*/
export interface AutoscanScanSourceDescriptor {
delivery_modes: AutoscanDeliveryMode[];
connection: AutoscanConnectionRequirement;
connection_kinds?: string[];
/** Plugin already emits Silo-native paths, so path rewrites can be skipped. */
emits_native_paths?: boolean;
summary?: string;
icon_url?: string;
/** Per-source config fields, rendered by the shared plugin SchemaForm. */
config_form?: PluginAdminForm;
}
export interface AutoscanAvailableSource {
plugin_id: string;
capability_id: string;
display_name: string;
description?: string;
descriptor: AutoscanScanSourceDescriptor;
}
export interface AutoscanAvailableSourcesResponse {
@@ -3442,6 +3467,13 @@ export interface PluginAdminFormField {
show_when?: PluginAdminFormCondition[];
validation?: PluginAdminFormValidation;
exclusive_group_field?: string;
/**
* Names a host-known value this field can be populated from in one click.
* Used by autoscan source config so a path field can be filled from Silo's
* own library paths without the UI knowing which plugin owns it. Unknown
* values render no action.
*/
fill_from?: string;
}
export interface PluginCapability {