Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions docs/public/user-guide/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,25 @@ Deleting an environment permanently removes all of its pipelines, nodes, and sec

## Agent enrollment tokens

Before an agent can connect to an environment, you must generate an **enrollment token** on the environment detail page. The token is displayed once -- copy it immediately and provide it to the agent at startup:
Before an agent can connect to an environment, you must generate an **enrollment token** on the environment detail page. The token is displayed once -- copy it immediately.

### Linux (binary)

The install script downloads the agent binary, verifies its checksum, installs [Vector](https://vector.dev) if it is not already present, and creates a systemd service:

```bash
curl -sSfL https://raw.githubusercontent.com/TerrifiedBug/vectorflow/main/agent/install.sh | \
sudo bash -s -- --url https://your-vectorflow-instance --token <enrollment-token>
```

### Docker

```bash
VF_URL=https://your-vectorflow-instance:3000
VF_TOKEN=<enrollment-token>
./vf-agent
docker run -d --name vf-agent --restart unless-stopped \
-e VF_URL=https://your-vectorflow-instance \
-e VF_TOKEN=<enrollment-token> \
-v /var/lib/vf-agent:/var/lib/vf-agent \
ghcr.io/terrifiedbug/vectorflow-agent:latest
```

You can regenerate or revoke the token at any time. Revoking a token prevents new agents from enrolling, but already-connected agents continue operating until their individual node tokens are revoked from the Fleet page.
Expand Down
68 changes: 68 additions & 0 deletions src/app/(dashboard)/environments/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,74 @@ export default function EnvironmentDetailPage({
<Badge variant="secondary" className="ml-auto">Active</Badge>
</div>
)}

{/* Quick Start snippets — shown when a token exists */}
{env.hasEnrollmentToken && (
<div className="space-y-3 rounded-md border p-4">
<p className="text-sm font-medium">Quick Start</p>

<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Linux (installs agent + Vector)</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
aria-label="Copy Linux install command"
onClick={async () => {
const token = enrollmentToken || "<enrollment-token>";
const cmd = `curl -sSfL https://raw.githubusercontent.com/TerrifiedBug/vectorflow/main/agent/install.sh | sudo bash -s -- --url ${window.location.origin} --token ${token}`;
await copyToClipboard(cmd);
toast.success("Command copied");
}}
>
<Copy className="h-3 w-3" />
</Button>
</div>
<pre className="overflow-x-auto rounded bg-muted px-3 py-2 text-xs">
{`curl -sSfL https://raw.githubusercontent.com/TerrifiedBug/vectorflow/main/agent/install.sh | \\
sudo bash -s -- --url ${typeof window !== "undefined" ? window.location.origin : "https://your-vectorflow-instance"} --token ${enrollmentToken || "<enrollment-token>"}`}
</pre>
</div>

<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Docker</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
aria-label="Copy Docker run command"
onClick={async () => {
const token = enrollmentToken || "<enrollment-token>";
const cmd = `docker run -d --name vf-agent --restart unless-stopped \\\n -e VF_URL=${window.location.origin} \\\n -e VF_TOKEN=${token} \\\n -v /var/lib/vf-agent:/var/lib/vf-agent \\\n ghcr.io/terrifiedbug/vectorflow-agent:latest`;
await copyToClipboard(cmd);
toast.success("Command copied");
}}
>
<Copy className="h-3 w-3" />
</Button>
</div>
<pre className="overflow-x-auto rounded bg-muted px-3 py-2 text-xs">
{`docker run -d --name vf-agent --restart unless-stopped \\
-e VF_URL=${typeof window !== "undefined" ? window.location.origin : "https://your-vectorflow-instance"} \\
-e VF_TOKEN=${enrollmentToken || "<enrollment-token>"} \\
-v /var/lib/vf-agent:/var/lib/vf-agent \\
ghcr.io/terrifiedbug/vectorflow-agent:latest`}
</pre>
</div>

<a
href="https://docs.vectorflow.io/user-guide/environments#agent-enrollment-tokens"
target="_blank"
rel="noopener noreferrer"
className="inline-block text-xs text-muted-foreground underline hover:text-foreground"
>
View full setup guide
</a>
</div>
)}

<div className="flex gap-2">
<Button
onClick={() => generateTokenMutation.mutate({ environmentId: id })}
Expand Down
42 changes: 25 additions & 17 deletions src/app/(dashboard)/settings/_components/backup-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,23 +208,31 @@ export function BackupSettings() {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="storage-backend-toggle">
{storageBackend === "s3" ? (
<span className="flex items-center gap-2">
<Cloud className="h-4 w-4" /> S3 Storage
</span>
) : (
<span className="flex items-center gap-2">
<HardDrive className="h-4 w-4" /> Local Storage
</span>
)}
</Label>
<Switch
id="storage-backend-toggle"
checked={storageBackend === "s3"}
onCheckedChange={(checked) => setStorageBackend(checked ? "s3" : "local")}
/>
<div className="inline-flex rounded-lg border p-1">
<button
type="button"
className={`inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
storageBackend === "local"
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
onClick={() => setStorageBackend("local")}
>
<HardDrive className="h-4 w-4" />
Local
</button>
<button
type="button"
className={`inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
storageBackend === "s3"
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
onClick={() => setStorageBackend("s3")}
>
<Cloud className="h-4 w-4" />
S3
</button>
</div>

{storageBackend === "s3" && (
Expand Down
12 changes: 11 additions & 1 deletion src/app/(dashboard)/settings/webhooks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -758,10 +758,20 @@ function WebhookEndpointsSettings() {

const endpoints = (listQuery.data ?? []) as Endpoint[];

if (!selectedTeamId) {
return (
<div className="space-y-2 p-6">
{[...Array(3)].map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
);
}

if (listQuery.isError) {
return (
<QueryError
message="Failed to load webhook endpoints"
message={listQuery.error?.message || "Failed to load webhook endpoints"}
onRetry={() => listQuery.refetch()}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/use-sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function useSSE() {
useEffect(() => {
// Guard: only one connection per tab
if (activeConnectionCount > 0) {
console.warn(
console.debug(
"[useSSE] Multiple mounts detected — skipping duplicate connection. " +
"Call useSSE() once at the app root.",
);
Expand Down
Loading