fix: NODE1_REPAIR - healthchecks, dependencies, SSR env, telegram gateway
TASK_PHASE_NODE1_REPAIR: - Fix daarion-web SSR: use CITY_API_BASE_URL instead of 127.0.0.1 - Fix auth API routes: use AUTH_API_URL env var - Add wget to Dockerfiles for healthchecks (stt, ocr, web-search, swapper, vector-db, rag) - Update healthchecks to use wget instead of curl - Fix vector-db-service: update torch==2.4.0, sentence-transformers==2.6.1 - Fix rag-service: correct haystack imports for v2.x - Fix telegram-gateway: remove msg.ack() for non-JetStream NATS - Add /health endpoint to nginx mvp-routes.conf - Add room_role, is_public, sort_order columns to city_rooms migration - Add TASK_PHASE_NODE1_REPAIR.md and DEPLOY_NODE1_REPAIR.md docs Previous tasks included: - TASK 039-044: Orchestrator rooms, Matrix chat cleanup, CrewAI integration
This commit is contained in:
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { getMicrodao, getMembers, getTreasury, type MicrodaoRead, type MicrodaoMember, type TreasuryItem } from '@/api/microdao';
|
||||
import { getMicrodao, getMembers, getTreasury, uploadAsset, updateMicrodaoBranding, type MicrodaoRead, type MicrodaoMember, type TreasuryItem } from '@/api/microdao';
|
||||
import { getAgents, type AgentListItem } from '@/api/agents';
|
||||
import { MicrodaoHero } from './components/MicrodaoHero';
|
||||
import { MicrodaoBrandBadge } from './components/MicrodaoBrandBadge';
|
||||
|
||||
type TabType = 'overview' | 'members' | 'agents' | 'treasury';
|
||||
type TabType = 'overview' | 'members' | 'agents' | 'treasury' | 'settings';
|
||||
|
||||
export function MicrodaoConsolePage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
@@ -62,6 +64,26 @@ export function MicrodaoConsolePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File | undefined, type: string) => {
|
||||
if (!file || !microdao) return;
|
||||
try {
|
||||
// Optimistic UI update could be done here, but better wait for server
|
||||
const { processed_url } = await uploadAsset(file, type);
|
||||
|
||||
// Update branding
|
||||
const updated = await updateMicrodaoBranding(
|
||||
microdao.slug,
|
||||
type === 'microdao_logo' ? processed_url : undefined,
|
||||
type === 'microdao_banner' ? processed_url : undefined
|
||||
);
|
||||
|
||||
setMicrodao(prev => prev ? { ...prev, ...updated } : null);
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err);
|
||||
alert('Upload failed');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
@@ -95,81 +117,82 @@ export function MicrodaoConsolePage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
{/* Hero Header */}
|
||||
<MicrodaoHero
|
||||
name={microdao.name}
|
||||
tagline={microdao.description}
|
||||
logoUrl={microdao.logo_url}
|
||||
bannerUrl={microdao.banner_url}
|
||||
>
|
||||
<button
|
||||
onClick={() => navigate('/microdao')}
|
||||
className="px-4 py-2 bg-white/10 backdrop-blur-md border border-white/20 text-white rounded-lg hover:bg-white/20 transition-colors text-sm"
|
||||
>
|
||||
← Список
|
||||
</button>
|
||||
<Link
|
||||
to={`/dao/${microdao.slug}-governance`}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 shadow-md text-sm font-medium"
|
||||
>
|
||||
<span>🗳️</span>
|
||||
<span>Governance</span>
|
||||
</Link>
|
||||
</MicrodaoHero>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<button
|
||||
onClick={() => navigate('/microdao')}
|
||||
className="text-blue-600 hover:text-blue-700 mb-4 flex items-center gap-2"
|
||||
>
|
||||
← Назад до списку
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{microdao.name}
|
||||
</h1>
|
||||
{microdao.description && (
|
||||
<p className="text-gray-600 mt-1">{microdao.description}</p>
|
||||
)}
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
<span className="font-mono">{microdao.slug}</span> · {microdao.external_id}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
to={`/dao/${microdao.slug}-governance`}
|
||||
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 transition flex items-center gap-2 shadow-md"
|
||||
>
|
||||
<span className="text-xl">🗳️</span>
|
||||
<span className="font-semibold">DAO Governance</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 border-b border-gray-200 mt-6">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex gap-1 -mb-px pt-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'overview'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
📊 Огляд
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('members')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'members'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
👥 Учасники ({members.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('agents')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'agents'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
🤖 Агенти ({agents.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('treasury')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'treasury'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
💰 Казна
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className={`px-6 py-3 font-medium transition-colors border-b-2 ${
|
||||
activeTab === 'settings'
|
||||
? 'text-blue-600 border-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-900 border-transparent'
|
||||
}`}
|
||||
>
|
||||
⚙️ Налаштування
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,6 +321,52 @@ export function MicrodaoConsolePage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Tab */}
|
||||
{activeTab === 'settings' && microdao && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 space-y-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-6">Брендинг</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Logo Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Логотип (256x256)</label>
|
||||
<div className="flex items-center gap-6">
|
||||
<MicrodaoBrandBadge name={microdao.name} logoUrl={microdao.logo_url} size="xl" />
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => handleUpload(e.target.files?.[0], 'microdao_logo')}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-2">PNG, JPG, SVG. Макс 5MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banner Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Банер</label>
|
||||
<div className="relative w-full h-32 bg-gray-100 rounded-lg overflow-hidden mb-3 border border-gray-200 group">
|
||||
{microdao.banner_url ? (
|
||||
<img src={microdao.banner_url} className="w-full h-full object-cover" alt="Banner" />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">Немає банера</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => handleUpload(e.target.files?.[0], 'microdao_banner')}
|
||||
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMicrodaos } from './hooks/useMicrodaos';
|
||||
import { createMicrodao, type MicrodaoCreate } from '@/api/microdao';
|
||||
import { MicrodaoBrandBadge } from './components/MicrodaoBrandBadge';
|
||||
|
||||
export function MicrodaoListPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -120,24 +121,42 @@ export function MicrodaoListPage() {
|
||||
<div
|
||||
key={dao.id}
|
||||
onClick={() => navigate(`/microdao/${dao.slug}`)}
|
||||
className="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-lg hover:border-blue-500 transition-all cursor-pointer"
|
||||
className="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg hover:border-blue-500 transition-all cursor-pointer group relative"
|
||||
>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{dao.name}
|
||||
</h3>
|
||||
|
||||
{dao.description && (
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
||||
{dao.description}
|
||||
</p>
|
||||
{/* Banner Background */}
|
||||
{dao.banner_url && (
|
||||
<div
|
||||
className="absolute inset-0 h-32 bg-cover bg-center opacity-10 group-hover:opacity-20 transition-opacity"
|
||||
style={{ backgroundImage: `url(${dao.banner_url})` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<div>
|
||||
👥 {dao.member_count || 0} учасників
|
||||
|
||||
<div className="p-6 relative z-10">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<MicrodaoBrandBadge name={dao.name} logoUrl={dao.logo_url} size="md" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{dao.name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">@{dao.slug}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
🤖 {dao.agent_count || 0} агентів
|
||||
|
||||
{dao.description && (
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
||||
{dao.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500 mt-auto">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>👥</span> {dao.member_count || 0}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>🤖</span> {dao.agent_count || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
47
src/features/microdao/components/MicrodaoBrandBadge.tsx
Normal file
47
src/features/microdao/components/MicrodaoBrandBadge.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Building2 } from 'lucide-react';
|
||||
|
||||
interface MicrodaoBrandBadgeProps {
|
||||
logoUrl?: string | null;
|
||||
name: string;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MicrodaoBrandBadge: React.FC<MicrodaoBrandBadgeProps> = ({
|
||||
logoUrl,
|
||||
name,
|
||||
size = 'md',
|
||||
className = ''
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-8 h-8',
|
||||
md: 'w-12 h-12',
|
||||
lg: 'w-16 h-16',
|
||||
xl: 'w-24 h-24'
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: 16,
|
||||
md: 24,
|
||||
lg: 32,
|
||||
xl: 48
|
||||
};
|
||||
|
||||
if (logoUrl) {
|
||||
return (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
className={`rounded-full object-cover bg-gray-100 shadow-sm border border-gray-200/50 ${sizeClasses[size]} ${className}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-full bg-gradient-to-br from-blue-500/10 to-purple-500/10 flex items-center justify-center border border-gray-200 shadow-sm ${sizeClasses[size]} ${className}`}>
|
||||
<Building2 size={iconSizes[size]} className="text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
71
src/features/microdao/components/MicrodaoHero.tsx
Normal file
71
src/features/microdao/components/MicrodaoHero.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { MicrodaoBrandBadge } from './MicrodaoBrandBadge';
|
||||
|
||||
interface MicrodaoHeroProps {
|
||||
bannerUrl?: string | null;
|
||||
logoUrl?: string | null;
|
||||
name: string;
|
||||
tagline?: string | null;
|
||||
children?: React.ReactNode; // For action buttons etc.
|
||||
}
|
||||
|
||||
export const MicrodaoHero: React.FC<MicrodaoHeroProps> = ({
|
||||
bannerUrl,
|
||||
logoUrl,
|
||||
name,
|
||||
tagline,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative w-full h-48 md:h-64 lg:h-80 bg-gray-900 overflow-hidden group">
|
||||
{/* Background / Banner */}
|
||||
{bannerUrl ? (
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-cover bg-center transition-transform duration-700 group-hover:scale-105"
|
||||
style={{ backgroundImage: `url(${bannerUrl})` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 w-full h-full bg-gradient-to-r from-slate-900 via-purple-900 to-slate-900" />
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-[2px]" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute bottom-0 left-0 w-full p-6 md:p-8">
|
||||
<div className="container mx-auto flex flex-col md:flex-row items-end md:items-end gap-6">
|
||||
{/* Logo */}
|
||||
<div className="relative -mb-2 md:mb-0 shrink-0">
|
||||
<MicrodaoBrandBadge
|
||||
logoUrl={logoUrl}
|
||||
name={name}
|
||||
size="xl"
|
||||
className="ring-4 ring-black/20 shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex-1 mb-1">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-white drop-shadow-lg tracking-tight">
|
||||
{name}
|
||||
</h1>
|
||||
{tagline && (
|
||||
<p className="text-gray-200 text-lg mt-1 max-w-2xl drop-shadow-md font-light">
|
||||
{tagline}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{children && (
|
||||
<div className="flex items-center gap-3 mt-4 md:mt-0">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
90
src/features/microdao/components/RoomBrandHeader.tsx
Normal file
90
src/features/microdao/components/RoomBrandHeader.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { MicrodaoBrandBadge } from './MicrodaoBrandBadge';
|
||||
import { Hash, Users, Info } from 'lucide-react';
|
||||
|
||||
interface RoomBrandHeaderProps {
|
||||
bannerUrl?: string | null;
|
||||
logoUrl?: string | null; // Room logo
|
||||
microdaoLogoUrl?: string | null; // Fallback to MicroDAO logo
|
||||
name: string;
|
||||
description?: string | null;
|
||||
microdaoName?: string;
|
||||
membersCount?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RoomBrandHeader: React.FC<RoomBrandHeaderProps> = ({
|
||||
bannerUrl,
|
||||
logoUrl,
|
||||
microdaoLogoUrl,
|
||||
name,
|
||||
description,
|
||||
microdaoName,
|
||||
membersCount,
|
||||
children
|
||||
}) => {
|
||||
// Use room logo if available, else MicroDAO logo
|
||||
const displayLogo = logoUrl || microdaoLogoUrl;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-32 md:h-40 bg-gray-800 overflow-hidden rounded-t-xl shrink-0">
|
||||
{/* Background / Banner */}
|
||||
{bannerUrl ? (
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${bannerUrl})` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 w-full h-full bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800" />
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" />
|
||||
|
||||
{/* Top Actions (Children) */}
|
||||
{children && (
|
||||
<div className="absolute top-0 left-0 w-full p-4 z-10">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="absolute bottom-0 left-0 w-full p-4 md:p-6 flex items-end gap-4">
|
||||
<div className="shrink-0">
|
||||
<MicrodaoBrandBadge
|
||||
logoUrl={displayLogo}
|
||||
name={name}
|
||||
size="lg"
|
||||
className="ring-2 ring-black/10 shadow-lg bg-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 mb-0.5">
|
||||
<div className="flex items-center gap-2 text-gray-300 text-xs uppercase tracking-wider font-medium mb-0.5">
|
||||
{microdaoName && <span>{microdaoName}</span>}
|
||||
{membersCount !== undefined && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Users size={10} />
|
||||
{membersCount} online
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl md:text-2xl font-bold text-white flex items-center gap-2 truncate">
|
||||
<Hash className="text-gray-400 w-5 h-5 md:w-6 md:h-6 shrink-0" />
|
||||
{name}
|
||||
</h2>
|
||||
{description && (
|
||||
<p className="text-gray-300 text-sm truncate max-w-xl opacity-90">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user