From 469a8a9cfcbec106ea0f5dea6644965abdd00bae Mon Sep 17 00:00:00 2001 From: markov Date: Fri, 13 Mar 2026 22:28:58 +0100 Subject: [PATCH] feat: allow slug change for members via API + UI --- specs/API.md | 6 +- tracker | 2 +- web-client-vite/.gitignore | 2 +- web-client-vite/src/App.tsx | 4 +- web-client-vite/src/components/AgentModal.tsx | 313 ++++++++++- web-client-vite/src/components/AuthGuard.tsx | 25 +- web-client-vite/src/components/ChatPanel.tsx | 448 ++++++++++++++- .../src/components/CreateAgentModal.tsx | 159 +++++- .../src/components/CreateProjectModal.tsx | 153 +++-- .../src/components/CreateTaskModal.tsx | 244 ++++++++ .../src/components/KanbanBoard.tsx | 240 +++++++- .../src/components/MentionInput.tsx | 157 ++++++ .../src/components/ProjectFiles.tsx | 265 ++++++++- .../src/components/ProjectSettings.tsx | 320 ++++++++++- web-client-vite/src/components/Sidebar.tsx | 13 +- web-client-vite/src/components/TaskModal.tsx | 526 ++++++++++++++++++ web-client-vite/src/lib/api.ts | 286 ++++++++-- web-client-vite/src/lib/ws.ts | 31 +- web-client-vite/src/main.tsx | 7 +- .../src/pages/CreateProjectPage.tsx | 2 +- web-client-vite/src/pages/HomePage.tsx | 4 +- web-client-vite/src/pages/LoginPage.tsx | 2 +- web-client-vite/src/pages/ProjectPage.tsx | 43 +- .../src/pages/settings/LabelsPage.tsx | 67 +++ .../src/pages/settings/SettingsLayout.tsx | 1 + .../src/pages/settings/SettingsPage.tsx | 2 +- 26 files changed, 3099 insertions(+), 223 deletions(-) create mode 100644 web-client-vite/src/components/CreateTaskModal.tsx create mode 100644 web-client-vite/src/components/MentionInput.tsx create mode 100644 web-client-vite/src/components/TaskModal.tsx create mode 100644 web-client-vite/src/pages/settings/LabelsPage.tsx diff --git a/specs/API.md b/specs/API.md index 5b95ea1..8b2e00a 100644 --- a/specs/API.md +++ b/specs/API.md @@ -876,4 +876,8 @@ Swagger UI документация **CORS:** автоматически обрабатывается для разрешённых origins -Этот документ описывает API на основе исходного кода в `/root/projects/team-board/tracker/src/tracker/api/` на дату создания спецификации. \ No newline at end of file +Этот документ описывает API на основе исходного кода в `/root/projects/team-board/tracker/src/tracker/api/` на дату создания спецификации. +### Обновление slug (добавлено 2026-03-13) +`PATCH /api/v1/members/{member_id}` теперь принимает поле `slug`. +- Валидация: slug уникален, при конфликте → 409 +- Формат: lowercase, только `[a-z0-9_-]` diff --git a/tracker b/tracker index e39c26d..f7622e1 160000 --- a/tracker +++ b/tracker @@ -1 +1 @@ -Subproject commit e39c26d3212a7b656bd5629c6feaafcb259f71e3 +Subproject commit f7622e13d5da79020848b86cc13d4e21f931ad65 diff --git a/web-client-vite/.gitignore b/web-client-vite/.gitignore index a547bf3..54f07af 100644 --- a/web-client-vite/.gitignore +++ b/web-client-vite/.gitignore @@ -21,4 +21,4 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? +*.sw? \ No newline at end of file diff --git a/web-client-vite/src/App.tsx b/web-client-vite/src/App.tsx index 83660fe..97c7175 100644 --- a/web-client-vite/src/App.tsx +++ b/web-client-vite/src/App.tsx @@ -6,6 +6,7 @@ import ProjectPage from "@/pages/ProjectPage"; import CreateProjectPage from "@/pages/CreateProjectPage"; import SettingsLayout from "@/pages/settings/SettingsLayout"; import SettingsPage from "@/pages/settings/SettingsPage"; +import LabelsPage from "@/pages/settings/LabelsPage"; import AgentsPage from "@/pages/settings/AgentsPage"; function App() { @@ -20,7 +21,7 @@ function App() { } /> - @@ -38,6 +39,7 @@ function App() { }> } /> + } /> } /> diff --git a/web-client-vite/src/components/AgentModal.tsx b/web-client-vite/src/components/AgentModal.tsx index 8ede738..33a0049 100644 --- a/web-client-vite/src/components/AgentModal.tsx +++ b/web-client-vite/src/components/AgentModal.tsx @@ -1,28 +1,315 @@ -import type { Member } from "@/lib/api"; + +import { useState, useEffect } from "react"; +import type { Member, Label } from "@/lib/api"; +import { updateMember, regenerateToken, revokeToken, getLabels } from "@/lib/api"; interface Props { agent: Member; onClose: () => void; - onUpdated: () => void; + onUpdated: (agent: Member) => void; } -export default function AgentModal({ agent, onClose, onUpdated: _onUpdated }: Props) { +export default function AgentModal({ agent, onClose, onUpdated }: Props) { + const [name, setName] = useState(agent.name); + const [slug, setSlug] = useState(agent.slug); + const [slugError, setSlugError] = useState(""); + const [capabilities, setCapabilities] = useState( + agent.agent_config?.capabilities?.join(", ") || "" + ); + const [chatListen, setChatListen] = useState(agent.agent_config?.chat_listen || "mentions"); + const [taskListen, setTaskListen] = useState(agent.agent_config?.task_listen || "assigned"); + const [prompt, setPrompt] = useState(agent.agent_config?.prompt || ""); + const [model, setModel] = useState(agent.agent_config?.model || ""); + const [saving, setSaving] = useState(false); + const [currentToken, setCurrentToken] = useState(agent.token || null); + const [copied, setCopied] = useState(false); + const [confirmRegen, setConfirmRegen] = useState(false); + const [confirmRevoke, setConfirmRevoke] = useState(false); + const [allLabels, setAllLabels] = useState([]); + const [agentLabels, setAgentLabels] = useState(agent.agent_config?.labels || []); + + useEffect(() => { + getLabels().then(setAllLabels).catch(() => {}); + }, []); + + const handleSave = async () => { + setSaving(true); + try { + const caps = capabilities.split(",").map((c) => c.trim()).filter(Boolean); + const trimmedSlug = slug.trim().toLowerCase().replace(/[^a-z0-9_-]/g, ''); + if (!trimmedSlug) { + setSlugError("Slug не может быть пустым"); + return; + } + const updated = await updateMember(agent.id, { + name: name.trim(), + slug: trimmedSlug !== agent.slug ? trimmedSlug : undefined, + agent_config: { + capabilities: caps, + labels: agentLabels, + chat_listen: chatListen, + task_listen: taskListen, + prompt: prompt.trim() || null, + model: model.trim() || null, + }, + }); + onUpdated(updated); + onClose(); + } catch (e: any) { + if (e?.response?.status === 409) { + setSlugError("Этот slug уже занят"); + } else { + console.error(e); + } + } finally { + setSaving(false); + } + }; + + const handleRegenerate = async () => { + try { + const { token } = await regenerateToken(agent.id); + setCurrentToken(token); + setConfirmRegen(false); + } catch (e) { + console.error(e); + } + }; + + const handleRevoke = async () => { + try { + await revokeToken(agent.id); + setCurrentToken(null); + setConfirmRevoke(false); + } catch (e) { + console.error(e); + } + }; + + const copyToken = () => { + if (currentToken) { + navigator.clipboard.writeText(currentToken); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + return ( -
-
-

Агент: {agent.name}

-

@{agent.slug}

-

Coming soon...

-
+
+
e.stopPropagation()} + > +
+
+

{agent.name}

+
@{agent.slug}
+
+ + {agent.status === "online" ? "online" : "offline"} + +
+ +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm" + /> +
+ +
+ + { setSlug(e.target.value); setSlugError(""); }} + className={`w-full px-3 py-2 bg-[var(--bg)] border rounded-lg outline-none text-sm ${slugError ? 'border-red-500' : 'border-[var(--border)] focus:border-[var(--accent)]'}`} + placeholder="agent-slug" + /> + {slugError &&
{slugError}
} +
+ +
+ + setCapabilities(e.target.value)} + className="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg outline-none focus:border-[var(--accent)] text-sm" + placeholder="code, review, deploy" + /> +
+ + {/* Labels */} + {allLabels.length > 0 && ( +
+ +
+ {allLabels.map((l) => ( + + ))} +
+

Для авто-назначения: лейблы задачи ∩ лейблы агента

+
+ )} + +
+
+ + +
+
+ + +
+
+ +
+ +