feat: TASK 034-036 - MicroDAO Multi-Room Support

TASK 034: MicroDAO Multi-Room Backend
- Added migration 031_microdao_multi_room.sql
- Extended city_rooms with microdao_id, room_role, is_public, sort_order
- Added CityRoomSummary, MicrodaoRoomsList, MicrodaoRoomUpdate models
- Added get_microdao_rooms, get_microdao_rooms_by_slug functions
- Added attach_room_to_microdao, update_microdao_room functions
- Added API endpoints: GET/POST/PATCH /city/microdao/{slug}/rooms

TASK 035: MicroDAO Multi-Room UI
- Added proxy routes for rooms API
- Extended CityRoomSummary type with multi-room fields
- Added useMicrodaoRooms hook
- Created MicrodaoRoomsSection component with role labels/icons

TASK 036: MicroDAO Room Orchestrator Panel
- Created MicrodaoRoomsAdminPanel component
- Role selector, visibility toggle, set primary button
- Attach existing room form
- Integrated into /microdao/[slug] page
This commit is contained in:
Apple
2025-11-29 01:07:15 -08:00
parent 20dddd9051
commit 3f41d0e0a2
13 changed files with 1407 additions and 54 deletions

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || "http://daarion-city-service:7001";
/**
* PATCH /api/microdao/[slug]/rooms/[roomId]
* Update a MicroDAO room settings
*/
export async function PATCH(
request: NextRequest,
context: { params: Promise<{ slug: string; roomId: string }> }
) {
try {
const { slug, roomId } = await context.params;
const body = await request.text();
const response = await fetch(`${CITY_API_URL}/city/microdao/${slug}/rooms/${roomId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body,
});
if (!response.ok) {
const text = await response.text();
console.error("Failed to update microdao room:", response.status, text);
return NextResponse.json(
{ error: "Failed to update room", detail: text },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error updating microdao room:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || "http://daarion-city-service:7001";
/**
* POST /api/microdao/[slug]/rooms/attach-existing
* Attach an existing city room to a MicroDAO
*/
export async function POST(
request: NextRequest,
context: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await context.params;
const body = await request.text();
const response = await fetch(`${CITY_API_URL}/city/microdao/${slug}/rooms/attach-existing`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body,
});
if (!response.ok) {
const text = await response.text();
console.error("Failed to attach room to microdao:", response.status, text);
return NextResponse.json(
{ error: "Failed to attach room", detail: text },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error attaching room to microdao:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
const CITY_API_URL = process.env.INTERNAL_API_URL || process.env.CITY_API_BASE_URL || "http://daarion-city-service:7001";
/**
* GET /api/microdao/[slug]/rooms
* Get all rooms for a MicroDAO
*/
export async function GET(
request: NextRequest,
context: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await context.params;
const response = await fetch(`${CITY_API_URL}/city/microdao/${slug}/rooms`, {
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
});
if (!response.ok) {
const text = await response.text();
console.error("Failed to get microdao rooms:", response.status, text);
return NextResponse.json(
{ error: "Failed to get MicroDAO rooms", detail: text },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error getting microdao rooms:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -2,16 +2,24 @@
import { useParams } from "next/navigation";
import Link from "next/link";
import { useMicrodaoDetail } from "@/hooks/useMicrodao";
import { useMicrodaoDetail, useMicrodaoRooms } from "@/hooks/useMicrodao";
import { DISTRICT_COLORS } from "@/lib/microdao";
import { MicrodaoVisibilityCard } from "@/components/microdao/MicrodaoVisibilityCard";
import { MicrodaoRoomsSection } from "@/components/microdao/MicrodaoRoomsSection";
import { MicrodaoRoomsAdminPanel } from "@/components/microdao/MicrodaoRoomsAdminPanel";
import { ChevronLeft, Users, MessageSquare, Crown, Building2, Globe, Lock, Layers, BarChart3, Bot, MessageCircle } from "lucide-react";
import { CityChatWidget } from "@/components/city/CityChatWidget";
export default function MicrodaoDetailPage() {
const params = useParams();
const slug = params?.slug as string;
const { microdao, isLoading, error } = useMicrodaoDetail(slug);
const { microdao, isLoading, error, mutate: refreshMicrodao } = useMicrodaoDetail(slug);
const { rooms, mutate: refreshRooms } = useMicrodaoRooms(slug);
const handleRoomUpdated = () => {
refreshRooms();
refreshMicrodao();
};
if (isLoading) {
return (
@@ -373,35 +381,21 @@ export default function MicrodaoDetailPage() {
</div>
</section>
{/* Public Chat Room */}
<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">
<MessageCircle className="w-5 h-5 text-purple-400" />
Публічний чат MicroDAO
</h2>
{microdao.primary_city_room ? (
<div className="space-y-3">
<p className="text-sm text-slate-400">
Matrix-чат у кімнаті: <span className="text-purple-400">{microdao.primary_city_room.name}</span>
</p>
{orchestrator && (
<p className="text-xs text-slate-500">
Оркестратор: <Link href={`/agents/${orchestrator.agent_id}`} className="text-cyan-400 hover:underline">{orchestrator.display_name}</Link>
</p>
)}
<CityChatWidget roomSlug={microdao.primary_city_room.slug} />
</div>
) : (
<div className="text-center py-8 text-slate-500">
<MessageCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>Для цього MicroDAO ще не налаштована публічна кімната.</p>
<p className="text-sm mt-2 text-slate-600">
Налаштуйте primary room у City Service, щоб увімкнути чат.
</p>
</div>
)}
</section>
{/* Orchestrator Room Management Panel */}
{orchestrator && (
<MicrodaoRoomsAdminPanel
microdaoSlug={slug}
rooms={rooms.length > 0 ? rooms : (microdao.rooms || [])}
canManage={true} // TODO: check if current user is orchestrator
onRoomUpdated={handleRoomUpdated}
/>
)}
{/* Multi-Room Section with Chats */}
<MicrodaoRoomsSection
rooms={rooms.length > 0 ? rooms : (microdao.rooms || [])}
primaryRoomSlug={microdao.primary_city_room?.slug}
/>
{/* Visibility Settings (only for orchestrator) */}
{orchestrator && (

View File

@@ -0,0 +1,309 @@
"use client";
import { useState } from "react";
import { Settings, Plus, Star, Eye, EyeOff, ArrowUpDown, Check, X, Loader2 } from "lucide-react";
import { CityRoomSummary, MicrodaoRoomUpdate, AttachExistingRoomRequest } from "@/lib/types/microdao";
interface MicrodaoRoomsAdminPanelProps {
microdaoSlug: string;
rooms: CityRoomSummary[];
canManage: boolean;
onRoomUpdated?: () => void;
}
const ROLE_OPTIONS = [
{ value: "primary", label: "Основна (Primary)" },
{ value: "lobby", label: "Лобі" },
{ value: "team", label: "Командна" },
{ value: "research", label: "Дослідницька" },
{ value: "security", label: "Безпека" },
{ value: "governance", label: "Управління" },
];
export function MicrodaoRoomsAdminPanel({
microdaoSlug,
rooms,
canManage,
onRoomUpdated,
}: MicrodaoRoomsAdminPanelProps) {
const [isOpen, setIsOpen] = useState(false);
const [saving, setSaving] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
// Attach room form
const [showAttachForm, setShowAttachForm] = useState(false);
const [attachRoomId, setAttachRoomId] = useState("");
const [attachRole, setAttachRole] = useState("");
const [attachSortOrder, setAttachSortOrder] = useState(100);
if (!canManage) return null;
const handleSetPrimary = async (roomId: string) => {
setSaving(roomId);
setError(null);
try {
const res = await fetch(`/api/microdao/${microdaoSlug}/rooms/${roomId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ set_primary: true } as MicrodaoRoomUpdate),
});
if (!res.ok) {
throw new Error("Failed to set primary room");
}
onRoomUpdated?.();
} catch (err) {
setError(err instanceof Error ? err.message : "Error");
} finally {
setSaving(null);
}
};
const handleUpdateRoom = async (roomId: string, update: MicrodaoRoomUpdate) => {
setSaving(roomId);
setError(null);
try {
const res = await fetch(`/api/microdao/${microdaoSlug}/rooms/${roomId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(update),
});
if (!res.ok) {
throw new Error("Failed to update room");
}
onRoomUpdated?.();
} catch (err) {
setError(err instanceof Error ? err.message : "Error");
} finally {
setSaving(null);
}
};
const handleAttachRoom = async () => {
if (!attachRoomId.trim()) {
setError("Введіть ID кімнати");
return;
}
setSaving("attach");
setError(null);
try {
const payload: AttachExistingRoomRequest = {
room_id: attachRoomId.trim(),
room_role: attachRole || null,
is_public: true,
sort_order: attachSortOrder,
};
const res = await fetch(`/api/microdao/${microdaoSlug}/rooms/attach-existing`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "Failed to attach room");
}
// Reset form
setAttachRoomId("");
setAttachRole("");
setAttachSortOrder(100);
setShowAttachForm(false);
onRoomUpdated?.();
} catch (err) {
setError(err instanceof Error ? err.message : "Error");
} finally {
setSaving(null);
}
};
return (
<section className="bg-amber-500/5 border border-amber-500/30 rounded-xl p-4 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-slate-100 font-semibold flex items-center gap-2">
<Settings className="w-4 h-4 text-amber-400" />
Керування кімнатами MicroDAO
</h3>
<button
className="text-xs px-3 py-1.5 border border-slate-600 rounded-lg text-slate-300 hover:bg-slate-700/50 transition-colors"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? "Сховати" : "Показати"}
</button>
</div>
{isOpen && (
<div className="space-y-4">
<p className="text-xs text-slate-500">
Тут ви можете привʼязати існуючі кімнати міста до цього MicroDAO,
змінювати їх ролі, видимість та порядок сортування.
</p>
{error && (
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3 text-sm text-red-400">
{error}
</div>
)}
{/* Rooms list with controls */}
{rooms.length > 0 && (
<div className="space-y-2">
<div className="text-xs text-slate-400 uppercase tracking-wide">Поточні кімнати</div>
<div className="space-y-2">
{rooms.map((room) => (
<div
key={room.id}
className="bg-slate-900/50 border border-slate-700/30 rounded-lg p-3 flex items-center justify-between gap-3"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-200 truncate">{room.name}</span>
{room.room_role === "primary" && (
<Star className="w-3.5 h-3.5 text-amber-400 flex-shrink-0" />
)}
</div>
<div className="text-xs text-slate-500 flex items-center gap-2">
<span>{room.slug}</span>
<span></span>
<span>Sort: {room.sort_order}</span>
</div>
</div>
<div className="flex items-center gap-2">
{/* Role selector */}
<select
className="text-xs bg-slate-800 border border-slate-600 rounded px-2 py-1 text-slate-300"
value={room.room_role || ""}
onChange={(e) => handleUpdateRoom(room.id, { room_role: e.target.value || null })}
disabled={saving === room.id}
>
<option value="">Без ролі</option>
{ROLE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{/* Visibility toggle */}
<button
className={`p-1.5 rounded ${room.is_public ? "text-green-400" : "text-slate-500"} hover:bg-slate-700/50`}
onClick={() => handleUpdateRoom(room.id, { is_public: !room.is_public })}
disabled={saving === room.id}
title={room.is_public ? "Публічна" : "Приватна"}
>
{room.is_public ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
{/* Set primary button */}
{room.room_role !== "primary" && (
<button
className="p-1.5 rounded text-slate-400 hover:text-amber-400 hover:bg-slate-700/50"
onClick={() => handleSetPrimary(room.id)}
disabled={saving === room.id}
title="Зробити основною"
>
{saving === room.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Star className="w-4 h-4" />
)}
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Attach existing room form */}
<div className="border-t border-slate-700/50 pt-4">
{!showAttachForm ? (
<button
className="flex items-center gap-2 text-sm text-cyan-400 hover:text-cyan-300 transition-colors"
onClick={() => setShowAttachForm(true)}
>
<Plus className="w-4 h-4" />
Привʼязати існуючу кімнату
</button>
) : (
<div className="space-y-3 bg-slate-900/50 border border-slate-700/30 rounded-lg p-4">
<div className="text-sm text-slate-300 font-medium">Привʼязати кімнату</div>
<div className="grid gap-3 sm:grid-cols-2">
<div>
<label className="text-xs text-slate-500 block mb-1">ID кімнати</label>
<input
type="text"
value={attachRoomId}
onChange={(e) => setAttachRoomId(e.target.value)}
placeholder="room_city_..."
className="w-full bg-slate-800 border border-slate-600 rounded px-3 py-2 text-sm text-slate-200 placeholder-slate-500"
/>
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Роль</label>
<select
value={attachRole}
onChange={(e) => setAttachRole(e.target.value)}
className="w-full bg-slate-800 border border-slate-600 rounded px-3 py-2 text-sm text-slate-200"
>
<option value="">Без ролі</option>
{ROLE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<label className="text-xs text-slate-500">Порядок:</label>
<input
type="number"
value={attachSortOrder}
onChange={(e) => setAttachSortOrder(parseInt(e.target.value) || 100)}
className="w-20 bg-slate-800 border border-slate-600 rounded px-2 py-1 text-sm text-slate-200"
/>
</div>
<div className="flex items-center gap-2">
<button
className="px-3 py-1.5 text-sm text-slate-400 hover:text-slate-200 transition-colors"
onClick={() => setShowAttachForm(false)}
>
Скасувати
</button>
<button
className="px-3 py-1.5 text-sm bg-cyan-500 hover:bg-cyan-400 text-white rounded transition-colors flex items-center gap-1"
onClick={handleAttachRoom}
disabled={saving === "attach"}
>
{saving === "attach" ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Check className="w-4 h-4" />
)}
Привʼязати
</button>
</div>
</div>
</div>
)}
</div>
</div>
)}
</section>
);
}

View File

@@ -0,0 +1,143 @@
"use client";
import Link from "next/link";
import { MessageCircle, Home, Users, FlaskConical, Shield, Vote, Hash } from "lucide-react";
import { CityRoomSummary } from "@/lib/types/microdao";
import { CityChatWidget } from "@/components/city/CityChatWidget";
interface MicrodaoRoomsSectionProps {
rooms: CityRoomSummary[];
primaryRoomSlug?: string | null;
showAllChats?: boolean; // If true, show chat widgets for all rooms
}
const ROLE_LABELS: Record<string, string> = {
primary: "Основна кімната",
lobby: "Лобі",
team: "Командна кімната",
research: "Дослідницька лабораторія",
security: "Безпека",
governance: "Управління",
};
const ROLE_ICONS: Record<string, React.ReactNode> = {
primary: <Home className="w-4 h-4" />,
lobby: <MessageCircle className="w-4 h-4" />,
team: <Users className="w-4 h-4" />,
research: <FlaskConical className="w-4 h-4" />,
security: <Shield className="w-4 h-4" />,
governance: <Vote className="w-4 h-4" />,
};
const ROLE_COLORS: Record<string, string> = {
primary: "text-cyan-400 bg-cyan-500/10 border-cyan-500/30",
lobby: "text-green-400 bg-green-500/10 border-green-500/30",
team: "text-blue-400 bg-blue-500/10 border-blue-500/30",
research: "text-purple-400 bg-purple-500/10 border-purple-500/30",
security: "text-red-400 bg-red-500/10 border-red-500/30",
governance: "text-amber-400 bg-amber-500/10 border-amber-500/30",
};
export function MicrodaoRoomsSection({
rooms,
primaryRoomSlug,
showAllChats = false
}: MicrodaoRoomsSectionProps) {
if (!rooms || rooms.length === 0) {
return (
<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">
<MessageCircle className="w-5 h-5 text-cyan-400" />
Кімнати MicroDAO
</h2>
<p className="text-sm text-slate-500">
Для цього MicroDAO ще не налаштовані кімнати міста.
</p>
</section>
);
}
// Find primary room
const primary = rooms.find(r => r.slug === primaryRoomSlug)
?? rooms.find(r => r.room_role === 'primary')
?? rooms[0];
const others = rooms.filter(r => r.id !== primary.id);
return (
<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">
<MessageCircle className="w-5 h-5 text-cyan-400" />
Кімнати MicroDAO
<span className="text-sm font-normal text-slate-500">({rooms.length})</span>
</h2>
{/* Primary room with inline chat */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg border ${ROLE_COLORS[primary.room_role || 'primary'] || ROLE_COLORS.primary}`}>
{ROLE_ICONS[primary.room_role || 'primary'] || <Hash className="w-4 h-4" />}
</div>
<div>
<div className="text-base font-medium text-slate-100">{primary.name}</div>
<div className="text-xs text-slate-500">
{ROLE_LABELS[primary.room_role || 'primary'] || primary.room_role || 'Кімната'}
</div>
</div>
</div>
<Link
href={`/city/${primary.slug}`}
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors"
>
Відкрити окремо
</Link>
</div>
<CityChatWidget roomSlug={primary.slug} />
</div>
{/* Other rooms */}
{others.length > 0 && (
<div className="space-y-3">
<div className="text-sm text-slate-400 font-medium">Інші кімнати</div>
<div className="grid gap-3 md:grid-cols-2">
{others.map(room => (
<div
key={room.id}
className="bg-slate-900/50 border border-slate-700/30 rounded-xl p-4 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<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'}`}>
{ROLE_ICONS[room.room_role || ''] || <Hash className="w-3.5 h-3.5" />}
</div>
<div>
<div className="text-sm font-medium text-slate-200">{room.name}</div>
<div className="text-xs text-slate-500">
{ROLE_LABELS[room.room_role || ''] || room.room_role || 'Кімната'}
</div>
</div>
</div>
<Link
href={`/city/${room.slug}`}
className="text-xs text-cyan-400 hover:text-cyan-300 transition-colors"
>
Відкрити
</Link>
</div>
{showAllChats && (
<div className="mt-2">
<CityChatWidget roomSlug={room.slug} />
</div>
)}
</div>
))}
</div>
</div>
)}
</section>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import type { MicrodaoSummary, MicrodaoDetail } from '@/lib/types/microdao';
import type { MicrodaoSummary, MicrodaoDetail, MicrodaoRoomsList, CityRoomSummary } from '@/lib/types/microdao';
interface UseMicrodaoListOptions {
district?: string;
@@ -138,3 +138,80 @@ export function useMicrodaoDetail(
mutate: fetchData,
};
}
// =============================================================================
// useMicrodaoRooms - fetch all rooms for a MicroDAO
// =============================================================================
interface UseMicrodaoRoomsOptions {
refreshInterval?: number;
enabled?: boolean;
}
interface UseMicrodaoRoomsResult {
rooms: CityRoomSummary[];
microdaoId: string | null;
microdaoSlug: string | null;
isLoading: boolean;
error: Error | null;
mutate: () => Promise<void>;
}
export function useMicrodaoRooms(
slug: string | undefined,
options: UseMicrodaoRoomsOptions = {}
): UseMicrodaoRoomsResult {
const { refreshInterval = 60000, enabled = true } = options;
const [rooms, setRooms] = useState<CityRoomSummary[]>([]);
const [microdaoId, setMicrodaoId] = useState<string | null>(null);
const [microdaoSlug, setMicrodaoSlug] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
if (!enabled || !slug) return;
try {
setIsLoading(true);
setError(null);
const res = await fetch(`/api/microdao/${encodeURIComponent(slug)}/rooms`);
if (!res.ok) {
throw new Error('Failed to fetch MicroDAO rooms');
}
const data: MicrodaoRoomsList = await res.json();
setRooms(data.rooms || []);
setMicrodaoId(data.microdao_id);
setMicrodaoSlug(data.microdao_slug);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch'));
} finally {
setIsLoading(false);
}
}, [slug, enabled]);
// Initial fetch
useEffect(() => {
fetchData();
}, [fetchData]);
// Auto-refresh
useEffect(() => {
if (!enabled || refreshInterval <= 0) return;
const interval = setInterval(fetchData, refreshInterval);
return () => clearInterval(interval);
}, [fetchData, refreshInterval, enabled]);
return {
rooms,
microdaoId,
microdaoSlug,
isLoading,
error,
mutate: fetchData,
};
}

