The file /home/ccuser/opsman-control-centre/src/hooks/useSupabase.js has been updated successfully.

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { supabase } from '../lib/supabase';

function ensureSupabase() {
  if (!supabase) throw new Error('Supabase client not configured');
  return supabase;
}

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '../lib/supabase';

function ensureSupabase() {
  if (!supabase) throw new Error('Supabase client not configured');
  return supabase;
}

export function useAgentSnapshots() {
  return useQuery({
    queryKey: ['cc-agent-snapshots'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_agent_snapshots')
        .select('*')
        .order('snapshot_at', { ascending: false })
        .limit(200);
      if (error) throw error;

      const latestByAgent = new Map();
      for (const row of data || []) {
        if (!latestByAgent.has(row.agent_name)) latestByAgent.set(row.agent_name, row);
      }
      return Array.from(latestByAgent.values());
    },
    staleTime: 15_000,
  });
}

export function useTasks(filters = {}) {
  return useQuery({
    queryKey: ['cc-tasks', filters],
    queryFn: async () => {
      const sb = ensureSupabase();
      let q = sb.from('cc_tasks').select('*').order('updated_at', { ascending: false });
      if (filters.status) q = q.eq('status', filters.status);
      if (filters.agent_id) q = q.eq('agent_id', filters.agent_id);
      if (filters.project_id) q = q.eq('project_id', filters.project_id);
      const { data, error } = await q;
      if (error) throw error;
      return data || [];
    },
    staleTime: 10_000,
  });
}

export function useCouncilTopics() {
  return useQuery({
    queryKey: ['cc-council-topics'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_topics')
        .select('*')
        .order('pinned', { ascending: false })
        .order('last_activity_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCouncilMessages(topicId) {
  return useQuery({
    queryKey: ['cc-council-messages', topicId],
    enabled: !!topicId,
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .select('*')
        .eq('topic_id', topicId)
        .order('created_at', { ascending: true })
        .limit(200);
      if (error) throw error;
      return data || [];
    },
  });
}

export function useActivityFeed(limit = 20) {
  return useQuery({
    queryKey: ['cc-activity', limit],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_activity_log')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(limit);
      if (error) throw error;
      return data || [];
    },
    staleTime: 5_000,
  });
}

export function useApprovals(status = 'pending') {
  return useQuery({
    queryKey: ['cc-approvals', status],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .select('*')
        .eq('status', status)
        .order('created_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useProjects() {
  return useQuery({
    queryKey: ['cc-projects'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb.from('cc_projects').select('*').order('name');
      if (error) throw error;
      return data || [];
    },
  });
}

export function useApiCosts(days = 7) {
  return useQuery({
    queryKey: ['cc-api-costs', days],
    queryFn: async () => {
      const sb = ensureSupabase();
      const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
      const { data, error } = await sb
        .from('cc_api_costs')
        .select('*')
        .gte('recorded_at', since)
        .order('recorded_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCreateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (payload) => {
      const sb = ensureSupabase();
      const { data, error } = await sb.from('cc_tasks').insert(payload).select('*').single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-tasks'] }),
  });
}

export function useUpdateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, ...patch }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_tasks')
        .update({ ...patch, updated_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-tasks'] }),
  });
}

export function useSendCouncilMessage() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (payload) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .insert(payload)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: (_, vars) => {
      qc.invalidateQueries({ queryKey: ['cc-council-messages', vars.topic_id] });
      qc.invalidateQueries({ queryKey: ['cc-council-topics'] });
    },
  });
}

export function useApproveAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, decided_by = 'michael' }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .update({ status: 'approved', decided_by, decided_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-approvals'] }),
  });
}

export function useRejectAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, decided_by = 'michael' }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .update({ status: 'rejected', decided_by, decided_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-approvals'] }),
  });
}


Edit

