forked from unom/punktfunk
Console: a Plugins section (Browse / Installed / Sources) on a static nav entry, with install friction proportional to trust — a plain confirm for a verified entry, a warning naming the curator for an external one, and a danger dialog that makes you retype the spec for a raw package. Tier badges are permanent and follow the plugin onto its own UI page. Index: unom/punktfunk-plugin-index published and served from Gitea's raw endpoint (real HTTPS, byte-exact, no vhost to stand up) — merge to main is publish, which resolves the design's open hosting question. Four things only running it could find: - runner discovery matched @punktfunk/plugin-* only, so a third-party scoped plugin (which D8 requires) would install and never run - ...and that convention also matches @punktfunk/plugin-kit, a plugin's own framework: it listed as installed and would have been imported as a unit. Both now key off the plugins dir's top-level dependencies, with an emptied dependency list meaning 'nothing installed' rather than falling back to the naming convention - the store must not pass new flags to the runner: the scripting package ships separately and an older one reads an unknown flag's value as a package name. The host writes the bunfig scope mapping itself - ureq reports only >= 400 as Err, so a conditional request's 304 arrives as Ok with an empty body — handled as an error it made every refresh after the first verify a signature over zero bytes and sit stale Also: the console's first Tabs use exposed an @unom/ui theme gap that rendered inactive tabs invisible (caught in a browser pass, not by types). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import { toast } from "@unom/ui/toast";
|
|
import { Play, Power, PowerOff } from "lucide-react";
|
|
import type { FC } from "react";
|
|
import {
|
|
type RuntimeStatus,
|
|
useSetRuntime,
|
|
useStoreRuntime,
|
|
} from "@/api/store";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { m } from "@/paraglide/messages";
|
|
|
|
// The plugin/script runner is the service every plugin actually executes inside. Installing a plugin
|
|
// while it's switched off silently gets you nothing running, so Browse carries a banner and the
|
|
// Installed tab carries the full switch.
|
|
|
|
/** Small helper both surfaces share: fire the toggle, surface a failure as a toast. */
|
|
function useRunnerToggle() {
|
|
const set = useSetRuntime();
|
|
const toggle = (enabled: boolean) => {
|
|
set.mutate(enabled, {
|
|
onError: () => toast.error(m.store_runner_failed()),
|
|
});
|
|
};
|
|
return { toggle, isPending: set.isPending };
|
|
}
|
|
|
|
/**
|
|
* Browse-tab banner: the runner is installed but off, so nothing an operator installs here would
|
|
* start. Renders nothing in every other state (including "not installed" — the Installed tab's card
|
|
* explains that case properly).
|
|
*/
|
|
export const RunnerBanner: FC = () => {
|
|
const runtime = useStoreRuntime();
|
|
const { toggle, isPending } = useRunnerToggle();
|
|
const s = runtime.data;
|
|
if (!s?.installed || s.enabled) return null;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3 rounded-lg border border-amber-600/40 bg-amber-500/10 p-4 text-sm text-amber-600 sm:flex-row sm:items-center dark:border-amber-500/40 dark:text-amber-500">
|
|
<PowerOff className="size-5 shrink-0" />
|
|
<p className="flex-1">{m.store_runner_banner()}</p>
|
|
<Button size="sm" disabled={isPending} onClick={() => toggle(true)}>
|
|
<Play className="size-4" />
|
|
{m.store_runner_enable()}
|
|
</Button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Container: the runner switch at the top of the Installed tab. Like the library's source toggles
|
|
* this is a secondary control — it stays out of the way until the query resolves rather than
|
|
* stacking a second error banner on top of the installed list's own.
|
|
*/
|
|
export const RunnerCardSection: FC = () => {
|
|
const runtime = useStoreRuntime();
|
|
const { toggle, isPending } = useRunnerToggle();
|
|
if (!runtime.data) return null;
|
|
return (
|
|
<RunnerCard status={runtime.data} busy={isPending} onToggle={toggle} />
|
|
);
|
|
};
|
|
|
|
/** The runner card: what the service is, whether it's up, and the one switch that changes it. */
|
|
export const RunnerCard: FC<{
|
|
status: RuntimeStatus;
|
|
busy: boolean;
|
|
onToggle: (enabled: boolean) => void;
|
|
}> = ({ status, busy, onToggle }) => (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center justify-between gap-3 text-base">
|
|
<span>{m.store_runner_title()}</span>
|
|
{!status.installed ? (
|
|
<Badge variant="outline">{m.store_runner_state_missing()}</Badge>
|
|
) : status.running ? (
|
|
<Badge variant="success">{m.store_runner_state_running()}</Badge>
|
|
) : status.enabled ? (
|
|
<Badge variant="outline">{m.store_runner_state_stopped()}</Badge>
|
|
) : (
|
|
<Badge variant="secondary">{m.store_runner_state_disabled()}</Badge>
|
|
)}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<p className="max-w-prose text-sm text-muted-foreground">
|
|
{status.installed
|
|
? m.store_runner_help()
|
|
: m.store_runner_not_installed()}
|
|
</p>
|
|
<dl className="flex flex-wrap gap-x-8 gap-y-1 text-xs text-muted-foreground">
|
|
<div className="flex gap-2">
|
|
<dt>{m.store_runner_unit()}</dt>
|
|
<dd className="font-mono text-foreground">{status.unit}</dd>
|
|
</div>
|
|
{status.principal && (
|
|
<div className="flex gap-2">
|
|
<dt>{m.store_runner_principal()}</dt>
|
|
<dd className="font-mono text-foreground">{status.principal}</dd>
|
|
</div>
|
|
)}
|
|
</dl>
|
|
{status.detail && (
|
|
<p className="text-xs text-muted-foreground">{status.detail}</p>
|
|
)}
|
|
{status.installed && (
|
|
<Button
|
|
size="sm"
|
|
variant={status.enabled ? "outline" : "default"}
|
|
disabled={busy}
|
|
onClick={() => onToggle(!status.enabled)}
|
|
>
|
|
{status.enabled ? (
|
|
<PowerOff className="size-4" />
|
|
) : (
|
|
<Power className="size-4" />
|
|
)}
|
|
{status.enabled ? m.store_runner_disable() : m.store_runner_enable()}
|
|
</Button>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|