View File

@@ -73,7 +73,7 @@ export interface MicrodaoCitizenView {
}
// =============================================================================
// City Room Summary (for chat embedding)
// City Room Summary (for chat embedding and multi-room support)
// =============================================================================
export interface CityRoomSummary {
@@ -81,6 +81,39 @@ export interface CityRoomSummary {
slug: string;
name: string;
matrix_room_id?: string | null;
microdao_id?: string | null;
microdao_slug?: string | null;
room_role?: string | null; // 'primary', 'lobby', 'team', 'research', 'security', 'governance'
is_public?: boolean;
sort_order?: number;
}
// =============================================================================
// MicroDAO Rooms List (for /microdao/[slug]/rooms)
// =============================================================================
export interface MicrodaoRoomsList {
microdao_id: string;
microdao_slug: string;
rooms: CityRoomSummary[];
}
// =============================================================================
// MicroDAO Room Update (for management)
// =============================================================================
export interface MicrodaoRoomUpdate {
room_role?: string | null;
is_public?: boolean | null;
sort_order?: number | null;
set_primary?: boolean | null;
}
export interface AttachExistingRoomRequest {
room_id: string;
room_role?: string | null;
is_public?: boolean;
sort_order?: number;
}
// =============================================================================
@@ -114,8 +147,9 @@ export interface MicrodaoDetail {
channels: MicrodaoChannelView[];
public_citizens: MicrodaoCitizenView[];
// Primary city room for chat
// Multi-room support
primary_city_room?: CityRoomSummary | null;
rooms?: CityRoomSummary[];
}
// =============================================================================