export function useCouncilMessages(topicId) {
  return useQuery({
    queryKey: ['cc-council-messages', topicId],
    enabled: !!topicId,
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .select('*')
        .eq('topic_id', topicId)
        .order('created_at', { ascending: true })
        .limit(200);
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCouncilMessages(topicId) {
  const qc = useQueryClient();

  const query = useQuery({
    queryKey: ['cc-council-messages', topicId],
    enabled: !!topicId,
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .select('*')
        .eq('topic_id', topicId)
        .order('created_at', { ascending: true })
        .limit(200);
      if (error) throw error;
      return data || [];
    },
  });

  // Realtime subscription for new messages
  useEffect(() => {
    if (!topicId || !supabase) return;

    const channel = supabase
      .channel(`council-messages-${topicId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'cc_council_messages',
          filter: `topic_id=eq.${topicId}`,
        },
        () => {
          qc.invalidateQueries({ queryKey: ['cc-council-messages', topicId] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [topicId, qc]);

  return query;
}


export function useCouncilMessages(topicId) {
  const qc = useQueryClient();

  const query = useQuery({
    queryKey: ['cc-council-messages', topicId],
    enabled: !!topicId,
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .select('*')
        .eq('topic_id', topicId)
        .order('created_at', { ascending: true })
        .limit(200);
      if (error) throw error;
      return data || [];
    },
  });

  // Realtime subscription for new messages
  useEffect(() => {
    if (!topicId || !supabase) return;

    const channel = supabase
      .channel(`council-messages-${topicId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'cc_council_messages',
          filter: `topic_id=eq.${topicId}`,
        },
        () => {
          qc.invalidateQueries({ queryKey: ['cc-council-messages', topicId] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [topicId, qc]);

  return query;
}

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { supabase } from '../lib/supabase';

function ensureSupabase() {
  if (!supabase) throw new Error('Supabase client not configured');
  return supabase;
}

export function useAgentSnapshots() {
  return useQuery({
    queryKey: ['cc-agent-snapshots'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_agent_snapshots')
        .select('*')
        .order('snapshot_at', { ascending: false })
        .limit(200);
      if (error) throw error;

      const latestByAgent = new Map();
      for (const row of data || []) {
        if (!latestByAgent.has(row.agent_name)) latestByAgent.set(row.agent_name, row);
      }
      return Array.from(latestByAgent.values());
    },
    staleTime: 15_000,
  });
}

export function useTasks(filters = {}) {
  return useQuery({
    queryKey: ['cc-tasks', filters],
    queryFn: async () => {
      const sb = ensureSupabase();
      let q = sb.from('cc_tasks').select('*').order('updated_at', { ascending: false });
      if (filters.status) q = q.eq('status', filters.status);
      if (filters.agent_id) q = q.eq('agent_id', filters.agent_id);
      if (filters.project_id) q = q.eq('project_id', filters.project_id);
      const { data, error } = await q;
      if (error) throw error;
      return data || [];
    },
    staleTime: 10_000,
  });
}

export function useCouncilTopics() {
  return useQuery({
    queryKey: ['cc-council-topics'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_topics')
        .select('*')
        .order('pinned', { ascending: false })
        .order('last_activity_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCouncilMessages(topicId) {
  return useQuery({
    queryKey: ['cc-council-messages', topicId],
    enabled: !!topicId,
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .select('*')
        .eq('topic_id', topicId)
        .order('created_at', { ascending: true })
        .limit(200);
      if (error) throw error;
      return data || [];
    },
  });
}

export function useActivityFeed(limit = 20) {
  return useQuery({
    queryKey: ['cc-activity', limit],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_activity_log')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(limit);
      if (error) throw error;
      return data || [];
    },
    staleTime: 5_000,
  });
}

export function useApprovals(status = 'pending') {
  return useQuery({
    queryKey: ['cc-approvals', status],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .select('*')
        .eq('status', status)
        .order('created_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useProjects() {
  return useQuery({
    queryKey: ['cc-projects'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb.from('cc_projects').select('*').order('name');
      if (error) throw error;
      return data || [];
    },
  });
}

export function useApiCosts(days = 7) {
  return useQuery({
    queryKey: ['cc-api-costs', days],
    queryFn: async () => {
      const sb = ensureSupabase();
      const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
      const { data, error } = await sb
        .from('cc_api_costs')
        .select('*')
        .gte('recorded_at', since)
        .order('recorded_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCreateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (payload) => {
      const sb = ensureSupabase();
      const { data, error } = await sb.from('cc_tasks').insert(payload).select('*').single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-tasks'] }),
  });
}

export function useUpdateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, ...patch }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_tasks')
        .update({ ...patch, updated_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-tasks'] }),
  });
}

export function useSendCouncilMessage() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (payload) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .insert(payload)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: (_, vars) => {
      qc.invalidateQueries({ queryKey: ['cc-council-messages', vars.topic_id] });
      qc.invalidateQueries({ queryKey: ['cc-council-topics'] });
    },
  });
}

export function useApproveAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, decided_by = 'michael' }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .update({ status: 'approved', decided_by, decided_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-approvals'] }),
  });
}

export function useRejectAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, decided_by = 'michael' }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .update({ status: 'rejected', decided_by, decided_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-approvals'] }),
  });
}

+  // Realtime subscription for new messages

+      .channel(`council-messages-${topicId}`)

+          table: 'cc_council_messages',

+          filter: `topic_id=eq.${topicId}`,

+          qc.invalidateQueries({ queryKey: ['cc-council-messages', topicId] });


export function useActivityFeed(limit = 20) {
  return useQuery({
    queryKey: ['cc-activity', limit],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_activity_log')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(limit);
      if (error) throw error;
      return data || [];
    },
    staleTime: 5_000,
  });
}

export function useActivityFeed(limit = 20) {
  const qc = useQueryClient();

  const query = useQuery({
    queryKey: ['cc-activity', limit],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_activity_log')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(limit);
      if (error) throw error;
      return data || [];
    },
    staleTime: 5_000,
  });

  // Realtime subscription for activity updates
  useEffect(() => {
    if (!supabase) return;

    const channel = supabase
      .channel('activity-feed')
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'cc_activity_log',
        },
        () => {
          qc.invalidateQueries({ queryKey: ['cc-activity'] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [qc]);

  return query;
}


export function useActivityFeed(limit = 20) {
  const qc = useQueryClient();

  const query = useQuery({
    queryKey: ['cc-activity', limit],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_activity_log')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(limit);
      if (error) throw error;
      return data || [];
    },
    staleTime: 5_000,
  });

  // Realtime subscription for activity updates
  useEffect(() => {
    if (!supabase) return;

    const channel = supabase
      .channel('activity-feed')
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'cc_activity_log',
        },
        () => {
          qc.invalidateQueries({ queryKey: ['cc-activity'] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [qc]);

  return query;
}

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { supabase } from '../lib/supabase';

function ensureSupabase() {
  if (!supabase) throw new Error('Supabase client not configured');
  return supabase;
}

export function useAgentSnapshots() {
  return useQuery({
    queryKey: ['cc-agent-snapshots'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_agent_snapshots')
        .select('*')
        .order('snapshot_at', { ascending: false })
        .limit(200);
      if (error) throw error;

      const latestByAgent = new Map();
      for (const row of data || []) {
        if (!latestByAgent.has(row.agent_name)) latestByAgent.set(row.agent_name, row);
      }
      return Array.from(latestByAgent.values());
    },
    staleTime: 15_000,
  });
}

export function useTasks(filters = {}) {
  return useQuery({
    queryKey: ['cc-tasks', filters],
    queryFn: async () => {
      const sb = ensureSupabase();
      let q = sb.from('cc_tasks').select('*').order('updated_at', { ascending: false });
      if (filters.status) q = q.eq('status', filters.status);
      if (filters.agent_id) q = q.eq('agent_id', filters.agent_id);
      if (filters.project_id) q = q.eq('project_id', filters.project_id);
      const { data, error } = await q;
      if (error) throw error;
      return data || [];
    },
    staleTime: 10_000,
  });
}

export function useCouncilTopics() {
  return useQuery({
    queryKey: ['cc-council-topics'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_topics')
        .select('*')
        .order('pinned', { ascending: false })
        .order('last_activity_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCouncilMessages(topicId) {
  const qc = useQueryClient();

  const query = useQuery({
    queryKey: ['cc-council-messages', topicId],
    enabled: !!topicId,
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .select('*')
        .eq('topic_id', topicId)
        .order('created_at', { ascending: true })
        .limit(200);
      if (error) throw error;
      return data || [];
    },
  });

  // Realtime subscription for new messages
  useEffect(() => {
    if (!topicId || !supabase) return;

    const channel = supabase
      .channel(`council-messages-${topicId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'cc_council_messages',
          filter: `topic_id=eq.${topicId}`,
        },
        () => {
          qc.invalidateQueries({ queryKey: ['cc-council-messages', topicId] });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [topicId, qc]);

  return query;
}

export function useActivityFeed(limit = 20) {
  return useQuery({
    queryKey: ['cc-activity', limit],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_activity_log')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(limit);
      if (error) throw error;
      return data || [];
    },
    staleTime: 5_000,
  });
}

export function useApprovals(status = 'pending') {
  return useQuery({
    queryKey: ['cc-approvals', status],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .select('*')
        .eq('status', status)
        .order('created_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useProjects() {
  return useQuery({
    queryKey: ['cc-projects'],
    queryFn: async () => {
      const sb = ensureSupabase();
      const { data, error } = await sb.from('cc_projects').select('*').order('name');
      if (error) throw error;
      return data || [];
    },
  });
}

export function useApiCosts(days = 7) {
  return useQuery({
    queryKey: ['cc-api-costs', days],
    queryFn: async () => {
      const sb = ensureSupabase();
      const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
      const { data, error } = await sb
        .from('cc_api_costs')
        .select('*')
        .gte('recorded_at', since)
        .order('recorded_at', { ascending: false });
      if (error) throw error;
      return data || [];
    },
  });
}

export function useCreateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (payload) => {
      const sb = ensureSupabase();
      const { data, error } = await sb.from('cc_tasks').insert(payload).select('*').single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-tasks'] }),
  });
}

export function useUpdateTask() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, ...patch }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_tasks')
        .update({ ...patch, updated_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-tasks'] }),
  });
}

export function useSendCouncilMessage() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (payload) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_council_messages')
        .insert(payload)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: (_, vars) => {
      qc.invalidateQueries({ queryKey: ['cc-council-messages', vars.topic_id] });
      qc.invalidateQueries({ queryKey: ['cc-council-topics'] });
    },
  });
}

export function useApproveAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, decided_by = 'michael' }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .update({ status: 'approved', decided_by, decided_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-approvals'] }),
  });
}

export function useRejectAction() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async ({ id, decided_by = 'michael' }) => {
      const sb = ensureSupabase();
      const { data, error } = await sb
        .from('cc_approvals')
        .update({ status: 'rejected', decided_by, decided_at: new Date().toISOString() })
        .eq('id', id)
        .select('*')
        .single();
      if (error) throw error;
      return data;
    },
    onSuccess: () => qc.invalidateQueries({ queryKey: ['cc-approvals'] }),
  });
}

+  // Realtime subscription for activity updates

+          qc.invalidateQueries({ queryKey: ['cc-activity'] });


Now let's wire up the Council page:


import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';

import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';
import { useCouncilTopics, useCouncilMessages, useSendCouncilMessage } from '../hooks/useSupabase';


The file /home/ccuser/opsman-control-centre/src/pages/Council.jsx has been updated successfully.

import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';
import { useCouncilTopics, useCouncilMessages, useSendCouncilMessage } from '../hooks/useSupabase';

/**
 * Council — Multi-Agent Discussion
 * ==================================
 * Chat room where Michael talks to agents individually or as a group.
 * Topics on the left, messages on the right.
 *
 * DATA: cc_council_topics + cc_council_messages in Supabase
 * API: councilApi in src/lib/api.js
 * REALTIME: Subscribe to cc_council_messages for live updates
 *
 * REPLACING DEMO DATA:
 *   const { data: topics } = useQuery({ queryKey: ['council-topics'], queryFn: councilApi.getTopics });
 *   const { data: messages } = useQuery({
 *     queryKey: ['council-messages', activeTopic],
 *     queryFn: () => councilApi.getMessages(activeTopic)
 *   });
 */
import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';

// ── Demo topics ──────────────────────────────────────────
const DEMO_TOPICS = [
  { id: 'general', title: 'General', pinned: true, lastActivity: '2m ago' },
  { id: 'deploys', title: 'Deployments', pinned: false, lastActivity: '1h ago' },
  { id: 'growth-q2', title: 'Q2 Growth Strategy', pinned: true, lastActivity: '3h ago' },
  { id: 'bugs', title: 'Bug Reports', pinned: false, lastActivity: '5h ago' },
  { id: 'watercooler-archive', title: 'Off Topic', pinned: false, lastActivity: '1d ago' },
];

// ── Demo messages per topic ──────────────────────────────
const DEMO_MESSAGES = {
  general: [
    { id: 1, sender: 'michael', type: 'michael', content: 'Morning team. What\'s the priority today?', time: '9:00 AM', pinned: false },
    { id: 2, sender: 'rivet', type: 'agent', content: 'Morning boss. Top priorities: Builder has the SMS retry fix ready for review, Susan\'s running batch #48, and Sentinel flagged RAM at 78%. I\'d say approve Builder\'s deploy first — it\'s blocking Susan\'s batch reliability.', time: '9:01 AM', pinned: false },
    { id: 3, sender: 'builder', type: 'agent', content: 'Confirmed — the SMS retry fix uses exponential backoff with jitter. Tested against Twilio\'s sandbox. Zero failed sends in 500 test messages. Ready when you are.', time: '9:02 AM', pinned: false },
    { id: 4, sender: 'susan', type: 'agent', content: 'That fix would be huge for me. Lost 3 sends to 429s yesterday. Batch #48 has 847 leads queued — best run it after the deploy.', time: '9:03 AM', pinned: false },
    { id: 5, sender: 'sentinel', type: 'agent', content: 'RAM note: it\'s Radar\'s competitor scraper. Peaks at 78% during scans then drops. Not critical but worth watching. Cog, could we bump the VPS to 16GB?', time: '9:04 AM', pinned: false },
    { id: 6, sender: 'cog', type: 'agent', content: 'Can do, but it\'s $20/mo more on DO. Alternative: I can optimize Radar\'s scraper to stream results instead of loading all to memory. That\'d cut peak usage by ~30%.', time: '9:05 AM', pinned: false },
    { id: 7, sender: 'michael', type: 'michael', content: 'Good thinking Cog. Do the optimization first, we\'ll upgrade if it\'s still tight. Builder — approved, ship the SMS fix. Susan, run batch #48 after deploy confirms.', time: '9:07 AM', pinned: true },
    { id: 8, sender: 'rivet', type: 'agent', content: 'Copy that. Builder: deploy GE v2.4. Susan: hold batch #48 until Builder confirms. Sentinel: keep monitoring RAM. Cog: start Radar optimizer today. I\'ll track all four.', time: '9:08 AM', pinned: false },
    { id: 9, sender: 'harper', type: 'agent', content: 'FYI — the LinkedIn post I published yesterday hit 500 views and 23 reactions. Best performer this month. Should I double down on that topic (hiring tips)?', time: '9:15 AM', pinned: false },
    { id: 10, sender: 'radar', type: 'agent', content: 'Intel update: Hipages raised their Gold tier pricing 15% this quarter. Our value prop just got stronger. Harper, that could make a great LinkedIn angle.', time: '9:20 AM', pinned: false },
  ],
  deploys: [
    { id: 20, sender: 'builder', type: 'agent', content: 'GE v2.3.1 deployed successfully. Zero downtime. SMS retry logic active.', time: '2:30 PM', pinned: false },
    { id: 21, sender: 'rivet', type: 'agent', content: 'Confirmed — monitoring post-deploy. All health checks passing.', time: '2:31 PM', pinned: false },
  ],
  'growth-q2': [
    { id: 30, sender: 'michael', type: 'michael', content: 'Team, let\'s plan Q2. Our targets: 100 contractors, $50K ARR, 3 enterprise clients. Thoughts?', time: 'Yesterday', pinned: true },
    { id: 31, sender: 'radar', type: 'agent', content: 'Based on my analysis, the construction hiring market in Sydney is growing 12% YoY. Hipages\' price hike creates an opening. I recommend targeting their dissatisfied Gold tier customers.', time: 'Yesterday', pinned: false },
    { id: 32, sender: 'susan', type: 'agent', content: 'I can run targeted SMS campaigns to ex-Hipages users if we can get the list. Radar, can you scrape their public reviews for contact signals?', time: 'Yesterday', pinned: false },
  ],
};

// ── Topic Sidebar ────────────────────────────────────────
function TopicList({ topics, activeTopic, onSelect, onCreateTopic }) {
  return (

Topics

        <button
          onClick={onCreateTopic}
          className="w-6 h-6 rounded-md bg-[var(--cc-surface-hover)] hover:bg-[var(--cc-surface-active)] flex items-center justify-center transition-colors"
          title="New topic"
        >
          <Plus className="w-3 h-3 text-[var(--cc-text-muted)]" />
        </button>

        {topics.map(topic => (
          <button
            key={topic.id}
            onClick={() => onSelect(topic.id)}
            className={`w-full text-left px-3 py-2.5 flex items-center gap-2 transition-colors ${
              activeTopic === topic.id
                ? 'bg-[var(--cc-accent-glow)] text-[var(--cc-accent)]'
                : 'text-[var(--cc-text-secondary)] hover:bg-[var(--cc-surface-hover)] hover:text-[var(--cc-text)]'
            }`}
          >
            {topic.pinned ? <Pin className="w-3 h-3 flex-shrink-0" /> : <Hash className="w-3 h-3 flex-shrink-0" />}
            {topic.title}
            {topic.lastActivity}
          </button>
        ))}

  );
}

// ── Message Bubble ───────────────────────────────────────
function MessageBubble({ msg }) {
  const isMichael = msg.type === 'michael';
  const agent = !isMichael ? getAgent(msg.sender) : null;

  return (
    <div className={`flex gap-3 px-4 py-2.5 ${msg.pinned ? 'bg-amber-500/5 border-l-2 border-l-amber-500/50' : ''}`}>
      {/* Avatar */}

        {isMichael ? '👤' : agent?.avatar || '🤖'}

      {/* Content */}

            {isMichael ? 'Michael' : agent?.name || msg.sender}

          {!isMichael && (
            {agent?.role}
          )}
          {msg.time}
          {msg.pinned && <Pin className="w-3 h-3 text-amber-400" />}

          {msg.content}

  );
}

// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  const messages = DEMO_MESSAGES[activeTopic] || [];

  // Auto-scroll to bottom
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length, activeTopic]);

  const handleSend = () => {
    if (!message.trim()) return;
    // TODO: councilApi.sendMessage(activeTopic, { content: message, sender: 'michael', mentions: sendTo === 'all' ? [] : [sendTo] })
    console.log('Send to council:', { topic: activeTopic, message, sendTo });
    setMessage('');
  };

  const activeTopicData = DEMO_TOPICS.find(t => t.id === activeTopic);

  return (

      <Card className="flex flex-col md:flex-row overflow-hidden" style={{ height: 'calc(100vh - var(--header-height) - 48px)' }}>
        {/* Mobile topic toggle */}
        <button
          onClick={() => setShowMobileSidebar(!showMobileSidebar)}
          className="md:hidden flex items-center gap-2 px-4 py-2.5 border-b border-[var(--cc-border)] text-[12px] font-medium text-[var(--cc-text-secondary)]"
        >
          <Hash className="w-3.5 h-3.5" />
          {activeTopicData?.title || 'Select topic'}
        </button>

        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={DEMO_TOPICS}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />

        {/* Messages area */}

          {/* Topic header */}

              <Hash className="w-4 h-4 text-[var(--cc-text-muted)]" />

{activeTopicData?.title}

              <Users className="w-3.5 h-3.5 text-[var(--cc-text-muted)]" />

                {agents.length + 1} members

          {/* Messages */}

            {messages.map(msg => (
              <MessageBubble key={msg.id} msg={msg} />
            ))}

          {/* Input area */}

            {/* Send-to selector */}

              Send to:
              <select
                value={sendTo}
                onChange={e => setSendTo(e.target.value)}
                className="bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded px-2 py-0.5 text-[10px] text-[var(--cc-text)] outline-none"
              >
                <option value="all">All agents</option>
                {agents.map(a => <option key={a.id} value={a.id}>{a.avatar} {a.name}</option>)}
              </select>

              <input
                type="text"
                value={message}
                onChange={e => setMessage(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && handleSend()}
                placeholder={`Message #${activeTopicData?.title || 'channel'}...`}
                className="flex-1 bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded-lg px-3 py-2.5 text-[12px] text-[var(--cc-text)] placeholder-[var(--cc-text-muted)] outline-none focus:border-[var(--cc-accent)]"
              />
              <button
                onClick={handleSend}
                disabled={!message.trim()}
                className="w-9 h-9 rounded-lg bg-[var(--cc-accent)] hover:bg-[var(--cc-accent-dim)] flex items-center justify-center transition-colors disabled:opacity-40"
              >
                <Send className="w-4 h-4 text-white" />
              </button>

      </Card>

  );
}

+import { useCouncilTopics, useCouncilMessages, useSendCouncilMessage } from '../hooks/useSupabase';


// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  const messages = DEMO_MESSAGES[activeTopic] || [];

// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  // Fetch topics and messages from Supabase
  const { data: topicsData } = useCouncilTopics();
  const { data: messagesData } = useCouncilMessages(activeTopic);
  const sendMessageMutation = useSendCouncilMessage();

  // Map topics (fallback to demo if empty)
  const topics = topicsData && topicsData.length > 0
    ? topicsData.map(t => ({
        id: t.id,
        title: t.title,
        pinned: t.pinned,
        lastActivity: new Date(t.last_activity_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
      }))
    : DEMO_TOPICS;

  // Map messages (fallback to demo if empty)
  const mappedMessages = messagesData && messagesData.length > 0
    ? messagesData.map(m => ({
        id: m.id,
        sender: m.sender_id,
        type: m.sender_id === 'michael' ? 'michael' : 'agent',
        content: m.content,
        time: new Date(m.created_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
        pinned: m.pinned || false,
      }))
    : [];

  const messages = mappedMessages.length > 0 ? mappedMessages : (DEMO_MESSAGES[activeTopic] || []);


// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  // Fetch topics and messages from Supabase
  const { data: topicsData } = useCouncilTopics();
  const { data: messagesData } = useCouncilMessages(activeTopic);
  const sendMessageMutation = useSendCouncilMessage();

  // Map topics (fallback to demo if empty)
  const topics = topicsData && topicsData.length > 0
    ? topicsData.map(t => ({
        id: t.id,
        title: t.title,
        pinned: t.pinned,
        lastActivity: new Date(t.last_activity_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
      }))
    : DEMO_TOPICS;

  // Map messages (fallback to demo if empty)
  const mappedMessages = messagesData && messagesData.length > 0
    ? messagesData.map(m => ({
        id: m.id,
        sender: m.sender_id,
        type: m.sender_id === 'michael' ? 'michael' : 'agent',
        content: m.content,
        time: new Date(m.created_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
        pinned: m.pinned || false,
      }))
    : [];

  const messages = mappedMessages.length > 0 ? mappedMessages : (DEMO_MESSAGES[activeTopic] || []);

/**
 * Council — Multi-Agent Discussion
 * ==================================
 * Chat room where Michael talks to agents individually or as a group.
 * Topics on the left, messages on the right.
 *
 * DATA: cc_council_topics + cc_council_messages in Supabase
 * API: councilApi in src/lib/api.js
 * REALTIME: Subscribe to cc_council_messages for live updates
 *
 * REPLACING DEMO DATA:
 *   const { data: topics } = useQuery({ queryKey: ['council-topics'], queryFn: councilApi.getTopics });
 *   const { data: messages } = useQuery({
 *     queryKey: ['council-messages', activeTopic],
 *     queryFn: () => councilApi.getMessages(activeTopic)
 *   });
 */
import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';
import { useCouncilTopics, useCouncilMessages, useSendCouncilMessage } from '../hooks/useSupabase';

// ── Demo topics ──────────────────────────────────────────
const DEMO_TOPICS = [
  { id: 'general', title: 'General', pinned: true, lastActivity: '2m ago' },
  { id: 'deploys', title: 'Deployments', pinned: false, lastActivity: '1h ago' },
  { id: 'growth-q2', title: 'Q2 Growth Strategy', pinned: true, lastActivity: '3h ago' },
  { id: 'bugs', title: 'Bug Reports', pinned: false, lastActivity: '5h ago' },
  { id: 'watercooler-archive', title: 'Off Topic', pinned: false, lastActivity: '1d ago' },
];

// ── Demo messages per topic ──────────────────────────────
const DEMO_MESSAGES = {
  general: [
    { id: 1, sender: 'michael', type: 'michael', content: 'Morning team. What\'s the priority today?', time: '9:00 AM', pinned: false },
    { id: 2, sender: 'rivet', type: 'agent', content: 'Morning boss. Top priorities: Builder has the SMS retry fix ready for review, Susan\'s running batch #48, and Sentinel flagged RAM at 78%. I\'d say approve Builder\'s deploy first — it\'s blocking Susan\'s batch reliability.', time: '9:01 AM', pinned: false },
    { id: 3, sender: 'builder', type: 'agent', content: 'Confirmed — the SMS retry fix uses exponential backoff with jitter. Tested against Twilio\'s sandbox. Zero failed sends in 500 test messages. Ready when you are.', time: '9:02 AM', pinned: false },
    { id: 4, sender: 'susan', type: 'agent', content: 'That fix would be huge for me. Lost 3 sends to 429s yesterday. Batch #48 has 847 leads queued — best run it after the deploy.', time: '9:03 AM', pinned: false },
    { id: 5, sender: 'sentinel', type: 'agent', content: 'RAM note: it\'s Radar\'s competitor scraper. Peaks at 78% during scans then drops. Not critical but worth watching. Cog, could we bump the VPS to 16GB?', time: '9:04 AM', pinned: false },
    { id: 6, sender: 'cog', type: 'agent', content: 'Can do, but it\'s $20/mo more on DO. Alternative: I can optimize Radar\'s scraper to stream results instead of loading all to memory. That\'d cut peak usage by ~30%.', time: '9:05 AM', pinned: false },
    { id: 7, sender: 'michael', type: 'michael', content: 'Good thinking Cog. Do the optimization first, we\'ll upgrade if it\'s still tight. Builder — approved, ship the SMS fix. Susan, run batch #48 after deploy confirms.', time: '9:07 AM', pinned: true },
    { id: 8, sender: 'rivet', type: 'agent', content: 'Copy that. Builder: deploy GE v2.4. Susan: hold batch #48 until Builder confirms. Sentinel: keep monitoring RAM. Cog: start Radar optimizer today. I\'ll track all four.', time: '9:08 AM', pinned: false },
    { id: 9, sender: 'harper', type: 'agent', content: 'FYI — the LinkedIn post I published yesterday hit 500 views and 23 reactions. Best performer this month. Should I double down on that topic (hiring tips)?', time: '9:15 AM', pinned: false },
    { id: 10, sender: 'radar', type: 'agent', content: 'Intel update: Hipages raised their Gold tier pricing 15% this quarter. Our value prop just got stronger. Harper, that could make a great LinkedIn angle.', time: '9:20 AM', pinned: false },
  ],
  deploys: [
    { id: 20, sender: 'builder', type: 'agent', content: 'GE v2.3.1 deployed successfully. Zero downtime. SMS retry logic active.', time: '2:30 PM', pinned: false },
    { id: 21, sender: 'rivet', type: 'agent', content: 'Confirmed — monitoring post-deploy. All health checks passing.', time: '2:31 PM', pinned: false },
  ],
  'growth-q2': [
    { id: 30, sender: 'michael', type: 'michael', content: 'Team, let\'s plan Q2. Our targets: 100 contractors, $50K ARR, 3 enterprise clients. Thoughts?', time: 'Yesterday', pinned: true },
    { id: 31, sender: 'radar', type: 'agent', content: 'Based on my analysis, the construction hiring market in Sydney is growing 12% YoY. Hipages\' price hike creates an opening. I recommend targeting their dissatisfied Gold tier customers.', time: 'Yesterday', pinned: false },
    { id: 32, sender: 'susan', type: 'agent', content: 'I can run targeted SMS campaigns to ex-Hipages users if we can get the list. Radar, can you scrape their public reviews for contact signals?', time: 'Yesterday', pinned: false },
  ],
};

// ── Topic Sidebar ────────────────────────────────────────
function TopicList({ topics, activeTopic, onSelect, onCreateTopic }) {
  return (

Topics

        <button
          onClick={onCreateTopic}
          className="w-6 h-6 rounded-md bg-[var(--cc-surface-hover)] hover:bg-[var(--cc-surface-active)] flex items-center justify-center transition-colors"
          title="New topic"
        >
          <Plus className="w-3 h-3 text-[var(--cc-text-muted)]" />
        </button>

        {topics.map(topic => (
          <button
            key={topic.id}
            onClick={() => onSelect(topic.id)}
            className={`w-full text-left px-3 py-2.5 flex items-center gap-2 transition-colors ${
              activeTopic === topic.id
                ? 'bg-[var(--cc-accent-glow)] text-[var(--cc-accent)]'
                : 'text-[var(--cc-text-secondary)] hover:bg-[var(--cc-surface-hover)] hover:text-[var(--cc-text)]'
            }`}
          >
            {topic.pinned ? <Pin className="w-3 h-3 flex-shrink-0" /> : <Hash className="w-3 h-3 flex-shrink-0" />}
            {topic.title}
            {topic.lastActivity}
          </button>
        ))}

  );
}

// ── Message Bubble ───────────────────────────────────────
function MessageBubble({ msg }) {
  const isMichael = msg.type === 'michael';
  const agent = !isMichael ? getAgent(msg.sender) : null;

  return (
    <div className={`flex gap-3 px-4 py-2.5 ${msg.pinned ? 'bg-amber-500/5 border-l-2 border-l-amber-500/50' : ''}`}>
      {/* Avatar */}

        {isMichael ? '👤' : agent?.avatar || '🤖'}

      {/* Content */}

            {isMichael ? 'Michael' : agent?.name || msg.sender}

          {!isMichael && (
            {agent?.role}
          )}
          {msg.time}
          {msg.pinned && <Pin className="w-3 h-3 text-amber-400" />}

          {msg.content}

  );
}

// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  const messages = DEMO_MESSAGES[activeTopic] || [];

  // Auto-scroll to bottom
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length, activeTopic]);

  const handleSend = () => {
    if (!message.trim()) return;
    // TODO: councilApi.sendMessage(activeTopic, { content: message, sender: 'michael', mentions: sendTo === 'all' ? [] : [sendTo] })
    console.log('Send to council:', { topic: activeTopic, message, sendTo });
    setMessage('');
  };

  const activeTopicData = DEMO_TOPICS.find(t => t.id === activeTopic);

  return (

      <Card className="flex flex-col md:flex-row overflow-hidden" style={{ height: 'calc(100vh - var(--header-height) - 48px)' }}>
        {/* Mobile topic toggle */}
        <button
          onClick={() => setShowMobileSidebar(!showMobileSidebar)}
          className="md:hidden flex items-center gap-2 px-4 py-2.5 border-b border-[var(--cc-border)] text-[12px] font-medium text-[var(--cc-text-secondary)]"
        >
          <Hash className="w-3.5 h-3.5" />
          {activeTopicData?.title || 'Select topic'}
        </button>

        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={DEMO_TOPICS}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />

        {/* Messages area */}

          {/* Topic header */}

              <Hash className="w-4 h-4 text-[var(--cc-text-muted)]" />

{activeTopicData?.title}

              <Users className="w-3.5 h-3.5 text-[var(--cc-text-muted)]" />

                {agents.length + 1} members

          {/* Messages */}

            {messages.map(msg => (
              <MessageBubble key={msg.id} msg={msg} />
            ))}

          {/* Input area */}

            {/* Send-to selector */}

              Send to:
              <select
                value={sendTo}
                onChange={e => setSendTo(e.target.value)}
                className="bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded px-2 py-0.5 text-[10px] text-[var(--cc-text)] outline-none"
              >
                <option value="all">All agents</option>
                {agents.map(a => <option key={a.id} value={a.id}>{a.avatar} {a.name}</option>)}
              </select>

              <input
                type="text"
                value={message}
                onChange={e => setMessage(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && handleSend()}
                placeholder={`Message #${activeTopicData?.title || 'channel'}...`}
                className="flex-1 bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded-lg px-3 py-2.5 text-[12px] text-[var(--cc-text)] placeholder-[var(--cc-text-muted)] outline-none focus:border-[var(--cc-accent)]"
              />
              <button
                onClick={handleSend}
                disabled={!message.trim()}
                className="w-9 h-9 rounded-lg bg-[var(--cc-accent)] hover:bg-[var(--cc-accent-dim)] flex items-center justify-center transition-colors disabled:opacity-40"
              >
                <Send className="w-4 h-4 text-white" />
              </button>

      </Card>

  );
}

-  const messages = DEMO_MESSAGES[activeTopic] || [];

+  // Fetch topics and messages from Supabase

+  const { data: topicsData } = useCouncilTopics();

+  const { data: messagesData } = useCouncilMessages(activeTopic);

+  const sendMessageMutation = useSendCouncilMessage();

+  // Map topics (fallback to demo if empty)

+  const topics = topicsData && topicsData.length > 0

+        lastActivity: new Date(t.last_activity_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),

+  // Map messages (fallback to demo if empty)

+  const mappedMessages = messagesData && messagesData.length > 0

+        type: m.sender_id === 'michael' ? 'michael' : 'agent',

+        time: new Date(m.created_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),

+  const messages = mappedMessages.length > 0 ? mappedMessages : (DEMO_MESSAGES[activeTopic] || []);


  const handleSend = () => {
    if (!message.trim()) return;
    // TODO: councilApi.sendMessage(activeTopic, { content: message, sender: 'michael', mentions: sendTo === 'all' ? [] : [sendTo] })
    console.log('Send to council:', { topic: activeTopic, message, sendTo });
    setMessage('');
  };

  const activeTopicData = DEMO_TOPICS.find(t => t.id === activeTopic);

  const handleSend = () => {
    if (!message.trim()) return;

    sendMessageMutation.mutate({
      topic_id: activeTopic,
      sender_id: 'michael',
      content: message.trim(),
      mentions: sendTo === 'all' ? [] : [sendTo],
    });
    setMessage('');
  };

  const activeTopicData = topics.find(t => t.id === activeTopic);


  const handleSend = () => {
    if (!message.trim()) return;

    sendMessageMutation.mutate({
      topic_id: activeTopic,
      sender_id: 'michael',
      content: message.trim(),
      mentions: sendTo === 'all' ? [] : [sendTo],
    });
    setMessage('');
  };

  const activeTopicData = topics.find(t => t.id === activeTopic);

/**
 * Council — Multi-Agent Discussion
 * ==================================
 * Chat room where Michael talks to agents individually or as a group.
 * Topics on the left, messages on the right.
 *
 * DATA: cc_council_topics + cc_council_messages in Supabase
 * API: councilApi in src/lib/api.js
 * REALTIME: Subscribe to cc_council_messages for live updates
 *
 * REPLACING DEMO DATA:
 *   const { data: topics } = useQuery({ queryKey: ['council-topics'], queryFn: councilApi.getTopics });
 *   const { data: messages } = useQuery({
 *     queryKey: ['council-messages', activeTopic],
 *     queryFn: () => councilApi.getMessages(activeTopic)
 *   });
 */
import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';
import { useCouncilTopics, useCouncilMessages, useSendCouncilMessage } from '../hooks/useSupabase';

// ── Demo topics ──────────────────────────────────────────
const DEMO_TOPICS = [
  { id: 'general', title: 'General', pinned: true, lastActivity: '2m ago' },
  { id: 'deploys', title: 'Deployments', pinned: false, lastActivity: '1h ago' },
  { id: 'growth-q2', title: 'Q2 Growth Strategy', pinned: true, lastActivity: '3h ago' },
  { id: 'bugs', title: 'Bug Reports', pinned: false, lastActivity: '5h ago' },
  { id: 'watercooler-archive', title: 'Off Topic', pinned: false, lastActivity: '1d ago' },
];

// ── Demo messages per topic ──────────────────────────────
const DEMO_MESSAGES = {
  general: [
    { id: 1, sender: 'michael', type: 'michael', content: 'Morning team. What\'s the priority today?', time: '9:00 AM', pinned: false },
    { id: 2, sender: 'rivet', type: 'agent', content: 'Morning boss. Top priorities: Builder has the SMS retry fix ready for review, Susan\'s running batch #48, and Sentinel flagged RAM at 78%. I\'d say approve Builder\'s deploy first — it\'s blocking Susan\'s batch reliability.', time: '9:01 AM', pinned: false },
    { id: 3, sender: 'builder', type: 'agent', content: 'Confirmed — the SMS retry fix uses exponential backoff with jitter. Tested against Twilio\'s sandbox. Zero failed sends in 500 test messages. Ready when you are.', time: '9:02 AM', pinned: false },
    { id: 4, sender: 'susan', type: 'agent', content: 'That fix would be huge for me. Lost 3 sends to 429s yesterday. Batch #48 has 847 leads queued — best run it after the deploy.', time: '9:03 AM', pinned: false },
    { id: 5, sender: 'sentinel', type: 'agent', content: 'RAM note: it\'s Radar\'s competitor scraper. Peaks at 78% during scans then drops. Not critical but worth watching. Cog, could we bump the VPS to 16GB?', time: '9:04 AM', pinned: false },
    { id: 6, sender: 'cog', type: 'agent', content: 'Can do, but it\'s $20/mo more on DO. Alternative: I can optimize Radar\'s scraper to stream results instead of loading all to memory. That\'d cut peak usage by ~30%.', time: '9:05 AM', pinned: false },
    { id: 7, sender: 'michael', type: 'michael', content: 'Good thinking Cog. Do the optimization first, we\'ll upgrade if it\'s still tight. Builder — approved, ship the SMS fix. Susan, run batch #48 after deploy confirms.', time: '9:07 AM', pinned: true },
    { id: 8, sender: 'rivet', type: 'agent', content: 'Copy that. Builder: deploy GE v2.4. Susan: hold batch #48 until Builder confirms. Sentinel: keep monitoring RAM. Cog: start Radar optimizer today. I\'ll track all four.', time: '9:08 AM', pinned: false },
    { id: 9, sender: 'harper', type: 'agent', content: 'FYI — the LinkedIn post I published yesterday hit 500 views and 23 reactions. Best performer this month. Should I double down on that topic (hiring tips)?', time: '9:15 AM', pinned: false },
    { id: 10, sender: 'radar', type: 'agent', content: 'Intel update: Hipages raised their Gold tier pricing 15% this quarter. Our value prop just got stronger. Harper, that could make a great LinkedIn angle.', time: '9:20 AM', pinned: false },
  ],
  deploys: [
    { id: 20, sender: 'builder', type: 'agent', content: 'GE v2.3.1 deployed successfully. Zero downtime. SMS retry logic active.', time: '2:30 PM', pinned: false },
    { id: 21, sender: 'rivet', type: 'agent', content: 'Confirmed — monitoring post-deploy. All health checks passing.', time: '2:31 PM', pinned: false },
  ],
  'growth-q2': [
    { id: 30, sender: 'michael', type: 'michael', content: 'Team, let\'s plan Q2. Our targets: 100 contractors, $50K ARR, 3 enterprise clients. Thoughts?', time: 'Yesterday', pinned: true },
    { id: 31, sender: 'radar', type: 'agent', content: 'Based on my analysis, the construction hiring market in Sydney is growing 12% YoY. Hipages\' price hike creates an opening. I recommend targeting their dissatisfied Gold tier customers.', time: 'Yesterday', pinned: false },
    { id: 32, sender: 'susan', type: 'agent', content: 'I can run targeted SMS campaigns to ex-Hipages users if we can get the list. Radar, can you scrape their public reviews for contact signals?', time: 'Yesterday', pinned: false },
  ],
};

// ── Topic Sidebar ────────────────────────────────────────
function TopicList({ topics, activeTopic, onSelect, onCreateTopic }) {
  return (

Topics

        <button
          onClick={onCreateTopic}
          className="w-6 h-6 rounded-md bg-[var(--cc-surface-hover)] hover:bg-[var(--cc-surface-active)] flex items-center justify-center transition-colors"
          title="New topic"
        >
          <Plus className="w-3 h-3 text-[var(--cc-text-muted)]" />
        </button>

        {topics.map(topic => (
          <button
            key={topic.id}
            onClick={() => onSelect(topic.id)}
            className={`w-full text-left px-3 py-2.5 flex items-center gap-2 transition-colors ${
              activeTopic === topic.id
                ? 'bg-[var(--cc-accent-glow)] text-[var(--cc-accent)]'
                : 'text-[var(--cc-text-secondary)] hover:bg-[var(--cc-surface-hover)] hover:text-[var(--cc-text)]'
            }`}
          >
            {topic.pinned ? <Pin className="w-3 h-3 flex-shrink-0" /> : <Hash className="w-3 h-3 flex-shrink-0" />}
            {topic.title}
            {topic.lastActivity}
          </button>
        ))}

  );
}

// ── Message Bubble ───────────────────────────────────────
function MessageBubble({ msg }) {
  const isMichael = msg.type === 'michael';
  const agent = !isMichael ? getAgent(msg.sender) : null;

  return (
    <div className={`flex gap-3 px-4 py-2.5 ${msg.pinned ? 'bg-amber-500/5 border-l-2 border-l-amber-500/50' : ''}`}>
      {/* Avatar */}

        {isMichael ? '👤' : agent?.avatar || '🤖'}

      {/* Content */}

            {isMichael ? 'Michael' : agent?.name || msg.sender}

          {!isMichael && (
            {agent?.role}
          )}
          {msg.time}
          {msg.pinned && <Pin className="w-3 h-3 text-amber-400" />}

          {msg.content}

  );
}

// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  // Fetch topics and messages from Supabase
  const { data: topicsData } = useCouncilTopics();
  const { data: messagesData } = useCouncilMessages(activeTopic);
  const sendMessageMutation = useSendCouncilMessage();

  // Map topics (fallback to demo if empty)
  const topics = topicsData && topicsData.length > 0
    ? topicsData.map(t => ({
        id: t.id,
        title: t.title,
        pinned: t.pinned,
        lastActivity: new Date(t.last_activity_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
      }))
    : DEMO_TOPICS;

  // Map messages (fallback to demo if empty)
  const mappedMessages = messagesData && messagesData.length > 0
    ? messagesData.map(m => ({
        id: m.id,
        sender: m.sender_id,
        type: m.sender_id === 'michael' ? 'michael' : 'agent',
        content: m.content,
        time: new Date(m.created_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
        pinned: m.pinned || false,
      }))
    : [];

  const messages = mappedMessages.length > 0 ? mappedMessages : (DEMO_MESSAGES[activeTopic] || []);

  // Auto-scroll to bottom
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length, activeTopic]);

  const handleSend = () => {
    if (!message.trim()) return;
    // TODO: councilApi.sendMessage(activeTopic, { content: message, sender: 'michael', mentions: sendTo === 'all' ? [] : [sendTo] })
    console.log('Send to council:', { topic: activeTopic, message, sendTo });
    setMessage('');
  };

  const activeTopicData = DEMO_TOPICS.find(t => t.id === activeTopic);

  return (

      <Card className="flex flex-col md:flex-row overflow-hidden" style={{ height: 'calc(100vh - var(--header-height) - 48px)' }}>
        {/* Mobile topic toggle */}
        <button
          onClick={() => setShowMobileSidebar(!showMobileSidebar)}
          className="md:hidden flex items-center gap-2 px-4 py-2.5 border-b border-[var(--cc-border)] text-[12px] font-medium text-[var(--cc-text-secondary)]"
        >
          <Hash className="w-3.5 h-3.5" />
          {activeTopicData?.title || 'Select topic'}
        </button>

        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={DEMO_TOPICS}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />

        {/* Messages area */}

          {/* Topic header */}

              <Hash className="w-4 h-4 text-[var(--cc-text-muted)]" />

{activeTopicData?.title}

              <Users className="w-3.5 h-3.5 text-[var(--cc-text-muted)]" />

                {agents.length + 1} members

          {/* Messages */}

            {messages.map(msg => (
              <MessageBubble key={msg.id} msg={msg} />
            ))}

          {/* Input area */}

            {/* Send-to selector */}

              Send to:
              <select
                value={sendTo}
                onChange={e => setSendTo(e.target.value)}
                className="bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded px-2 py-0.5 text-[10px] text-[var(--cc-text)] outline-none"
              >
                <option value="all">All agents</option>
                {agents.map(a => <option key={a.id} value={a.id}>{a.avatar} {a.name}</option>)}
              </select>

              <input
                type="text"
                value={message}
                onChange={e => setMessage(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && handleSend()}
                placeholder={`Message #${activeTopicData?.title || 'channel'}...`}
                className="flex-1 bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded-lg px-3 py-2.5 text-[12px] text-[var(--cc-text)] placeholder-[var(--cc-text-muted)] outline-none focus:border-[var(--cc-accent)]"
              />
              <button
                onClick={handleSend}
                disabled={!message.trim()}
                className="w-9 h-9 rounded-lg bg-[var(--cc-accent)] hover:bg-[var(--cc-accent-dim)] flex items-center justify-center transition-colors disabled:opacity-40"
              >
                <Send className="w-4 h-4 text-white" />
              </button>

      </Card>

  );
}

-    // TODO: councilApi.sendMessage(activeTopic, { content: message, sender: 'michael', mentions: sendTo === 'all' ? [] : [sendTo] })

-    console.log('Send to council:', { topic: activeTopic, message, sendTo });

+      mentions: sendTo === 'all' ? [] : [sendTo],

-  const activeTopicData = DEMO_TOPICS.find(t => t.id === activeTopic);

+  const activeTopicData = topics.find(t => t.id === activeTopic);


        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={DEMO_TOPICS}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />
        </div>

        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={topics}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />
        </div>


        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={DEMO_TOPICS}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />
        </div>

        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={topics}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />
        </div>

/**
 * Council — Multi-Agent Discussion
 * ==================================
 * Chat room where Michael talks to agents individually or as a group.
 * Topics on the left, messages on the right.
 *
 * DATA: cc_council_topics + cc_council_messages in Supabase
 * API: councilApi in src/lib/api.js
 * REALTIME: Subscribe to cc_council_messages for live updates
 *
 * REPLACING DEMO DATA:
 *   const { data: topics } = useQuery({ queryKey: ['council-topics'], queryFn: councilApi.getTopics });
 *   const { data: messages } = useQuery({
 *     queryKey: ['council-messages', activeTopic],
 *     queryFn: () => councilApi.getMessages(activeTopic)
 *   });
 */
import { useState, useRef, useEffect } from 'react';
import { getAgents, getAgent } from '../lib/fleet';
import Card from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import { Send, Plus, Pin, Hash, Users, X } from 'lucide-react';
import { useCouncilTopics, useCouncilMessages, useSendCouncilMessage } from '../hooks/useSupabase';

// ── Demo topics ──────────────────────────────────────────
const DEMO_TOPICS = [
  { id: 'general', title: 'General', pinned: true, lastActivity: '2m ago' },
  { id: 'deploys', title: 'Deployments', pinned: false, lastActivity: '1h ago' },
  { id: 'growth-q2', title: 'Q2 Growth Strategy', pinned: true, lastActivity: '3h ago' },
  { id: 'bugs', title: 'Bug Reports', pinned: false, lastActivity: '5h ago' },
  { id: 'watercooler-archive', title: 'Off Topic', pinned: false, lastActivity: '1d ago' },
];

// ── Demo messages per topic ──────────────────────────────
const DEMO_MESSAGES = {
  general: [
    { id: 1, sender: 'michael', type: 'michael', content: 'Morning team. What\'s the priority today?', time: '9:00 AM', pinned: false },
    { id: 2, sender: 'rivet', type: 'agent', content: 'Morning boss. Top priorities: Builder has the SMS retry fix ready for review, Susan\'s running batch #48, and Sentinel flagged RAM at 78%. I\'d say approve Builder\'s deploy first — it\'s blocking Susan\'s batch reliability.', time: '9:01 AM', pinned: false },
    { id: 3, sender: 'builder', type: 'agent', content: 'Confirmed — the SMS retry fix uses exponential backoff with jitter. Tested against Twilio\'s sandbox. Zero failed sends in 500 test messages. Ready when you are.', time: '9:02 AM', pinned: false },
    { id: 4, sender: 'susan', type: 'agent', content: 'That fix would be huge for me. Lost 3 sends to 429s yesterday. Batch #48 has 847 leads queued — best run it after the deploy.', time: '9:03 AM', pinned: false },
    { id: 5, sender: 'sentinel', type: 'agent', content: 'RAM note: it\'s Radar\'s competitor scraper. Peaks at 78% during scans then drops. Not critical but worth watching. Cog, could we bump the VPS to 16GB?', time: '9:04 AM', pinned: false },
    { id: 6, sender: 'cog', type: 'agent', content: 'Can do, but it\'s $20/mo more on DO. Alternative: I can optimize Radar\'s scraper to stream results instead of loading all to memory. That\'d cut peak usage by ~30%.', time: '9:05 AM', pinned: false },
    { id: 7, sender: 'michael', type: 'michael', content: 'Good thinking Cog. Do the optimization first, we\'ll upgrade if it\'s still tight. Builder — approved, ship the SMS fix. Susan, run batch #48 after deploy confirms.', time: '9:07 AM', pinned: true },
    { id: 8, sender: 'rivet', type: 'agent', content: 'Copy that. Builder: deploy GE v2.4. Susan: hold batch #48 until Builder confirms. Sentinel: keep monitoring RAM. Cog: start Radar optimizer today. I\'ll track all four.', time: '9:08 AM', pinned: false },
    { id: 9, sender: 'harper', type: 'agent', content: 'FYI — the LinkedIn post I published yesterday hit 500 views and 23 reactions. Best performer this month. Should I double down on that topic (hiring tips)?', time: '9:15 AM', pinned: false },
    { id: 10, sender: 'radar', type: 'agent', content: 'Intel update: Hipages raised their Gold tier pricing 15% this quarter. Our value prop just got stronger. Harper, that could make a great LinkedIn angle.', time: '9:20 AM', pinned: false },
  ],
  deploys: [
    { id: 20, sender: 'builder', type: 'agent', content: 'GE v2.3.1 deployed successfully. Zero downtime. SMS retry logic active.', time: '2:30 PM', pinned: false },
    { id: 21, sender: 'rivet', type: 'agent', content: 'Confirmed — monitoring post-deploy. All health checks passing.', time: '2:31 PM', pinned: false },
  ],
  'growth-q2': [
    { id: 30, sender: 'michael', type: 'michael', content: 'Team, let\'s plan Q2. Our targets: 100 contractors, $50K ARR, 3 enterprise clients. Thoughts?', time: 'Yesterday', pinned: true },
    { id: 31, sender: 'radar', type: 'agent', content: 'Based on my analysis, the construction hiring market in Sydney is growing 12% YoY. Hipages\' price hike creates an opening. I recommend targeting their dissatisfied Gold tier customers.', time: 'Yesterday', pinned: false },
    { id: 32, sender: 'susan', type: 'agent', content: 'I can run targeted SMS campaigns to ex-Hipages users if we can get the list. Radar, can you scrape their public reviews for contact signals?', time: 'Yesterday', pinned: false },
  ],
};

// ── Topic Sidebar ────────────────────────────────────────
function TopicList({ topics, activeTopic, onSelect, onCreateTopic }) {
  return (

Topics

        <button
          onClick={onCreateTopic}
          className="w-6 h-6 rounded-md bg-[var(--cc-surface-hover)] hover:bg-[var(--cc-surface-active)] flex items-center justify-center transition-colors"
          title="New topic"
        >
          <Plus className="w-3 h-3 text-[var(--cc-text-muted)]" />
        </button>

        {topics.map(topic => (
          <button
            key={topic.id}
            onClick={() => onSelect(topic.id)}
            className={`w-full text-left px-3 py-2.5 flex items-center gap-2 transition-colors ${
              activeTopic === topic.id
                ? 'bg-[var(--cc-accent-glow)] text-[var(--cc-accent)]'
                : 'text-[var(--cc-text-secondary)] hover:bg-[var(--cc-surface-hover)] hover:text-[var(--cc-text)]'
            }`}
          >
            {topic.pinned ? <Pin className="w-3 h-3 flex-shrink-0" /> : <Hash className="w-3 h-3 flex-shrink-0" />}
            {topic.title}
            {topic.lastActivity}
          </button>
        ))}

  );
}

// ── Message Bubble ───────────────────────────────────────
function MessageBubble({ msg }) {
  const isMichael = msg.type === 'michael';
  const agent = !isMichael ? getAgent(msg.sender) : null;

  return (
    <div className={`flex gap-3 px-4 py-2.5 ${msg.pinned ? 'bg-amber-500/5 border-l-2 border-l-amber-500/50' : ''}`}>
      {/* Avatar */}

        {isMichael ? '👤' : agent?.avatar || '🤖'}

      {/* Content */}

            {isMichael ? 'Michael' : agent?.name || msg.sender}

          {!isMichael && (
            {agent?.role}
          )}
          {msg.time}
          {msg.pinned && <Pin className="w-3 h-3 text-amber-400" />}

          {msg.content}

  );
}

// ── Main Component ───────────────────────────────────────
export default function Council() {
  const [activeTopic, setActiveTopic] = useState('general');
  const [message, setMessage] = useState('');
  const [sendTo, setSendTo] = useState('all');
  const [showMobileSidebar, setShowMobileSidebar] = useState(false);
  const messagesEndRef = useRef(null);
  const agents = getAgents();

  // Fetch topics and messages from Supabase
  const { data: topicsData } = useCouncilTopics();
  const { data: messagesData } = useCouncilMessages(activeTopic);
  const sendMessageMutation = useSendCouncilMessage();

  // Map topics (fallback to demo if empty)
  const topics = topicsData && topicsData.length > 0
    ? topicsData.map(t => ({
        id: t.id,
        title: t.title,
        pinned: t.pinned,
        lastActivity: new Date(t.last_activity_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
      }))
    : DEMO_TOPICS;

  // Map messages (fallback to demo if empty)
  const mappedMessages = messagesData && messagesData.length > 0
    ? messagesData.map(m => ({
        id: m.id,
        sender: m.sender_id,
        type: m.sender_id === 'michael' ? 'michael' : 'agent',
        content: m.content,
        time: new Date(m.created_at).toLocaleTimeString('en-AU', { hour: 'numeric', minute: '2-digit' }),
        pinned: m.pinned || false,
      }))
    : [];

  const messages = mappedMessages.length > 0 ? mappedMessages : (DEMO_MESSAGES[activeTopic] || []);

  // Auto-scroll to bottom
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length, activeTopic]);

  const handleSend = () => {
    if (!message.trim()) return;

    sendMessageMutation.mutate({
      topic_id: activeTopic,
      sender_id: 'michael',
      content: message.trim(),
      mentions: sendTo === 'all' ? [] : [sendTo],
    });
    setMessage('');
  };

  const activeTopicData = topics.find(t => t.id === activeTopic);

  return (

      <Card className="flex flex-col md:flex-row overflow-hidden" style={{ height: 'calc(100vh - var(--header-height) - 48px)' }}>
        {/* Mobile topic toggle */}
        <button
          onClick={() => setShowMobileSidebar(!showMobileSidebar)}
          className="md:hidden flex items-center gap-2 px-4 py-2.5 border-b border-[var(--cc-border)] text-[12px] font-medium text-[var(--cc-text-secondary)]"
        >
          <Hash className="w-3.5 h-3.5" />
          {activeTopicData?.title || 'Select topic'}
        </button>

        {/* Topic sidebar — always on desktop, toggle on mobile */}
        <div className={`${showMobileSidebar ? 'block' : 'hidden'} md:block`}>
          <TopicList
            topics={DEMO_TOPICS}
            activeTopic={activeTopic}
            onSelect={(id) => { setActiveTopic(id); setShowMobileSidebar(false); }}
            onCreateTopic={() => console.log('TODO: create topic modal')}
          />

        {/* Messages area */}

          {/* Topic header */}

              <Hash className="w-4 h-4 text-[var(--cc-text-muted)]" />

{activeTopicData?.title}

              <Users className="w-3.5 h-3.5 text-[var(--cc-text-muted)]" />

                {agents.length + 1} members

          {/* Messages */}

            {messages.map(msg => (
              <MessageBubble key={msg.id} msg={msg} />
            ))}

          {/* Input area */}

            {/* Send-to selector */}

              Send to:
              <select
                value={sendTo}
                onChange={e => setSendTo(e.target.value)}
                className="bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded px-2 py-0.5 text-[10px] text-[var(--cc-text)] outline-none"
              >
                <option value="all">All agents</option>
                {agents.map(a => <option key={a.id} value={a.id}>{a.avatar} {a.name}</option>)}
              </select>

              <input
                type="text"
                value={message}
                onChange={e => setMessage(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && handleSend()}
                placeholder={`Message #${activeTopicData?.title || 'channel'}...`}
                className="flex-1 bg-[var(--cc-bg-raised)] border border-[var(--cc-border)] rounded-lg px-3 py-2.5 text-[12px] text-[var(--cc-text)] placeholder-[var(--cc-text-muted)] outline-none focus:border-[var(--cc-accent)]"
              />
              <button
                onClick={handleSend}
                disabled={!message.trim()}
                className="w-9 h-9 rounded-lg bg-[var(--cc-accent)] hover:bg-[var(--cc-accent-dim)] flex items-center justify-center transition-colors disabled:opacity-40"
              >
                <Send className="w-4 h-4 text-white" />
              </button>

      </Card>

  );
}


TodoWrite

Wire CommandCentre page to real Supabase data

Wiring CommandCentre page to real Supabase data

Wire Tasks page to real Supabase data with mutations

Wiring Tasks page to real Supabase data with mutations

Wire Council page to real Supabase data with realtime

Wiring Council page to real Supabase data with realtime

Wire Fleet page to real Supabase data

Wire Projects page to real Supabase data

Wiring Projects page to real Supabase data

Wire ActivityFeed and PendingApprovals components

Wiring ActivityFeed and PendingApprovals components

Test build passes

Commit all changes with proof summary

Committing all changes with proof summary


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Wire CommandCentre page to real Supabase data

Wiring CommandCentre page to real Supabase data

Wire Tasks page to real Supabase data with mutations

Wiring Tasks page to real Supabase data with mutations

Wire Council page to real Supabase data with realtime

Wiring Council page to real Supabase data with realtime

Wire Fleet page to real Supabase data

Wire Projects page to real Supabase data

Wiring Projects page to real Supabase data

Wire ActivityFeed and PendingApprovals components

Wiring ActivityFeed and PendingApprovals components

Test build passes

Commit all changes with proof summary

Committing all changes with proof summary

Wire Fleet page to real Supabase data

Test build passes

Commit all changes with proof summary


Now let's wire up the Fleet page:


Edit

import { useNavigate } from 'react-router-dom';
import { getAgents, getStatusDisplay } from '../lib/fleet';
import Card, { CardHeader } from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import StatusDot from '../components/ui/StatusDot';
import { Bot, Play, Square, RotateCcw } from 'lucide-react';

// Demo statuses — replace with useQuery + fleetApi.getStatus()
const DEMO = {
  rivet:    { status: 'healthy', uptime: '14d 3h', sessions: 847, memory: '142MB' },
  builder:  { status: 'busy', uptime: '14d 3h', sessions: 312, memory: '198MB' },
  susan:    { status: 'healthy', uptime: '12d 8h', sessions: 1203, memory: '156MB' },
  harper:   { status: 'healthy', uptime: '10d 1h', sessions: 89, memory: '87MB' },
  sentinel: { status: 'warning', uptime: '14d 3h', sessions: 4521, memory: '312MB' },
  radar:    { status: 'healthy', uptime: '8d 14h', sessions: 203, memory: '134MB' },
  herald:   { status: 'healthy', uptime: '14d 3h', sessions: 567, memory: '92MB' },
  cog:      { status: 'offline', uptime: '—', sessions: 0, memory: '—' },
};

import { useNavigate } from 'react-router-dom';
import { getAgents, getStatusDisplay } from '../lib/fleet';
import Card, { CardHeader } from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import StatusDot from '../components/ui/StatusDot';
import { Bot, Play, Square, RotateCcw } from 'lucide-react';
import { useAgentSnapshots } from '../hooks/useSupabase';

// Fallback demo statuses
const DEMO = {
  rivet:    { status: 'healthy', uptime: '14d 3h', sessions: 847, memory: '142MB' },
  builder:  { status: 'busy', uptime: '14d 3h', sessions: 312, memory: '198MB' },
  susan:    { status: 'healthy', uptime: '12d 8h', sessions: 1203, memory: '156MB' },
  harper:   { status: 'healthy', uptime: '10d 1h', sessions: 89, memory: '87MB' },
  sentinel: { status: 'warning', uptime: '14d 3h', sessions: 4521, memory: '312MB' },
  radar:    { status: 'healthy', uptime: '8d 14h', sessions: 203, memory: '134MB' },
  herald:   { status: 'healthy', uptime: '14d 3h', sessions: 567, memory: '92MB' },
  cog:      { status: 'offline', uptime: '—', sessions: 0, memory: '—' },
};


The file /home/ccuser/opsman-control-centre/src/pages/Fleet.jsx has been updated successfully.

import { useNavigate } from 'react-router-dom';
import { getAgents, getStatusDisplay } from '../lib/fleet';
import Card, { CardHeader } from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import StatusDot from '../components/ui/StatusDot';
import { Bot, Play, Square, RotateCcw } from 'lucide-react';
import { useAgentSnapshots } from '../hooks/useSupabase';

// Fallback demo statuses
const DEMO = {
  rivet:    { status: 'healthy', uptime: '14d 3h', sessions: 847, memory: '142MB' },
  builder:  { status: 'busy', uptime: '14d 3h', sessions: 312, memory: '198MB' },
  susan:    { status: 'healthy', uptime: '12d 8h', sessions: 1203, memory: '156MB' },
  harper:   { status: 'healthy', uptime: '10d 1h', sessions: 89, memory: '87MB' },
  sentinel: { status: 'warning', uptime: '14d 3h', sessions: 4521, memory: '312MB' },
  radar:    { status: 'healthy', uptime: '8d 14h', sessions: 203, memory: '134MB' },
  herald:   { status: 'healthy', uptime: '14d 3h', sessions: 567, memory: '92MB' },
  cog:      { status: 'offline', uptime: '—', sessions: 0, memory: '—' },
};

/**
 * Fleet Page
 * ==========
 * Agent management hub. Lists all agents with controls.
 *
 * FEATURES:
 * - Agent table with status, model, port, uptime
 * - Start/Stop/Restart buttons (call fleetApi)
 * - Click row → navigate to agent detail
 *
 * EXTENDING:
 * - Add agent detail page at /fleet/[agentId] (see App.jsx routes)
 * - Wire up fleetApi.startAgent/stopAgent/restartAgent for real controls
 */
import { useNavigate } from 'react-router-dom';
import { getAgents, getStatusDisplay } from '../lib/fleet';
import Card, { CardHeader } from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import StatusDot from '../components/ui/StatusDot';
import { Bot, Play, Square, RotateCcw } from 'lucide-react';

// Demo statuses — replace with useQuery + fleetApi.getStatus()
const DEMO = {
  rivet:    { status: 'healthy', uptime: '14d 3h', sessions: 847, memory: '142MB' },
  builder:  { status: 'busy', uptime: '14d 3h', sessions: 312, memory: '198MB' },
  susan:    { status: 'healthy', uptime: '12d 8h', sessions: 1203, memory: '156MB' },
  harper:   { status: 'healthy', uptime: '10d 1h', sessions: 89, memory: '87MB' },
  sentinel: { status: 'warning', uptime: '14d 3h', sessions: 4521, memory: '312MB' },
  radar:    { status: 'healthy', uptime: '8d 14h', sessions: 203, memory: '134MB' },
  herald:   { status: 'healthy', uptime: '14d 3h', sessions: 567, memory: '92MB' },
  cog:      { status: 'offline', uptime: '—', sessions: 0, memory: '—' },
};

export default function Fleet() {
  const navigate = useNavigate();
  const agents = getAgents();

  return (

      <Card>
        <CardHeader title="Fleet Management" subtitle={`${agents.length} agents configured`} icon={Bot} />

        {/* Table */}

                {['Agent', 'Status', 'Model', 'Port', 'Uptime', 'Sessions', 'Memory', 'Controls'].map(h => (

                    {h}

                ))}

              {agents.map((agent) => {
                const demo = DEMO[agent.id] || { status: 'offline', uptime: '—', sessions: 0, memory: '—' };
                const statusInfo = getStatusDisplay(demo.status);
                const shortModel = agent.model.replace('claude-', '').replace(/-\d{8}$/, '').replace('deepseek-chat', 'deepseek');

                return (

 navigate(`/fleet/${agent.id}`)}
                    className="border-b border-[var(--cc-border)] hover:bg-[var(--cc-surface-hover)] cursor-pointer transition-colors"
                  >
                    {/* Agent */}

                        {agent.avatar}

{agent.name}

{agent.role}

                    {/* Status */}

                        <StatusDot status={demo.status} />
                        <Badge variant={demo.status}>{statusInfo.label}</Badge>

                    {/* Model */}

                      {shortModel}

                    {/* Port */}

                      :{agent.port}

                    {/* Uptime */}

                      {demo.uptime}

                    {/* Sessions */}

                      {demo.sessions.toLocaleString()}

                    {/* Memory */}

                      {demo.memory}

                    {/* Controls */}

 e.stopPropagation()}>
                        {demo.status === 'offline' ? (
                          <button className="w-7 h-7 rounded-lg bg-emerald-500/15 hover:bg-emerald-500/25 flex items-center justify-center transition-colors" title="Start">
                            <Play className="w-3 h-3 text-emerald-400" />
                          </button>
                        ) : (
                          <>
                            <button className="w-7 h-7 rounded-lg bg-red-500/15 hover:bg-red-500/25 flex items-center justify-center transition-colors" title="Stop">
                              <Square className="w-3 h-3 text-red-400" />
                            </button>
                            <button className="w-7 h-7 rounded-lg bg-amber-500/15 hover:bg-amber-500/25 flex items-center justify-center transition-colors" title="Restart">
                              <RotateCcw className="w-3 h-3 text-amber-400" />
                            </button>
                          </>
                        )}

                );
              })}

      </Card>

  );
}

+import { useAgentSnapshots } from '../hooks/useSupabase';

-// Demo statuses — replace with useQuery + fleetApi.getStatus()


export default function Fleet() {
  const navigate = useNavigate();
  const agents = getAgents();

  return (

export default function Fleet() {
  const navigate = useNavigate();
  const agents = getAgents();
  const { data: snapshots } = useAgentSnapshots();

  // Build agent statuses from snapshots (fallback to demo if empty)
  const agentStatuses = {};
  if (snapshots && snapshots.length > 0) {
    snapshots.forEach(snap => {
      agentStatuses[snap.agent_name] = {
        status: snap.status || 'offline',
        uptime: snap.uptime_hours ? `${Math.floor(snap.uptime_hours / 24)}d ${Math.floor(snap.uptime_hours % 24)}h` : '—',
        sessions: snap.session_count || 0,
        memory: snap.memory_mb ? `${snap.memory_mb}MB` : '—',
      };
    });
  }
  const finalStatuses = Object.keys(agentStatuses).length > 0 ? agentStatuses : DEMO;

  return (


export default function Fleet() {
  const navigate = useNavigate();
  const agents = getAgents();
  const { data: snapshots } = useAgentSnapshots();

  // Build agent statuses from snapshots (fallback to demo if empty)
  const agentStatuses = {};
  if (snapshots && snapshots.length > 0) {
    snapshots.forEach(snap => {
      agentStatuses[snap.agent_name] = {
        status: snap.status || 'offline',
        uptime: snap.uptime_hours ? `${Math.floor(snap.uptime_hours / 24)}d ${Math.floor(snap.uptime_hours % 24)}h` : '—',
        sessions: snap.session_count || 0,
        memory: snap.memory_mb ? `${snap.memory_mb}MB` : '—',
      };
    });
  }
  const finalStatuses = Object.keys(agentStatuses).length > 0 ? agentStatuses : DEMO;

  return (

/**
 * Fleet Page
 * ==========
 * Agent management hub. Lists all agents with controls.
 *
 * FEATURES:
 * - Agent table with status, model, port, uptime
 * - Start/Stop/Restart buttons (call fleetApi)
 * - Click row → navigate to agent detail
 *
 * EXTENDING:
 * - Add agent detail page at /fleet/[agentId] (see App.jsx routes)
 * - Wire up fleetApi.startAgent/stopAgent/restartAgent for real controls
 */
import { useNavigate } from 'react-router-dom';
import { getAgents, getStatusDisplay } from '../lib/fleet';
import Card, { CardHeader } from '../components/ui/Card';
import Badge from '../components/ui/Badge';
import StatusDot from '../components/ui/StatusDot';
import { Bot, Play, Square, RotateCcw } from 'lucide-react';
import { useAgentSnapshots } from '../hooks/useSupabase';

// Fallback demo statuses
const DEMO = {
  rivet:    { status: 'healthy', uptime: '14d 3h', sessions: 847, memory: '142MB' },
  builder:  { status: 'busy', uptime: '14d 3h', sessions: 312, memory: '198MB' },
  susan:    { status: 'healthy', uptime: '12d 8h', sessions: 1203, memory: '156MB' },
  harper:   { status: 'healthy', uptime: '10d 1h', sessions: 89, memory: '87MB' },
  sentinel: { status: 'warning', uptime: '14d 3h', sessions: 4521, memory: '312MB' },
  radar:    { status: 'healthy', uptime: '8d 14h', sessions: 203, memory: '134MB' },
  herald:   { status: 'healthy', uptime: '14d 3h', sessions: 567, memory: '92MB' },
  cog:      { status: 'offline', uptime: '—', sessions: 0, memory: '—' },
};

export default function Fleet() {
  const navigate = useNavigate();
  const agents = getAgents();

  return (

      <Card>
        <CardHeader title="Fleet Management" subtitle={`${agents.length} agents configured`} icon={Bot} />

        {/* Table */}

                {['Agent', 'Status', 'Model', 'Port', 'Uptime', 'Sessions', 'Memory', 'Controls'].map(h => (

                    {h}

                ))}

              {agents.map((agent) => {
                const demo = DEMO[agent.id] || { status: 'offline', uptime: '—', sessions: 0, memory: '—' };
                const statusInfo = getStatusDisplay(demo.status);
                const shortModel = agent.model.replace('claude-', '').replace(/-\d{8}$/, '').replace('deepseek-chat', 'deepseek');

                return (

 navigate(`/fleet/${agent.id}`)}
                    className="border-b border-[var(--cc-border)] hover:bg-[var(--cc-surface-hover)] cursor-pointer transition-colors"
                  >
                    {/* Agent */}

                        {agent.avatar}

{agent.name}

{agent.role}

                    {/* Status */}

                        <StatusDot status={demo.status} />
                        <Badge variant={demo.status}>{statusInfo.label}</Badge>

                    {/* Model */}

                      {shortModel}

                    {/* Port */}

                      :{agent.port}

                    {/* Uptime */}

                      {demo.uptime}

                    {/* Sessions */}

                      {demo.sessions.toLocaleString()}

                    {/* Memory */}

                      {demo.memory}

                    {/* Controls */}

 e.stopPropagation()}>
                        {demo.status === 'offline' ? (
                          <button className="w-7 h-7 rounded-lg bg-emerald-500/15 hover:bg-emerald-500/25 flex items-center justify-center transition-colors" title="Start">
                            <Play className="w-3 h-3 text-emerald-400" />
                          </button>
                        ) : (
                          <>
                            <button className="w-7 h-7 rounded-lg bg-red-500/15 hover:bg-red-500/25 flex items-center justify-center transition-colors" title="Stop">
                              <Square className="w-3 h-3 text-red-400" />
                            </button>
                            <button className="w-7 h-7 rounded-lg bg-amber-500/15 hover:bg-amber-500/25 flex items-center justify-center transition-colors" title="Restart">
                              <RotateCcw className="w-3 h-3 text-amber-400" />
                            </button>
                          </>
                        )}

                );
              })}

      </Card>

  );
}

+  const { data: snapshots } = useAgentSnapshots();

+  // Build agent statuses from snapshots (fallback to demo if empty)

+  if (snapshots && snapshots.length > 0) {

+      agentStatuses[snap.agent_name] = {

+        status: snap.status || 'offline',

+        uptime: snap.uptime_hours ? `${Math.floor(snap.uptime_hours / 24)}d ${Math.floor(snap.uptime_hours % 24)}h` : '—',

+        sessions: snap.session_count || 0,

+        memory: snap.memory_mb ? `${snap.memory_mb}MB` : '—',

+  const finalStatuses = Object.keys(agentStatuses).length > 0 ? agentStatuses : DEMO;