Add multi-user automation features and per-user error tracking. - Database migrations: add workflow_configs/workflow_runs (004), app_errors (005), pipeline_configs/pipeline_runs (006), and add user_token_hash to app_errors (007). - Backend: introduce per-request token handling (X-API-Token) via app.api.deps and update many API routes (auth, automations, bank, characters, dashboard, events, exchange, logs) to use user-scoped Artifacts client and character scoping. Auth endpoints no longer store tokens server-side (validate-only); clear is a no-op on server. - New Errors API and services: endpoint to list, filter, resolve, and report errors scoped to the requesting user; add error models, schemas, middleware/error handler and error_service for recording/hashing tokens. - Pipelines & Workflows: add API routers, models, schemas and engine modules (pipeline/worker/coordinator, workflow runner/conditions) and action_executor updates to support workflow/pipeline execution. - Logs: logs endpoint now prefers fetching recent action logs from the game API (with fallback to local DB), supports paging and filtering, and scopes results to the user. - Frontend: add pipeline/workflow builders, lists, progress components and hooks (use-errors, use-pipelines, use-workflows), sentry client config, and updates to API client/constants/types. - Misc: add middleware error handler, various engine strategy tweaks, tests adjusted. Overall this change enables per-user API tokens, scopes DB queries to each user, introduces pipelines/workflows runtime support, and centralizes application error tracking.
218 lines
7.3 KiB
TypeScript
218 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import { use } from "react";
|
|
import Link from "next/link";
|
|
import { ArrowLeft, Loader2, Repeat, Trash2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { usePipeline, usePipelineStatuses, useDeletePipeline } from "@/hooks/use-pipelines";
|
|
import { PipelineProgress } from "@/components/pipeline/pipeline-progress";
|
|
import { toast } from "sonner";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
export default function PipelineDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id: idStr } = use(params);
|
|
const id = Number(idStr);
|
|
const router = useRouter();
|
|
const { data, isLoading, error } = usePipeline(id);
|
|
const { data: statuses } = usePipelineStatuses();
|
|
const deleteMutation = useDeletePipeline();
|
|
|
|
const status = (statuses ?? []).find((s) => s.pipeline_id === id) ?? null;
|
|
|
|
function handleDelete() {
|
|
deleteMutation.mutate(id, {
|
|
onSuccess: () => {
|
|
toast.success("Pipeline deleted");
|
|
router.push("/automations");
|
|
},
|
|
onError: (err) => toast.error(`Failed to delete: ${err.message}`),
|
|
});
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<Card className="border-destructive bg-destructive/10 p-4">
|
|
<p className="text-sm text-destructive">
|
|
Failed to load pipeline. It may have been deleted.
|
|
</p>
|
|
<Link href="/automations" className="mt-2 inline-block">
|
|
<Button variant="outline" size="sm">
|
|
Back to Automations
|
|
</Button>
|
|
</Link>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const { config, runs } = data;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Link href="/automations">
|
|
<Button variant="ghost" size="icon-xs">
|
|
<ArrowLeft className="size-4" />
|
|
</Button>
|
|
</Link>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
|
{config.name}
|
|
</h1>
|
|
{config.loop && (
|
|
<Repeat className="size-4 text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
{config.description && (
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{config.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-destructive hover:text-destructive"
|
|
onClick={handleDelete}
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Live progress */}
|
|
<PipelineProgress config={config} status={status} />
|
|
|
|
{/* Stages overview */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">
|
|
Stages ({config.stages.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{config.stages.map((stage, idx) => (
|
|
<div key={stage.id} className="border rounded-lg p-3">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Badge variant="outline" className="text-xs">
|
|
Stage {idx + 1}
|
|
</Badge>
|
|
<span className="text-sm font-medium">{stage.name}</span>
|
|
<Badge variant="secondary" className="text-[10px]">
|
|
{stage.character_steps.length} worker
|
|
{stage.character_steps.length !== 1 && "s"}
|
|
</Badge>
|
|
</div>
|
|
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
{stage.character_steps.map((cs) => (
|
|
<div
|
|
key={cs.id}
|
|
className="text-xs bg-muted/30 rounded p-2 space-y-0.5"
|
|
>
|
|
<div className="font-medium">{cs.character_name}</div>
|
|
<div className="text-muted-foreground capitalize">
|
|
{cs.strategy_type}
|
|
</div>
|
|
{cs.transition && (
|
|
<div className="text-muted-foreground">
|
|
Until: {cs.transition.type}
|
|
{cs.transition.value != null &&
|
|
` ${cs.transition.operator ?? ">="} ${cs.transition.value}`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Run history */}
|
|
{runs.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">
|
|
Run History ({runs.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Run</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Stage</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
<TableHead>Started</TableHead>
|
|
<TableHead>Stopped</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{runs.slice(0, 20).map((run) => (
|
|
<TableRow key={run.id}>
|
|
<TableCell className="font-mono text-xs">
|
|
#{run.id}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={
|
|
run.status === "error" ? "destructive" : "secondary"
|
|
}
|
|
className="text-[10px]"
|
|
>
|
|
{run.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground">
|
|
{run.current_stage_index + 1}
|
|
{run.loop_count > 0 && ` (loop ${run.loop_count})`}
|
|
</TableCell>
|
|
<TableCell className="text-xs">
|
|
{run.total_actions_count}
|
|
</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground">
|
|
{new Date(run.started_at).toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-xs text-muted-foreground">
|
|
{run.stopped_at
|
|
? new Date(run.stopped_at).toLocaleString()
|
|
: "\u2014"}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|