Files
wanderer/web/src/lib/stores/plugin_store.ts
slothful-vassal 485ec53f6d feat: add plugin system (#1034)
* feat: add plugin system

* fix db docker build

* fix hammerhead readme, add strava subscription news to docs

* fixes and sdk improvements

* fix: reduce Meilisearch load, debounce federation sync (#1012)

* optimize meili trail index

* several fixes

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032)

Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.56.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Release v0.19.2 (#1035)

* chore: release v0.19.2

* add changelog

---------

Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>

* speed up plugin sync and several small fixes

* concepts for security improvements and process stability

* improve concept

* security concept implemented

* remove insecure TLS

* worker concept implemented

* fixes and cleanup

* fixes

* docu

* mermaid, namings

* WASM plugin host improvements, plugin logging

* fix db migration

* Improve plugin config and category mapping UI

* fixes

* further fixes

* remove manual test sync

* fix db migration and strava mapping

* type added, UI improvements

* fix plugin card toggle clickable area

* optimize synch status card layout

* plugin type 'trails' instead of 'integration'

* session auth validation in UI

* fix komoot date and waypoints

* improve category mapping

* fix send to hammerhead: trail name

* plugin setup error handling improved

* fix review findings

* re-mapping added

* rename remote_category

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-06-22 15:00:44 +02:00

126 lines
4.3 KiB
TypeScript

import type { LocalizedTextMap, PluginProvider } from "$lib/models/plugin_provider";
import type { PluginSystemPlugin } from "$lib/models/plugin_system";
import { APIError } from "$lib/util/api_util";
import { derived, writable, type Readable, type Writable } from "svelte/store";
import {
pluginInstances,
plugin_instances_index,
} from "./plugin_instance_store";
export const pluginProviders: Writable<PluginProvider[]> = writable([]);
// True when the user has at least one enabled plugin instance whose plugin
// advertises the trail send capability. Used to gate the trail "send to" action.
export const hasSendCapablePlugin: Readable<boolean> = derived(
[pluginProviders, pluginInstances],
([$plugins, $instances]) => {
const enabledProviders = new Set(
$instances.filter((a) => a.enabled).map((a) => a.plugin_id),
);
return $plugins.some(
(p) =>
p.status === "available" &&
(p.capabilities ?? []).includes("prepare_trail_send.v1") &&
enabledProviders.has(p.id),
);
},
);
let pluginDataLoaded = false;
// Loads plugins and instances once per session so the derived gating
// stores are populated (e.g. for the trail "send to" action). Safe to call
// repeatedly; only the first call performs the requests.
export async function load_plugin_data_once() {
if (pluginDataLoaded) {
return;
}
pluginDataLoaded = true;
try {
await Promise.all([
plugins_index(),
plugin_instances_index(),
]);
} catch (e) {
pluginDataLoaded = false;
console.error(e);
}
}
export async function plugins_index(
f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch,
) {
const r = await f("/api/v1/plugin-system/plugins", {
method: "GET",
});
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail);
}
const data: { items: PluginSystemPlugin[] } = await r.json();
const items = data.items.map(pluginSystemToPluginProvider);
pluginProviders.set(items);
return items;
}
function pluginSystemToPluginProvider(plugin: PluginSystemPlugin): PluginProvider {
const contexts = Object.entries(
(plugin.manifest.auth?.contexts ?? {}) as Record<string, any>,
);
const primaryAuth = contexts[0]?.[1] ?? {};
const fields =
primaryAuth.fields ??
primaryAuth.secretFields ??
(primaryAuth.secretField ? [primaryAuth.secretField] : []);
const metadata = plugin.manifest.metadata ?? {};
return {
id: plugin.id,
type: plugin.type ?? plugin.manifest.type ?? "trails",
name: plugin.name,
displayName: plugin.displayName,
displayNames: localizedMetadata(metadata, "displayNames"),
description: plugin.description,
descriptions: localizedMetadata(metadata, "descriptions"),
icon: plugin.icon,
iconDark: plugin.iconDark,
version: plugin.version,
auth: {
type: primaryAuth.type ?? "none",
fields,
secretFields: primaryAuth.secretFields ?? fields,
authorizationUrl: primaryAuth.authorizationUrl,
tokenUrl: primaryAuth.tokenUrl,
tokenRequestFormat: primaryAuth.tokenRequestFormat,
scopes: primaryAuth.scopes,
scopeSeparator: primaryAuth.scopeSeparator,
authorizationParams: primaryAuth.authorizationParams,
pkce: primaryAuth.pkce,
tokenAuth: primaryAuth.tokenAuth,
},
configSchema: plugin.manifest.configSchema as PluginProvider["configSchema"],
hostConfig: plugin.manifest.hostConfig,
metadata,
capabilities: plugin.capabilities,
status: plugin.status,
error: plugin.error,
};
}
function localizedMetadata(
metadata: Record<string, unknown>,
key: string,
): LocalizedTextMap | undefined {
const value = metadata[key];
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const entries = Object.entries(value as Record<string, unknown>).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
);
return entries.length ? Object.fromEntries(entries) : undefined;
}