chore: auto-commit (12 archivos)

- app.md
- backend/handlers.go
- backend/main.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/vite.config.ts
- backend/mcp_http.go
- backend/mcp_tokens.go
- backend/mcp_tokens_handlers.go
- backend/migrations/016_mcp_tokens.sql
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 14:38:17 +02:00
parent c9e15513c7
commit c28ae7d3c0
13 changed files with 771 additions and 4 deletions
+36 -3
View File
@@ -56,6 +56,7 @@ import {
IconLayoutKanban,
IconLogout,
IconPlug,
IconKey,
IconMenu2,
IconMessageChatbot,
IconMoodSmile,
@@ -84,6 +85,7 @@ import { AVATAR_COLORS } from "./components/colors";
import { colorBg, colorBorder } from "./components/colors";
import { NotificationsBell } from "./components/NotificationsBell";
import { ModulesModal } from "./components/ModulesModal";
import { MCPTokensModal } from "./components/MCPTokensModal";
import { useEventStream } from "./hooks/useEventStream";
import type { Board, Card, CardColor, Column, ColumnLocation, Notification, User } from "./types";
@@ -255,6 +257,23 @@ export function App() {
}
}, []);
// Coalesce ráfagas de board.invalidated (trailing debounce 300ms) — sin esto
// cada mutación remota dispara un refetch /api/board completo y la memoria
// del navegador crece sin techo.
const reloadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedReload = useCallback(() => {
if (reloadTimerRef.current) clearTimeout(reloadTimerRef.current);
reloadTimerRef.current = setTimeout(() => {
reloadTimerRef.current = null;
reload();
}, 300);
}, [reload]);
useEffect(() => {
return () => {
if (reloadTimerRef.current) clearTimeout(reloadTimerRef.current);
};
}, []);
useEffect(() => {
reload();
}, [reload]);
@@ -344,6 +363,7 @@ export function App() {
}, []);
const [modulesOpen, setModulesOpen] = useState(false);
const [mcpTokensOpen, setMcpTokensOpen] = useState(false);
const reloadNotifs = useCallback(async () => {
try {
@@ -367,7 +387,7 @@ export function App() {
useMemo(
() => ({
"board.invalidated": () => {
reload();
debouncedReload();
},
"notification.created": (payload: unknown) => {
const n = payload as Notification;
@@ -377,6 +397,7 @@ export function App() {
const who = n.actor_name || "Alguien";
const card = n.card_seq_num ? `#${n.card_seq_num}` : n.card_title;
notifications.show({
autoClose: 4000,
color: n.kind === "mention" ? "grape" : "blue",
title: `${who} en ${card}`,
message: n.snippet,
@@ -393,7 +414,7 @@ export function App() {
setNotifUnread(0);
},
}),
[reload],
[debouncedReload],
),
!!auth.user,
);
@@ -428,16 +449,21 @@ export function App() {
(c: Card): boolean => {
const term = searchTerm.trim().toLowerCase();
if (term) {
const seqStr = c.seq_num > 0 ? String(c.seq_num) : "";
const seqPadded = c.seq_num > 0 ? String(c.seq_num).padStart(5, "0") : "";
const hay = [
c.title,
c.description,
c.requester,
seqStr,
seqPadded,
...(c.tags || []),
]
.filter(Boolean)
.join(" ")
.toLowerCase();
if (!hay.includes(term)) return false;
const normalizedTerm = term.replace(/^#/, "").replace(/^0+(?=\d)/, "");
if (!hay.includes(term) && !(normalizedTerm && hay.includes(normalizedTerm))) return false;
}
if (filterAssigneeId && c.assignee_id !== filterAssigneeId) return false;
if (filterUnassigned && c.assignee_id) return false;
@@ -1266,6 +1292,12 @@ export function App() {
Modulos
</Menu.Item>
)}
<Menu.Item
leftSection={<IconKey size={14} />}
onClick={() => setMcpTokensOpen(true)}
>
MCP tokens
</Menu.Item>
<Menu.Item
leftSection={<IconLogout size={14} />}
color="red"
@@ -1279,6 +1311,7 @@ export function App() {
{auth.user?.is_admin && (
<ModulesModal opened={modulesOpen} onClose={() => setModulesOpen(false)} />
)}
<MCPTokensModal opened={mcpTokensOpen} onClose={() => setMcpTokensOpen(false)} />
</Group>
</Group>
</AppShell.Header>