feat: TASK 037A/B - MicroDAO Multi-Room Cleanup & UI Polish
TASK 037A: Backend Consistency - Added db/sql/037_microdao_agent_audit.sql - Added services/city-service/tools/fix_microdao_agent_consistency.py - Updated repo_city.get_public_citizens with stricter filtering (node_id, microdao_membership) - Updated PublicCitizenSummary model to include home_microdao and primary_city_room - Updated NodeProfile model and get_node_by_id to include microdaos list TASK 037B: UI Polish - Updated MicrodaoRoomsSection with role-based colors/icons and mini-map - Updated /microdao/[slug] with new Hero Block (badges, stats, orchestrator) - Updated /citizens/[slug] with MicroDAO cross-link in DAIS profile - Updated /nodes/[nodeId] with MicroDAO Presence section
This commit is contained in:
@@ -268,7 +268,18 @@ export default function CitizenProfilePage() {
|
|||||||
|
|
||||||
{/* DAIS Public Passport */}
|
{/* DAIS Public Passport */}
|
||||||
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
|
<section className="bg-white/5 border border-white/10 rounded-2xl p-6 space-y-4">
|
||||||
<h2 className="text-white font-semibold">DAIS Public Passport</h2>
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-white font-semibold">DAIS Public Passport</h2>
|
||||||
|
{citizen.microdao && (
|
||||||
|
<Link
|
||||||
|
href={`/microdao/${citizen.microdao.slug}`}
|
||||||
|
className="inline-flex items-center gap-1.5 text-xs text-cyan-300 hover:text-cyan-200 transition-colors"
|
||||||
|
>
|
||||||
|
<Building2 className="w-3.5 h-3.5" />
|
||||||
|
{citizen.microdao.name}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
|
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
|
||||||
<p className="text-xs uppercase text-white/40">Identity</p>
|
<p className="text-xs uppercase text-white/40">Identity</p>
|
||||||
|
|||||||
@@ -54,9 +54,12 @@ export default function MicrodaoDetailPage() {
|
|||||||
const publicCitizens = microdao.public_citizens ?? [];
|
const publicCitizens = microdao.public_citizens ?? [];
|
||||||
const childMicrodaos = microdao.child_microdaos ?? [];
|
const childMicrodaos = microdao.child_microdaos ?? [];
|
||||||
|
|
||||||
|
// Use fetched rooms if available, otherwise fallback to microdao.rooms
|
||||||
|
const displayRooms = rooms.length > 0 ? rooms : (microdao.rooms || []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8 space-y-8">
|
<div className="max-w-5xl mx-auto px-4 py-8 space-y-8">
|
||||||
{/* Back link */}
|
{/* Back link */}
|
||||||
<Link
|
<Link
|
||||||
href="/microdao"
|
href="/microdao"
|
||||||
@@ -66,102 +69,96 @@ export default function MicrodaoDetailPage() {
|
|||||||
Всі MicroDAO
|
Всі MicroDAO
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Hero Section (TASK 037B) */}
|
||||||
<header className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
<section className="rounded-3xl border border-white/10 bg-gradient-to-br from-sky-950/50 via-slate-900 to-black p-6 md:p-8 space-y-6 relative overflow-hidden shadow-2xl shadow-sky-900/10">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
{/* Background pattern */}
|
||||||
<div className="space-y-3">
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] opacity-20" />
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{microdao.logo_url ? (
|
<div className="relative z-10 flex flex-col gap-6 md:flex-row md:items-start md:justify-between">
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
<div className="space-y-5 max-w-3xl">
|
||||||
<img
|
|
||||||
src={microdao.logo_url}
|
|
||||||
alt={microdao.name}
|
|
||||||
className="w-14 h-14 rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-violet-500/30 to-purple-500/30 flex items-center justify-center">
|
|
||||||
<Building2 className="w-7 h-7 text-violet-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h1 className="text-2xl font-bold text-slate-100">{microdao.name}</h1>
|
|
||||||
{microdao.is_platform && (
|
|
||||||
<span className="px-2 py-0.5 rounded text-[10px] font-medium bg-amber-500/20 text-amber-400 border border-amber-500/30">
|
|
||||||
Platform
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{microdao.description && (
|
|
||||||
<p className="text-sm text-slate-400 mt-1">{microdao.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Badges */}
|
{/* Badges */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className={`px-2.5 py-0.5 rounded-full text-[11px] uppercase tracking-wider font-semibold border ${
|
||||||
|
microdao.is_platform
|
||||||
|
? "border-amber-500/40 text-amber-300 bg-amber-500/10"
|
||||||
|
: "border-cyan-400/40 text-cyan-300 bg-cyan-500/10"
|
||||||
|
}`}>
|
||||||
|
{microdao.is_platform ? "Platform District" : "MicroDAO"}
|
||||||
|
</span>
|
||||||
|
|
||||||
{microdao.district && (
|
{microdao.district && (
|
||||||
<span
|
<span className="px-2.5 py-0.5 rounded-full text-[11px] uppercase tracking-wider font-medium border border-white/10 text-white/60 bg-white/5">
|
||||||
className={`text-xs px-3 py-1 rounded-full border font-medium ${
|
|
||||||
DISTRICT_COLORS[microdao.district] ||
|
|
||||||
"bg-slate-500/10 text-slate-400 border-slate-500/30"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{microdao.district}
|
{microdao.district}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span
|
|
||||||
className={`text-xs px-3 py-1 rounded-full border font-medium flex items-center gap-1 ${
|
{microdao.parent_microdao_slug && (
|
||||||
microdao.is_active
|
<span className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] border border-white/10 text-white/50 bg-white/5">
|
||||||
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
|
<Layers className="w-3 h-3" />
|
||||||
: "bg-amber-500/10 text-amber-400 border-amber-500/30"
|
<span className="opacity-60">part of</span>
|
||||||
}`}
|
<Link href={`/microdao/${microdao.parent_microdao_slug}`} className="hover:text-white transition-colors font-medium">
|
||||||
>
|
{microdao.parent_microdao_slug}
|
||||||
<span className={`w-1.5 h-1.5 rounded-full ${microdao.is_active ? 'bg-emerald-500' : 'bg-amber-500'}`} />
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className={`px-2.5 py-0.5 rounded-full text-[11px] font-medium border flex items-center gap-1.5 ${
|
||||||
|
microdao.is_active
|
||||||
|
? "border-emerald-500/30 text-emerald-400 bg-emerald-500/5"
|
||||||
|
: "border-slate-600 text-slate-400 bg-slate-800"
|
||||||
|
}`}>
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full ${microdao.is_active ? 'bg-emerald-400' : 'bg-slate-500'}`} />
|
||||||
{microdao.is_active ? "Active" : "Inactive"}
|
{microdao.is_active ? "Active" : "Inactive"}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-xs px-3 py-1 rounded-full border font-medium flex items-center gap-1 ${
|
</div>
|
||||||
microdao.is_public
|
|
||||||
? "bg-blue-500/10 text-blue-400 border-blue-500/30"
|
{/* Title & Description */}
|
||||||
: "bg-slate-500/10 text-slate-400 border-slate-500/30"
|
<div className="space-y-3">
|
||||||
}`}>
|
<h1 className="text-3xl md:text-5xl font-bold text-white tracking-tight leading-tight">
|
||||||
{microdao.is_public ? <Globe className="w-3 h-3" /> : <Lock className="w-3 h-3" />}
|
{microdao.name}
|
||||||
{microdao.is_public ? "Public" : "Private"}
|
</h1>
|
||||||
</span>
|
{microdao.description && (
|
||||||
|
<p className="text-base md:text-lg text-slate-300 leading-relaxed max-w-2xl">
|
||||||
|
{microdao.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Parent MicroDAO */}
|
{/* Key Stats & Orchestrator */}
|
||||||
{microdao.parent_microdao_slug && (
|
<div className="flex flex-wrap gap-3 pt-2">
|
||||||
<div className="flex items-center gap-2 text-sm text-slate-400">
|
{orchestrator && (
|
||||||
<Layers className="w-4 h-4" />
|
<Link
|
||||||
<span>Parent:</span>
|
href={`/agents/${orchestrator.agent_id}`}
|
||||||
<Link
|
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20 hover:bg-amber-500/20 transition-colors group"
|
||||||
href={`/microdao/${microdao.parent_microdao_slug}`}
|
|
||||||
className="text-cyan-400 hover:text-cyan-300 transition-colors"
|
|
||||||
>
|
>
|
||||||
{microdao.parent_microdao_slug}
|
<Crown className="w-4 h-4 text-amber-400 group-hover:scale-110 transition-transform" />
|
||||||
|
<span className="text-sm text-amber-200 font-medium">Orchestrator: {orchestrator.display_name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
|
||||||
|
<Users className="w-4 h-4 text-white/60" />
|
||||||
|
<span className="text-sm text-white/80">{publicCitizens.length} citizens</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10">
|
||||||
|
<MessageCircle className="w-4 h-4 text-white/60" />
|
||||||
|
<span className="text-sm text-white/80">{displayRooms.length} rooms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Orchestrator */}
|
{/* Logo */}
|
||||||
{orchestrator && (
|
<div className="hidden md:flex flex-col gap-3">
|
||||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-lg p-4">
|
<div className="w-24 h-24 rounded-2xl bg-slate-800 border border-white/10 flex items-center justify-center overflow-hidden shadow-xl">
|
||||||
<div className="text-xs text-amber-400 mb-1 flex items-center gap-1">
|
{microdao.logo_url ? (
|
||||||
<Crown className="w-3 h-3" />
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
Оркестратор
|
<img src={microdao.logo_url} alt={microdao.name} className="w-full h-full object-cover" />
|
||||||
</div>
|
) : (
|
||||||
<Link
|
<Building2 className="w-10 h-10 text-slate-600" />
|
||||||
href={`/agents/${orchestrator.agent_id}`}
|
)}
|
||||||
className="text-sm font-medium text-slate-100 hover:text-cyan-400 transition-colors"
|
</div>
|
||||||
>
|
</div>
|
||||||
{orchestrator.display_name}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</section>
|
||||||
|
|
||||||
{/* Child MicroDAOs */}
|
{/* Child MicroDAOs */}
|
||||||
{childMicrodaos.length > 0 && (
|
{childMicrodaos.length > 0 && (
|
||||||
@@ -170,19 +167,19 @@ export default function MicrodaoDetailPage() {
|
|||||||
<Layers className="w-5 h-5 text-cyan-400" />
|
<Layers className="w-5 h-5 text-cyan-400" />
|
||||||
Дочірні MicroDAO
|
Дочірні MicroDAO
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3">
|
||||||
{childMicrodaos.map((child) => (
|
{childMicrodaos.map((child) => (
|
||||||
<Link
|
<Link
|
||||||
key={child.id}
|
key={child.id}
|
||||||
href={`/microdao/${child.slug}`}
|
href={`/microdao/${child.slug}`}
|
||||||
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 hover:border-cyan-500/30 transition-colors flex items-center justify-between"
|
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 hover:border-cyan-500/30 transition-colors flex items-center justify-between group"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-slate-200">{child.name}</p>
|
<p className="text-sm font-medium text-slate-200 group-hover:text-cyan-300 transition-colors">{child.name}</p>
|
||||||
<p className="text-xs text-slate-500">{child.slug}</p>
|
<p className="text-xs text-slate-500">{child.slug}</p>
|
||||||
</div>
|
</div>
|
||||||
{child.is_platform && (
|
{child.is_platform && (
|
||||||
<span className="text-[10px] px-2 py-1 rounded bg-amber-500/10 text-amber-400">
|
<span className="text-[10px] px-2 py-1 rounded bg-amber-500/10 text-amber-400 border border-amber-500/20">
|
||||||
Platform
|
Platform
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -192,200 +189,11 @@ export default function MicrodaoDetailPage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Agents */}
|
|
||||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
|
||||||
<Bot className="w-5 h-5 text-cyan-400" />
|
|
||||||
Агентська команда
|
|
||||||
<span className="text-sm font-normal text-slate-500">({microdao.agents.length})</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{microdao.agents.length === 0 ? (
|
|
||||||
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
{microdao.agents.map((a) => (
|
|
||||||
<div
|
|
||||||
key={a.agent_id}
|
|
||||||
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Link
|
|
||||||
href={`/agents/${a.agent_id}`}
|
|
||||||
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
|
|
||||||
>
|
|
||||||
{a.display_name}
|
|
||||||
</Link>
|
|
||||||
{a.role && (
|
|
||||||
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{a.agent_id === microdao.orchestrator_agent_id && (
|
|
||||||
<Crown className="w-4 h-4 text-amber-400" />
|
|
||||||
)}
|
|
||||||
{a.is_core && (
|
|
||||||
<span className="text-[10px] px-2 py-1 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
|
|
||||||
Core
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Public Citizens */}
|
|
||||||
{publicCitizens.length > 0 && (
|
|
||||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
|
||||||
<Users className="w-5 h-5 text-cyan-400" />
|
|
||||||
Громадяни цього MicroDAO
|
|
||||||
<span className="text-sm font-normal text-slate-500">({publicCitizens.length})</span>
|
|
||||||
</h2>
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
|
||||||
{publicCitizens.map((citizen) => (
|
|
||||||
<Link
|
|
||||||
key={citizen.slug}
|
|
||||||
href={`/citizens/${citizen.slug}`}
|
|
||||||
className="flex items-center justify-between bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 hover:border-cyan-500/40 transition-colors"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-200 font-medium">{citizen.display_name}</p>
|
|
||||||
{citizen.public_title && (
|
|
||||||
<p className="text-sm text-slate-400">{citizen.public_title}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{citizen.district && (
|
|
||||||
<span className="text-xs text-slate-500">{citizen.district}</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Channels */}
|
|
||||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
|
||||||
<MessageSquare className="w-5 h-5 text-cyan-400" />
|
|
||||||
Канали та кімнати
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{telegramChannels.length === 0 &&
|
|
||||||
matrixChannels.length === 0 &&
|
|
||||||
cityRooms.length === 0 &&
|
|
||||||
crewChannels.length === 0 ? (
|
|
||||||
<div className="text-sm text-slate-500">Канали ще не налаштовані.</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Telegram */}
|
|
||||||
{telegramChannels.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-slate-500 uppercase tracking-wide">Telegram</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{telegramChannels.map((c) => (
|
|
||||||
<a
|
|
||||||
key={c.ref_id}
|
|
||||||
href={`https://t.me/${c.ref_id.replace("@", "")}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-500/10 border border-blue-500/30 rounded-full text-sm text-blue-400 hover:bg-blue-500/20 transition-colors"
|
|
||||||
>
|
|
||||||
{c.display_name || c.ref_id}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Matrix */}
|
|
||||||
{matrixChannels.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-slate-500 uppercase tracking-wide">Matrix</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{matrixChannels.map((c) => (
|
|
||||||
<span
|
|
||||||
key={c.ref_id}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-green-500/10 border border-green-500/30 rounded-full text-sm text-green-400"
|
|
||||||
>
|
|
||||||
{c.display_name || c.ref_id}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* City Rooms */}
|
|
||||||
{cityRooms.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-slate-500 uppercase tracking-wide">Міські кімнати</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{cityRooms.map((c) => (
|
|
||||||
<Link
|
|
||||||
key={c.ref_id}
|
|
||||||
href={`/city/${c.ref_id}`}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-violet-500/10 border border-violet-500/30 rounded-full text-sm text-violet-400 hover:bg-violet-500/20 transition-colors"
|
|
||||||
>
|
|
||||||
{c.display_name || c.ref_id}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CrewAI */}
|
|
||||||
{crewChannels.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-slate-500 uppercase tracking-wide">CrewAI сценарії</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{crewChannels.map((c) => (
|
|
||||||
<span
|
|
||||||
key={c.ref_id}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500/10 border border-orange-500/30 rounded-full text-sm text-orange-400"
|
|
||||||
>
|
|
||||||
{c.display_name || c.ref_id}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
|
||||||
<BarChart3 className="w-5 h-5 text-cyan-400" />
|
|
||||||
Статистика
|
|
||||||
</h2>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
|
|
||||||
<div className="text-2xl font-bold text-slate-100">{microdao.agents.length}</div>
|
|
||||||
<div className="text-xs text-slate-500">Агентів</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
|
|
||||||
<div className="text-2xl font-bold text-slate-100">{publicCitizens.length}</div>
|
|
||||||
<div className="text-xs text-slate-500">Громадян</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
|
|
||||||
<div className="text-2xl font-bold text-slate-100">{microdao.channels.length}</div>
|
|
||||||
<div className="text-xs text-slate-500">Каналів</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-slate-900/50 rounded-lg p-4 text-center">
|
|
||||||
<div className="text-2xl font-bold text-slate-100">{childMicrodaos.length}</div>
|
|
||||||
<div className="text-xs text-slate-500">Дочірніх DAO</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Orchestrator Room Management Panel */}
|
{/* Orchestrator Room Management Panel */}
|
||||||
{orchestrator && (
|
{orchestrator && (
|
||||||
<MicrodaoRoomsAdminPanel
|
<MicrodaoRoomsAdminPanel
|
||||||
microdaoSlug={slug}
|
microdaoSlug={slug}
|
||||||
rooms={rooms.length > 0 ? rooms : (microdao.rooms || [])}
|
rooms={displayRooms}
|
||||||
canManage={true} // TODO: check if current user is orchestrator
|
canManage={true} // TODO: check if current user is orchestrator
|
||||||
onRoomUpdated={handleRoomUpdated}
|
onRoomUpdated={handleRoomUpdated}
|
||||||
/>
|
/>
|
||||||
@@ -393,22 +201,183 @@ export default function MicrodaoDetailPage() {
|
|||||||
|
|
||||||
{/* Multi-Room Section with Chats */}
|
{/* Multi-Room Section with Chats */}
|
||||||
<MicrodaoRoomsSection
|
<MicrodaoRoomsSection
|
||||||
rooms={rooms.length > 0 ? rooms : (microdao.rooms || [])}
|
rooms={displayRooms}
|
||||||
primaryRoomSlug={microdao.primary_city_room?.slug}
|
primaryRoomSlug={microdao.primary_city_room?.slug}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{/* Agents */}
|
||||||
|
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
|
<Bot className="w-5 h-5 text-cyan-400" />
|
||||||
|
Агентська команда
|
||||||
|
<span className="text-sm font-normal text-slate-500">({microdao.agents.length})</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{microdao.agents.length === 0 ? (
|
||||||
|
<div className="text-sm text-slate-500">Агенти ще не привʼязані.</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{microdao.agents.map((a) => (
|
||||||
|
<div
|
||||||
|
key={a.agent_id}
|
||||||
|
className="bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 flex items-center justify-between hover:border-slate-600/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Link
|
||||||
|
href={`/agents/${a.agent_id}`}
|
||||||
|
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{a.display_name}
|
||||||
|
{a.agent_id === microdao.orchestrator_agent_id && (
|
||||||
|
<Crown className="w-3 h-3 text-amber-400" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
{a.role && (
|
||||||
|
<div className="text-xs text-slate-500 capitalize">{a.role}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{a.is_core && (
|
||||||
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/10 text-violet-400 border border-violet-500/30 uppercase tracking-wide">
|
||||||
|
Core
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Public Citizens */}
|
||||||
|
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4 h-full">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
|
<Users className="w-5 h-5 text-cyan-400" />
|
||||||
|
Громадяни
|
||||||
|
<span className="text-sm font-normal text-slate-500">({publicCitizens.length})</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{publicCitizens.length === 0 ? (
|
||||||
|
<div className="text-sm text-slate-500">Немає публічних громадян.</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{publicCitizens.map((citizen) => (
|
||||||
|
<Link
|
||||||
|
key={citizen.slug}
|
||||||
|
href={`/citizens/${citizen.slug}`}
|
||||||
|
className="flex items-center justify-between bg-slate-900/50 border border-slate-700/30 rounded-lg px-4 py-3 hover:border-cyan-500/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-200 font-medium text-sm">{citizen.display_name}</p>
|
||||||
|
{citizen.public_title && (
|
||||||
|
<p className="text-xs text-slate-400">{citizen.public_title}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{citizen.district && (
|
||||||
|
<span className="text-[10px] text-slate-500 border border-slate-700 px-1.5 py-0.5 rounded">{citizen.district}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Channels & Stats Row */}
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
<section className="md:col-span-2 bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-5 h-5 text-cyan-400" />
|
||||||
|
Канали звʼязку
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{telegramChannels.length === 0 &&
|
||||||
|
matrixChannels.length === 0 &&
|
||||||
|
cityRooms.length === 0 &&
|
||||||
|
crewChannels.length === 0 ? (
|
||||||
|
<div className="text-sm text-slate-500">Канали ще не налаштовані.</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{telegramChannels.map((c) => (
|
||||||
|
<a
|
||||||
|
key={c.ref_id}
|
||||||
|
href={`https://t.me/${c.ref_id.replace("@", "")}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-400 hover:bg-blue-500/20 transition-colors"
|
||||||
|
>
|
||||||
|
{c.display_name || c.ref_id}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
{matrixChannels.map((c) => (
|
||||||
|
<span
|
||||||
|
key={c.ref_id}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 bg-green-500/10 border border-green-500/30 rounded-lg text-sm text-green-400"
|
||||||
|
>
|
||||||
|
{c.display_name || c.ref_id}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{cityRooms.map((c) => (
|
||||||
|
<Link
|
||||||
|
key={c.ref_id}
|
||||||
|
href={`/city/${c.ref_id}`}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 bg-violet-500/10 border border-violet-500/30 rounded-lg text-sm text-violet-400 hover:bg-violet-500/20 transition-colors"
|
||||||
|
>
|
||||||
|
{c.display_name || c.ref_id}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{crewChannels.map((c) => (
|
||||||
|
<span
|
||||||
|
key={c.ref_id}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 bg-orange-500/10 border border-orange-500/30 rounded-lg text-sm text-orange-400"
|
||||||
|
>
|
||||||
|
{c.display_name || c.ref_id}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
|
<BarChart3 className="w-5 h-5 text-cyan-400" />
|
||||||
|
Інфо
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between items-center text-sm border-b border-slate-700/50 pb-2">
|
||||||
|
<span className="text-slate-400">Агентів</span>
|
||||||
|
<span className="text-slate-200 font-medium">{microdao.agents.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm border-b border-slate-700/50 pb-2">
|
||||||
|
<span className="text-slate-400">Громадян</span>
|
||||||
|
<span className="text-slate-200 font-medium">{publicCitizens.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm border-b border-slate-700/50 pb-2">
|
||||||
|
<span className="text-slate-400">Каналів</span>
|
||||||
|
<span className="text-slate-200 font-medium">{microdao.channels.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-sm">
|
||||||
|
<span className="text-slate-400">Кімнат</span>
|
||||||
|
<span className="text-slate-200 font-medium">{displayRooms.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Visibility Settings (only for orchestrator) */}
|
{/* Visibility Settings (only for orchestrator) */}
|
||||||
{orchestrator && (
|
{orchestrator && (
|
||||||
<MicrodaoVisibilityCard
|
<div className="pt-8 border-t border-white/5">
|
||||||
microdaoId={microdao.id}
|
<MicrodaoVisibilityCard
|
||||||
isPublic={microdao.is_public}
|
microdaoId={microdao.id}
|
||||||
isPlatform={microdao.is_platform}
|
isPublic={microdao.is_public}
|
||||||
isOrchestrator={true} // TODO: check if current user is orchestrator
|
isPlatform={microdao.is_platform}
|
||||||
onUpdated={() => {
|
isOrchestrator={true} // TODO: check if current user is orchestrator
|
||||||
// Refresh the page data
|
onUpdated={() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -128,6 +128,32 @@ export default function NodeCabinetPage() {
|
|||||||
guardian={nodeProfile?.guardian_agent}
|
guardian={nodeProfile?.guardian_agent}
|
||||||
steward={nodeProfile?.steward_agent}
|
steward={nodeProfile?.steward_agent}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* MicroDAO Presence */}
|
||||||
|
{nodeProfile?.microdaos && nodeProfile.microdaos.length > 0 && (
|
||||||
|
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
|
||||||
|
<Activity className="w-5 h-5 text-cyan-400" />
|
||||||
|
MicroDAO Presence
|
||||||
|
</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{nodeProfile.microdaos.map((dao) => (
|
||||||
|
<li key={dao.id} className="flex items-center justify-between bg-slate-900/30 rounded-lg p-3">
|
||||||
|
<Link
|
||||||
|
href={`/microdao/${dao.slug}`}
|
||||||
|
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
|
||||||
|
>
|
||||||
|
{dao.name}
|
||||||
|
</Link>
|
||||||
|
<span className="text-xs text-slate-500 bg-slate-800/50 px-2 py-1 rounded">
|
||||||
|
{dao.rooms_count} rooms
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<NodeStandardComplianceCard node={dashboard.node} />
|
<NodeStandardComplianceCard node={dashboard.node} />
|
||||||
<MatrixCard matrix={dashboard.matrix} />
|
<MatrixCard matrix={dashboard.matrix} />
|
||||||
<ModulesCard modules={dashboard.node.modules} />
|
<ModulesCard modules={dashboard.node.modules} />
|
||||||
@@ -259,6 +285,31 @@ export default function NodeCabinetPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* MicroDAO Presence */}
|
||||||
|
{nodeProfile?.microdaos && nodeProfile.microdaos.length > 0 && (
|
||||||
|
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 mb-6">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-100 mb-4 flex items-center gap-2">
|
||||||
|
<Activity className="w-5 h-5 text-cyan-400" />
|
||||||
|
MicroDAO Presence
|
||||||
|
</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{nodeProfile.microdaos.map((dao) => (
|
||||||
|
<li key={dao.id} className="flex items-center justify-between bg-slate-900/30 rounded-lg p-3">
|
||||||
|
<Link
|
||||||
|
href={`/microdao/${dao.slug}`}
|
||||||
|
className="text-sm font-medium text-slate-200 hover:text-cyan-400 transition-colors"
|
||||||
|
>
|
||||||
|
{dao.name}
|
||||||
|
</Link>
|
||||||
|
<span className="text-xs text-slate-500 bg-slate-800/50 px-2 py-1 rounded">
|
||||||
|
{dao.rooms_count} rooms
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Notice for non-NODE1 */}
|
{/* Notice for non-NODE1 */}
|
||||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 mb-6">
|
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 mb-6">
|
||||||
<p className="text-amber-400 text-sm">
|
<p className="text-amber-400 text-sm">
|
||||||
@@ -280,4 +331,3 @@ export default function NodeCabinetPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,47 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MessageCircle, Home, Users, FlaskConical, Shield, Vote, Hash } from "lucide-react";
|
import { MessageCircle, Home, Users, FlaskConical, Shield, Gavel, Hash, Users2 } from "lucide-react";
|
||||||
import { CityRoomSummary } from "@/lib/types/microdao";
|
import { CityRoomSummary } from "@/lib/types/microdao";
|
||||||
import { CityChatWidget } from "@/components/city/CityChatWidget";
|
import { CityChatWidget } from "@/components/city/CityChatWidget";
|
||||||
|
|
||||||
interface MicrodaoRoomsSectionProps {
|
interface MicrodaoRoomsSectionProps {
|
||||||
rooms: CityRoomSummary[];
|
rooms: CityRoomSummary[];
|
||||||
primaryRoomSlug?: string | null;
|
primaryRoomSlug?: string | null;
|
||||||
showAllChats?: boolean; // If true, show chat widgets for all rooms
|
showAllChats?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROLE_LABELS: Record<string, string> = {
|
const ROLE_META: Record<string, { label: string; chipClass: string; icon: React.ReactNode }> = {
|
||||||
primary: "Основна кімната",
|
primary: {
|
||||||
lobby: "Лобі",
|
label: "Primary / Lobby",
|
||||||
team: "Командна кімната",
|
chipClass: "bg-emerald-500/10 text-emerald-300 border-emerald-500/30",
|
||||||
research: "Дослідницька лабораторія",
|
icon: <Home className="w-3.5 h-3.5" />,
|
||||||
security: "Безпека",
|
},
|
||||||
governance: "Управління",
|
lobby: {
|
||||||
};
|
label: "Lobby",
|
||||||
|
chipClass: "bg-sky-500/10 text-sky-300 border-sky-500/30",
|
||||||
const ROLE_ICONS: Record<string, React.ReactNode> = {
|
icon: <MessageCircle className="w-3.5 h-3.5" />,
|
||||||
primary: <Home className="w-4 h-4" />,
|
},
|
||||||
lobby: <MessageCircle className="w-4 h-4" />,
|
team: {
|
||||||
team: <Users className="w-4 h-4" />,
|
label: "Team",
|
||||||
research: <FlaskConical className="w-4 h-4" />,
|
chipClass: "bg-indigo-500/10 text-indigo-300 border-indigo-500/30",
|
||||||
security: <Shield className="w-4 h-4" />,
|
icon: <Users2 className="w-3.5 h-3.5" />,
|
||||||
governance: <Vote className="w-4 h-4" />,
|
},
|
||||||
};
|
research: {
|
||||||
|
label: "Research",
|
||||||
const ROLE_COLORS: Record<string, string> = {
|
chipClass: "bg-violet-500/10 text-violet-300 border-violet-500/30",
|
||||||
primary: "text-cyan-400 bg-cyan-500/10 border-cyan-500/30",
|
icon: <FlaskConical className="w-3.5 h-3.5" />,
|
||||||
lobby: "text-green-400 bg-green-500/10 border-green-500/30",
|
},
|
||||||
team: "text-blue-400 bg-blue-500/10 border-blue-500/30",
|
security: {
|
||||||
research: "text-purple-400 bg-purple-500/10 border-purple-500/30",
|
label: "Security",
|
||||||
security: "text-red-400 bg-red-500/10 border-red-500/30",
|
chipClass: "bg-rose-500/10 text-rose-300 border-rose-500/30",
|
||||||
governance: "text-amber-400 bg-amber-500/10 border-amber-500/30",
|
icon: <Shield className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
governance: {
|
||||||
|
label: "Governance",
|
||||||
|
chipClass: "bg-amber-500/10 text-amber-300 border-amber-500/30",
|
||||||
|
icon: <Gavel className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MicrodaoRoomsSection({
|
export function MicrodaoRoomsSection({
|
||||||
@@ -64,31 +70,68 @@ export function MicrodaoRoomsSection({
|
|||||||
|
|
||||||
const others = rooms.filter(r => r.id !== primary.id);
|
const others = rooms.filter(r => r.id !== primary.id);
|
||||||
|
|
||||||
|
// Group by role for mini-map
|
||||||
|
const byRole = rooms.reduce((acc, r) => {
|
||||||
|
const role = r.room_role || 'other';
|
||||||
|
if (!acc[role]) acc[role] = [];
|
||||||
|
acc[role].push(r);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, CityRoomSummary[]>);
|
||||||
|
|
||||||
|
// Get meta for primary room
|
||||||
|
const primaryMeta = primary.room_role ? ROLE_META[primary.room_role] : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
<section className="bg-slate-800/30 border border-slate-700/50 rounded-xl p-6 space-y-6">
|
||||||
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<MessageCircle className="w-5 h-5 text-cyan-400" />
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
||||||
Кімнати MicroDAO
|
<MessageCircle className="w-5 h-5 text-cyan-400" />
|
||||||
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
|
Кімнати MicroDAO
|
||||||
</h2>
|
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Mini-map */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Object.entries(byRole).map(([role, list]) => {
|
||||||
|
const meta = ROLE_META[role];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={role}
|
||||||
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-[11px] ${
|
||||||
|
meta ? meta.chipClass : "bg-slate-700/30 text-slate-400 border-slate-700/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{meta?.icon || <Hash className="w-3 h-3" />}
|
||||||
|
<span>{meta?.label ?? (role === 'other' ? 'Other' : role)}</span>
|
||||||
|
<span className="opacity-60">({list.length})</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Primary room with inline chat */}
|
{/* Primary room with inline chat */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`p-2 rounded-lg border ${ROLE_COLORS[primary.room_role || 'primary'] || ROLE_COLORS.primary}`}>
|
<div className={`p-2 rounded-lg border ${primaryMeta ? primaryMeta.chipClass : "text-cyan-400 bg-cyan-500/10 border-cyan-500/30"}`}>
|
||||||
{ROLE_ICONS[primary.room_role || 'primary'] || <Hash className="w-4 h-4" />}
|
{primaryMeta?.icon || <Home className="w-4 h-4" />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-medium text-slate-100">{primary.name}</div>
|
<div className="text-base font-medium text-slate-100 flex items-center gap-2">
|
||||||
|
{primary.name}
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-emerald-400 bg-emerald-500/10 px-1.5 py-0.5 rounded border border-emerald-500/20">
|
||||||
|
Primary
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="text-xs text-slate-500">
|
<div className="text-xs text-slate-500">
|
||||||
{ROLE_LABELS[primary.room_role || 'primary'] || primary.room_role || 'Кімната'}
|
{primaryMeta?.label || primary.room_role || 'Main Room'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/city/${primary.slug}`}
|
href={`/city/${primary.slug}`}
|
||||||
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors"
|
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors px-3 py-1.5 rounded-lg hover:bg-cyan-950/30 border border-transparent hover:border-cyan-500/20"
|
||||||
>
|
>
|
||||||
Відкрити окремо →
|
Відкрити окремо →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -99,45 +142,49 @@ export function MicrodaoRoomsSection({
|
|||||||
|
|
||||||
{/* Other rooms */}
|
{/* Other rooms */}
|
||||||
{others.length > 0 && (
|
{others.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 pt-2">
|
||||||
<div className="text-sm text-slate-400 font-medium">Інші кімнати</div>
|
<div className="text-sm text-slate-400 font-medium px-1">Інші кімнати</div>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
{others.map(room => (
|
{others.map(room => {
|
||||||
<div
|
const meta = room.room_role ? ROLE_META[room.room_role] : undefined;
|
||||||
key={room.id}
|
return (
|
||||||
className="bg-slate-900/50 border border-slate-700/30 rounded-xl p-4 space-y-3"
|
<div
|
||||||
>
|
key={room.id}
|
||||||
<div className="flex items-center justify-between gap-2">
|
className="bg-slate-900/50 border border-slate-700/30 rounded-xl p-4 space-y-3 hover:border-slate-600/50 transition-colors"
|
||||||
<div className="flex items-center gap-2">
|
>
|
||||||
<div className={`p-1.5 rounded-lg border ${ROLE_COLORS[room.room_role || ''] || 'text-slate-400 bg-slate-500/10 border-slate-500/30'}`}>
|
<div className="flex items-center justify-between gap-2">
|
||||||
{ROLE_ICONS[room.room_role || ''] || <Hash className="w-3.5 h-3.5" />}
|
<div className="flex items-center gap-3">
|
||||||
</div>
|
<div className={`p-1.5 rounded-lg border ${meta ? meta.chipClass : "text-slate-400 bg-slate-700/30 border-slate-700/50"}`}>
|
||||||
<div>
|
{meta?.icon || <Hash className="w-3.5 h-3.5" />}
|
||||||
<div className="text-sm font-medium text-slate-200">{room.name}</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500">
|
<div>
|
||||||
{ROLE_LABELS[room.room_role || ''] || room.room_role || 'Кімната'}
|
<div className="text-sm font-medium text-slate-200">{room.name}</div>
|
||||||
|
{meta && (
|
||||||
|
<div className="text-[11px] text-slate-500">
|
||||||
|
{meta.label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Link
|
||||||
|
href={`/city/${room.slug}`}
|
||||||
|
className="text-xs text-slate-400 hover:text-cyan-400 transition-colors px-2 py-1"
|
||||||
|
>
|
||||||
|
Увійти →
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
|
||||||
href={`/city/${room.slug}`}
|
{showAllChats && (
|
||||||
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors"
|
<div className="mt-2">
|
||||||
>
|
<CityChatWidget roomSlug={room.slug} compact />
|
||||||
Відкрити →
|
</div>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{showAllChats && (
|
})}
|
||||||
<div className="mt-2">
|
|
||||||
<CityChatWidget roomSlug={room.slug} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ export interface NodeAgentSummary {
|
|||||||
slug?: string;
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NodeMicrodaoSummary {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
rooms_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NodeProfile {
|
export interface NodeProfile {
|
||||||
node_id: string;
|
node_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,6 +27,7 @@ export interface NodeProfile {
|
|||||||
steward_agent_id?: string | null;
|
steward_agent_id?: string | null;
|
||||||
guardian_agent?: NodeAgentSummary | null;
|
guardian_agent?: NodeAgentSummary | null;
|
||||||
steward_agent?: NodeAgentSummary | null;
|
steward_agent?: NodeAgentSummary | null;
|
||||||
|
microdaos?: NodeMicrodaoSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeListResponse {
|
export interface NodeListResponse {
|
||||||
|
|||||||
40
db/sql/037_microdao_agent_audit.sql
Normal file
40
db/sql/037_microdao_agent_audit.sql
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
-- Публічні агенти без microDAO
|
||||||
|
SELECT id, display_name, node_id, is_public, public_slug
|
||||||
|
FROM agents
|
||||||
|
WHERE is_public = true
|
||||||
|
AND (id NOT IN (
|
||||||
|
SELECT agent_id FROM microdao_agents
|
||||||
|
));
|
||||||
|
|
||||||
|
-- Публічні агенти без node_id
|
||||||
|
SELECT id, display_name, is_public, public_slug
|
||||||
|
FROM agents
|
||||||
|
WHERE is_public = true
|
||||||
|
AND (node_id IS NULL OR node_id = '');
|
||||||
|
|
||||||
|
-- Публічні агенти без public_slug
|
||||||
|
SELECT id, display_name, is_public
|
||||||
|
FROM agents
|
||||||
|
WHERE is_public = true
|
||||||
|
AND (public_slug IS NULL OR public_slug = '');
|
||||||
|
|
||||||
|
-- Кімнати без microDAO, але з matrix_room_id (кандидати на привʼязку)
|
||||||
|
SELECT id, slug, name, matrix_room_id
|
||||||
|
FROM city_rooms
|
||||||
|
WHERE microdao_id IS NULL
|
||||||
|
AND matrix_room_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- MicroDAO без жодної кімнати
|
||||||
|
SELECT m.id, m.slug, m.name
|
||||||
|
FROM microdaos m
|
||||||
|
LEFT JOIN city_rooms r ON r.microdao_id = m.id
|
||||||
|
GROUP BY m.id, m.slug, m.name
|
||||||
|
HAVING COUNT(r.id) = 0;
|
||||||
|
|
||||||
|
-- MicroDAO з кількома primary-кімнатами
|
||||||
|
SELECT m.slug, COUNT(*)
|
||||||
|
FROM city_rooms r
|
||||||
|
JOIN microdaos m ON m.id = r.microdao_id
|
||||||
|
WHERE r.room_role = 'primary'
|
||||||
|
GROUP BY m.slug
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
80
docs/internal/maintenance/MICRODAO_AGENT_CLEANUP_037A.md
Normal file
80
docs/internal/maintenance/MICRODAO_AGENT_CLEANUP_037A.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# MicroDAO & Agent Consistency Cleanup (Task 037A)
|
||||||
|
|
||||||
|
**Дата:** 29 листопада 2025
|
||||||
|
**Статус:** Реалізовано (інструменти готові)
|
||||||
|
|
||||||
|
Цей документ описує процедуру очистки та вирівнювання даних між агентами, MicroDAO та Citizens Layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Проблематика
|
||||||
|
|
||||||
|
У системі накопичилися:
|
||||||
|
* Агенти без привʼязки до MicroDAO (orphans).
|
||||||
|
* Публічні агенти без `public_slug` або `node_id`.
|
||||||
|
* MicroDAO без кімнат або без `primary` кімнати.
|
||||||
|
* Це призводить до некоректного відображення у `/citizens`, `/microdao` та `/nodes`.
|
||||||
|
|
||||||
|
## 2. Інструменти аудиту
|
||||||
|
|
||||||
|
### SQL Аудит (`db/sql/037_microdao_agent_audit.sql`)
|
||||||
|
|
||||||
|
Цей SQL-файл містить запити для ручної перевірки стану бази даних:
|
||||||
|
* Пошук публічних агентів без membership.
|
||||||
|
* Пошук агентів без `node_id`.
|
||||||
|
* Пошук MicroDAO без кімнат.
|
||||||
|
* Пошук MicroDAO з дубльованими primary-кімнатами.
|
||||||
|
|
||||||
|
Запуск (приклад):
|
||||||
|
```bash
|
||||||
|
cat db/sql/037_microdao_agent_audit.sql | docker exec -i dagi-postgres psql -U postgres -d daarion
|
||||||
|
```
|
||||||
|
|
||||||
|
### Автоматичний скрипт (`services/city-service/tools/fix_microdao_agent_consistency.py`)
|
||||||
|
|
||||||
|
Скрипт для автоматичного виправлення типових помилок.
|
||||||
|
|
||||||
|
**Що він робить:**
|
||||||
|
1. **Агенти:**
|
||||||
|
* Якщо `public_slug` відсутній → встановлює `public_slug = id`.
|
||||||
|
* Логує агентів без `node_id` та MicroDAO membership.
|
||||||
|
2. **MicroDAO:**
|
||||||
|
* Перевіряє наявність кімнат.
|
||||||
|
* Якщо є кімнати, але немає `primary` → призначає кімнату з найменшим `sort_order` як primary.
|
||||||
|
* Якщо є кілька `primary` → залишає одну, інші робить `team`.
|
||||||
|
|
||||||
|
**Запуск:**
|
||||||
|
1. Зайти в контейнер `city-service` (або локально з налаштованим venv).
|
||||||
|
2. Запустити в режимі Dry Run (тільки логування):
|
||||||
|
```bash
|
||||||
|
python tools/fix_microdao_agent_consistency.py
|
||||||
|
```
|
||||||
|
3. Застосувати зміни:
|
||||||
|
```bash
|
||||||
|
python tools/fix_microdao_agent_consistency.py --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Зміни в API (Citizens Layer)
|
||||||
|
|
||||||
|
Впроваджено суворішу фільтрацію для публічних громадян (`/public/citizens`):
|
||||||
|
* Агент повинен мати `is_public = true`.
|
||||||
|
* `public_slug` не NULL.
|
||||||
|
* **`node_id` не NULL.**
|
||||||
|
* **Має хоча б одне MicroDAO membership** (`EXISTS (SELECT 1 FROM microdao_agents ...)`).
|
||||||
|
|
||||||
|
Це гарантує, що "сміттєві" тестові агенти не потрапляють у публічні списки.
|
||||||
|
|
||||||
|
Також API тепер повертає розширену інформацію:
|
||||||
|
* `home_microdao_slug`, `home_microdao_name`
|
||||||
|
* `primary_city_room` (об'єкт з деталями кімнати)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Рекомендації для Operations
|
||||||
|
|
||||||
|
1. Регулярно запускати `audit.sql` для моніторингу здоров'я даних.
|
||||||
|
2. При створенні нових агентів вручну через SQL — обов'язково додавати їх у `microdao_agents` та прописувати `node_id`.
|
||||||
|
3. При створенні MicroDAO — обов'язково створювати хоча б одну кімнату і робити її `primary`.
|
||||||
|
|
||||||
57
docs/users/microdao/MICRODAO_ROOMS_AND_PLATFORM_UI_037B.md
Normal file
57
docs/users/microdao/MICRODAO_ROOMS_AND_PLATFORM_UI_037B.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# MicroDAO UI & Multi-Room Experience (Task 037B)
|
||||||
|
|
||||||
|
**Дата:** 29 листопада 2025
|
||||||
|
**Статус:** Реалізовано
|
||||||
|
|
||||||
|
Цей документ описує оновлений інтерфейс MicroDAO, систему кімнат та інтеграцію з Citizens/Nodes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. MicroDAO Dashboard (`/microdao/[slug]`)
|
||||||
|
|
||||||
|
Сторінка MicroDAO отримала значне оновлення ("Hero Block"):
|
||||||
|
* **Візуальний стиль:** Великий заголовок, бейджі типу (Platform/MicroDAO), District, Parent DAO.
|
||||||
|
* **Статистика:** Кількість громадян, кімнат, посилання на Оркестратора.
|
||||||
|
* **Навігація:** Чіткий поділ на дочірні DAO, агентів, громадян, канали.
|
||||||
|
|
||||||
|
### Multi-Room Section
|
||||||
|
Секція кімнат тепер підтримує рольову модель:
|
||||||
|
* **Primary Room:** Завжди відображається розгорнутою з вбудованим чатом. Має особливий стиль.
|
||||||
|
* **Other Rooms:** Відображаються компактними картками.
|
||||||
|
* **Mini-Map:** Кольорові індикатори показують структуру кімнат за ролями (Governance, Research, Team, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ролі Кімнат
|
||||||
|
|
||||||
|
Кожна кімната в MicroDAO має роль, яка визначає її колір та іконку:
|
||||||
|
|
||||||
|
| Роль | Колір | Іконка | Призначення |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `primary` | Emerald (Зелений) | Home | Головна кімната, лобі, загальний чат |
|
||||||
|
| `lobby` | Sky (Блакитний) | Message | Привітання, флуд |
|
||||||
|
| `team` | Indigo (Синій) | Users | Внутрішня робота команди |
|
||||||
|
| `research` | Violet (Фіолетовий) | Flask | Дослідження, R&D |
|
||||||
|
| `security` | Rose (Червоний) | Shield | Безпека, алерти |
|
||||||
|
| `governance` | Amber (Жовтий) | Gavel | Голосування, рішення |
|
||||||
|
|
||||||
|
Оркестратор може змінювати ролі через **Admin Panel** на сторінці MicroDAO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Інтеграція Citizens ↔ MicroDAO
|
||||||
|
|
||||||
|
* **Citizen Profile:** У паспорті громадянина (`/citizens/[slug]`) тепер є пряме посилання на його Home MicroDAO.
|
||||||
|
* **Hero Badge:** У шапці профілю громадянина відображається бейдж MicroDAO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Інтеграція Nodes ↔ MicroDAO
|
||||||
|
|
||||||
|
На сторінці Ноди (`/nodes/[nodeId]`):
|
||||||
|
* Додано секцію **MicroDAO Presence**.
|
||||||
|
* Вона показує список MicroDAO, чий Оркестратор працює на цій ноді.
|
||||||
|
* Відображається кількість кімнат кожного DAO.
|
||||||
|
|
||||||
|
Це дозволяє бачити фізичне/логічне розміщення спільнот по інфраструктурі мережі.
|
||||||
|
|
||||||
@@ -208,6 +208,14 @@ class NodeAgentSummary(BaseModel):
|
|||||||
slug: Optional[str] = None
|
slug: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class NodeMicrodaoSummary(BaseModel):
|
||||||
|
"""Summary of a MicroDAO hosted on a node (via orchestrator)"""
|
||||||
|
id: str
|
||||||
|
slug: str
|
||||||
|
name: str
|
||||||
|
rooms_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
class NodeProfile(BaseModel):
|
class NodeProfile(BaseModel):
|
||||||
"""Node profile for Node Directory"""
|
"""Node profile for Node Directory"""
|
||||||
node_id: str
|
node_id: str
|
||||||
@@ -224,6 +232,7 @@ class NodeProfile(BaseModel):
|
|||||||
steward_agent_id: Optional[str] = None
|
steward_agent_id: Optional[str] = None
|
||||||
guardian_agent: Optional[NodeAgentSummary] = None
|
guardian_agent: Optional[NodeAgentSummary] = None
|
||||||
steward_agent: Optional[NodeAgentSummary] = None
|
steward_agent: Optional[NodeAgentSummary] = None
|
||||||
|
microdaos: List[NodeMicrodaoSummary] = []
|
||||||
|
|
||||||
|
|
||||||
class ModelBindings(BaseModel):
|
class ModelBindings(BaseModel):
|
||||||
@@ -302,6 +311,12 @@ class PublicCitizenSummary(BaseModel):
|
|||||||
status: Optional[str] = None # backward compatibility
|
status: Optional[str] = None # backward compatibility
|
||||||
# Home node info
|
# Home node info
|
||||||
home_node: Optional[HomeNodeView] = None
|
home_node: Optional[HomeNodeView] = None
|
||||||
|
node_id: Optional[str] = None
|
||||||
|
|
||||||
|
# TASK 037A: Alignment
|
||||||
|
home_microdao_slug: Optional[str] = None
|
||||||
|
home_microdao_name: Optional[str] = None
|
||||||
|
primary_city_room: Optional["CityRoomSummary"] = None
|
||||||
|
|
||||||
|
|
||||||
class PublicCitizenProfile(BaseModel):
|
class PublicCitizenProfile(BaseModel):
|
||||||
|
|||||||
@@ -941,7 +941,10 @@ async def get_public_citizens(
|
|||||||
"a.public_slug IS NOT NULL",
|
"a.public_slug IS NOT NULL",
|
||||||
"COALESCE(a.is_archived, false) = false",
|
"COALESCE(a.is_archived, false) = false",
|
||||||
"COALESCE(a.is_test, false) = false",
|
"COALESCE(a.is_test, false) = false",
|
||||||
"a.deleted_at IS NULL"
|
"a.deleted_at IS NULL",
|
||||||
|
# TASK 037A: Stricter filtering for Citizens Layer
|
||||||
|
"a.node_id IS NOT NULL",
|
||||||
|
"EXISTS (SELECT 1 FROM microdao_agents ma WHERE ma.agent_id = a.id)"
|
||||||
]
|
]
|
||||||
|
|
||||||
if district:
|
if district:
|
||||||
@@ -978,9 +981,28 @@ async def get_public_citizens(
|
|||||||
nc.hostname AS home_node_hostname,
|
nc.hostname AS home_node_hostname,
|
||||||
nc.roles AS home_node_roles,
|
nc.roles AS home_node_roles,
|
||||||
nc.environment AS home_node_environment,
|
nc.environment AS home_node_environment,
|
||||||
|
-- MicroDAO info
|
||||||
|
m.slug AS home_microdao_slug,
|
||||||
|
m.name AS home_microdao_name,
|
||||||
|
-- Room info
|
||||||
|
cr.id AS room_id,
|
||||||
|
cr.slug AS room_slug,
|
||||||
|
cr.name AS room_name,
|
||||||
|
cr.matrix_room_id AS room_matrix_id,
|
||||||
COUNT(*) OVER() AS total_count
|
COUNT(*) OVER() AS total_count
|
||||||
FROM agents a
|
FROM agents a
|
||||||
LEFT JOIN node_cache nc ON a.node_id = nc.node_id
|
LEFT JOIN node_cache nc ON a.node_id = nc.node_id
|
||||||
|
-- Join primary MicroDAO
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT ma.agent_id, md.slug, md.name
|
||||||
|
FROM microdao_agents ma
|
||||||
|
JOIN microdaos md ON ma.microdao_id = md.id
|
||||||
|
WHERE ma.agent_id = a.id
|
||||||
|
ORDER BY ma.is_core DESC, md.name
|
||||||
|
LIMIT 1
|
||||||
|
) m ON true
|
||||||
|
-- Join primary room (by public_primary_room_slug)
|
||||||
|
LEFT JOIN city_rooms cr ON cr.slug = a.public_primary_room_slug
|
||||||
WHERE {where_sql}
|
WHERE {where_sql}
|
||||||
ORDER BY a.display_name
|
ORDER BY a.display_name
|
||||||
LIMIT ${len(params) + 1} OFFSET ${len(params) + 2}
|
LIMIT ${len(params) + 1} OFFSET ${len(params) + 2}
|
||||||
@@ -1011,8 +1033,21 @@ async def get_public_citizens(
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
data["home_node"] = None
|
data["home_node"] = None
|
||||||
|
|
||||||
|
# Build primary_city_room object
|
||||||
|
if data.get("room_id"):
|
||||||
|
data["primary_city_room"] = {
|
||||||
|
"id": str(data["room_id"]),
|
||||||
|
"slug": data["room_slug"],
|
||||||
|
"name": data["room_name"],
|
||||||
|
"matrix_room_id": data.get("room_matrix_id")
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data["primary_city_room"] = None
|
||||||
|
|
||||||
# Clean up intermediate fields
|
# Clean up intermediate fields
|
||||||
for key in ["home_node_name", "home_node_hostname", "home_node_roles", "home_node_environment"]:
|
for key in ["home_node_name", "home_node_hostname", "home_node_roles", "home_node_environment",
|
||||||
|
"room_id", "room_slug", "room_name", "room_matrix_id"]:
|
||||||
data.pop(key, None)
|
data.pop(key, None)
|
||||||
items.append(data)
|
items.append(data)
|
||||||
|
|
||||||
@@ -1595,6 +1630,19 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
|||||||
|
|
||||||
data = dict(row)
|
data = dict(row)
|
||||||
|
|
||||||
|
# Fetch MicroDAOs where orchestrator is on this node
|
||||||
|
microdaos = await pool.fetch("""
|
||||||
|
SELECT m.id, m.slug, m.name, COUNT(cr.id) as rooms_count
|
||||||
|
FROM microdaos m
|
||||||
|
JOIN agents a ON m.orchestrator_agent_id = a.id
|
||||||
|
LEFT JOIN city_rooms cr ON cr.microdao_id = m.id
|
||||||
|
WHERE a.node_id = $1
|
||||||
|
GROUP BY m.id, m.slug, m.name
|
||||||
|
ORDER BY m.name
|
||||||
|
""", node_id)
|
||||||
|
|
||||||
|
data["microdaos"] = [dict(m) for m in microdaos]
|
||||||
|
|
||||||
# Build guardian_agent object
|
# Build guardian_agent object
|
||||||
if data.get("guardian_agent_id"):
|
if data.get("guardian_agent_id"):
|
||||||
data["guardian_agent"] = {
|
data["guardian_agent"] = {
|
||||||
@@ -1616,7 +1664,7 @@ async def get_node_by_id(node_id: str) -> Optional[dict]:
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
data["steward_agent"] = None
|
data["steward_agent"] = None
|
||||||
|
|
||||||
# Clean up intermediate fields
|
# Clean up intermediate fields
|
||||||
for key in ["guardian_name", "guardian_kind", "guardian_slug",
|
for key in ["guardian_name", "guardian_kind", "guardian_slug",
|
||||||
"steward_name", "steward_kind", "steward_slug"]:
|
"steward_name", "steward_kind", "steward_slug"]:
|
||||||
|
|||||||
177
services/city-service/tools/fix_microdao_agent_consistency.py
Normal file
177
services/city-service/tools/fix_microdao_agent_consistency.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import asyncpg
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
# Add parent directory to path to allow importing from app if needed,
|
||||||
|
# though we will try to use direct SQL for maintenance script ease.
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@postgres:5432/daarion")
|
||||||
|
|
||||||
|
async def get_connection():
|
||||||
|
return await asyncpg.connect(DATABASE_URL)
|
||||||
|
|
||||||
|
async def fix_agents(conn, apply=False):
|
||||||
|
print("--- CHECKING AGENTS ---")
|
||||||
|
|
||||||
|
# 1. Agents with missing public_slug
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT id, display_name FROM agents
|
||||||
|
WHERE is_public = true AND (public_slug IS NULL OR public_slug = '')
|
||||||
|
""")
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
print(f"Found {len(rows)} public agents without public_slug.")
|
||||||
|
for r in rows:
|
||||||
|
print(f" - {r['display_name']} ({r['id']}) -> will set to {r['id']}")
|
||||||
|
if apply:
|
||||||
|
await conn.execute("""
|
||||||
|
UPDATE agents SET public_slug = id
|
||||||
|
WHERE id = $1
|
||||||
|
""", r['id'])
|
||||||
|
else:
|
||||||
|
print("OK: All public agents have public_slug.")
|
||||||
|
|
||||||
|
# 2. Agents without node_id
|
||||||
|
# We assume NODE1 as default fallback if we must set it, but per task we mostly log it.
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT id, display_name FROM agents
|
||||||
|
WHERE is_public = true AND (node_id IS NULL OR node_id = '')
|
||||||
|
""")
|
||||||
|
if rows:
|
||||||
|
print(f"Found {len(rows)} public agents without node_id (WARNING).")
|
||||||
|
for r in rows:
|
||||||
|
print(f" - {r['display_name']} ({r['id']})")
|
||||||
|
# Option: could set to default node if needed, but task says "optionally try to set".
|
||||||
|
# We will skip auto-setting for now to avoid assigning wrong node.
|
||||||
|
else:
|
||||||
|
print("OK: All public agents have node_id.")
|
||||||
|
|
||||||
|
# 3. Agents without microdao memberships
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT a.id, a.display_name
|
||||||
|
FROM agents a
|
||||||
|
WHERE a.is_public = true
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM microdao_agents ma WHERE ma.agent_id = a.id)
|
||||||
|
""")
|
||||||
|
if rows:
|
||||||
|
print(f"Found {len(rows)} public agents without MicroDAO membership (WARNING).")
|
||||||
|
for r in rows:
|
||||||
|
print(f" - {r['display_name']} ({r['id']})")
|
||||||
|
else:
|
||||||
|
print("OK: All public agents have at least one MicroDAO membership.")
|
||||||
|
|
||||||
|
|
||||||
|
async def fix_microdaos(conn, apply=False):
|
||||||
|
print("\n--- CHECKING MICRODAOS ---")
|
||||||
|
|
||||||
|
# 1. MicroDAO without rooms
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT m.id, m.slug, m.name
|
||||||
|
FROM microdaos m
|
||||||
|
WHERE m.is_public = true
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM city_rooms cr WHERE cr.microdao_id = m.id)
|
||||||
|
""")
|
||||||
|
if rows:
|
||||||
|
print(f"Found {len(rows)} public MicroDAOs without rooms (WARNING).")
|
||||||
|
for r in rows:
|
||||||
|
print(f" - {r['name']} ({r['slug']})")
|
||||||
|
else:
|
||||||
|
print("OK: All public MicroDAOs have at least one room.")
|
||||||
|
|
||||||
|
# 2. MicroDAO without PRIMARY room (but has rooms)
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT m.id, m.slug, m.name
|
||||||
|
FROM microdaos m
|
||||||
|
WHERE m.is_public = true
|
||||||
|
AND EXISTS (SELECT 1 FROM city_rooms cr WHERE cr.microdao_id = m.id)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM city_rooms cr WHERE cr.microdao_id = m.id AND cr.room_role = 'primary')
|
||||||
|
""")
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
print(f"Found {len(rows)} MicroDAOs with rooms but NO primary room.")
|
||||||
|
for r in rows:
|
||||||
|
print(f" - {r['name']} ({r['slug']})")
|
||||||
|
|
||||||
|
# Find candidate: lowest sort_order
|
||||||
|
candidate = await conn.fetchrow("""
|
||||||
|
SELECT id, name, sort_order FROM city_rooms
|
||||||
|
WHERE microdao_id = $1
|
||||||
|
ORDER BY sort_order ASC, name ASC
|
||||||
|
LIMIT 1
|
||||||
|
""", r['id'])
|
||||||
|
|
||||||
|
if candidate:
|
||||||
|
print(f" -> Candidate: {candidate['name']} (sort: {candidate['sort_order']})")
|
||||||
|
if apply:
|
||||||
|
print(" -> Setting as primary...")
|
||||||
|
await conn.execute("""
|
||||||
|
UPDATE city_rooms SET room_role = 'primary', sort_order = 0
|
||||||
|
WHERE id = $1
|
||||||
|
""", candidate['id'])
|
||||||
|
else:
|
||||||
|
print("OK: All MicroDAOs with rooms have a primary room.")
|
||||||
|
|
||||||
|
# 3. MicroDAO with MULTIPLE primary rooms
|
||||||
|
rows = await conn.fetch("""
|
||||||
|
SELECT m.id, m.slug, m.name
|
||||||
|
FROM microdaos m
|
||||||
|
JOIN city_rooms cr ON cr.microdao_id = m.id
|
||||||
|
WHERE cr.room_role = 'primary'
|
||||||
|
GROUP BY m.id, m.slug, m.name
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
""")
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
print(f"Found {len(rows)} MicroDAOs with MULTIPLE primary rooms.")
|
||||||
|
for r in rows:
|
||||||
|
print(f" - {r['name']} ({r['slug']})")
|
||||||
|
|
||||||
|
primaries = await conn.fetch("""
|
||||||
|
SELECT id, name, sort_order FROM city_rooms
|
||||||
|
WHERE microdao_id = $1 AND room_role = 'primary'
|
||||||
|
ORDER BY sort_order ASC, name ASC
|
||||||
|
""", r['id'])
|
||||||
|
|
||||||
|
# Keep the first one, demote others
|
||||||
|
keep = primaries[0]
|
||||||
|
others = primaries[1:]
|
||||||
|
|
||||||
|
print(f" -> Keeping: {keep['name']}")
|
||||||
|
for o in others:
|
||||||
|
print(f" -> Demoting: {o['name']}")
|
||||||
|
if apply:
|
||||||
|
await conn.execute("""
|
||||||
|
UPDATE city_rooms SET room_role = 'team'
|
||||||
|
WHERE id = $1
|
||||||
|
""", o['id'])
|
||||||
|
else:
|
||||||
|
print("OK: No MicroDAOs have multiple primary rooms.")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Fix MicroDAO/Agent consistency")
|
||||||
|
parser.add_argument("--apply", action="store_true", help="Apply changes to DB")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"Connecting to {DATABASE_URL}...")
|
||||||
|
try:
|
||||||
|
conn = await get_connection()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to DB: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await fix_agents(conn, args.apply)
|
||||||
|
await fix_microdaos(conn, args.apply)
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
Reference in New Issue
Block a user