Ai-Mee Help Centre
Home
Features
How-To Guides
FAQ
Need Help?
Home
Features
How-To Guides
FAQ
Need Help?

Phase 5: Dashboard Evolution (PLANNED)

Timeline: Week 8-10
Status: ⚠️ PLANNED - NOT YET IMPLEMENTED
Note: Can run in parallel with Phase 3-4
Objective: Transform the Vue dashboard from primary client interface to admin/analytics panel.


Current Dashboard State (As-Is)

The dashboard currently exists with the following pages:

Existing Pages (Implemented)

  • / - Landing page
  • /auth - Login
  • /auth/signup - Sign up
  • /auth/profile - User profile
  • /app - Dashboard home
  • /app/clients - Client list
  • /app/posts - Posts list
  • /app/edit - Post editor
  • /app/settings - User settings
  • /app/admin - Admin panel
  • /app/client/:id - Client overview
  • /app/client/:id/integrations - OAuth integrations
  • /app/client/:id/settings - Client settings (brand voice, Telegram pairing)
  • /app/client/campaigns/:id - Campaign detail
  • /app/campaign/new - Create campaign
  • /app/campaign/:id - Campaign editor

Technology Stack

  • Vue 3.5 + Vite 6.2
  • Pinia 3.0 for state management
  • Bulma CSS with CSS variables
  • File-based routing (unplugin-vue-router)
  • Supabase auth integration
  • Auto-imports for composables, stores, utils

5.1 — Dashboard Role Change (Planned)

Current Dashboard Role

  • Primary interface: Clients log in to create and manage posts
  • Campaign management: Create campaigns, manage client details
  • Content generation: Form-based post creation
  • Email templates: Upload and process templates

New Dashboard Role

  • Admin view: Agency staff manage all clients from one place
  • Analytics hub: Cross-client performance metrics and insights
  • Content calendar: Visual scheduling and publishing management
  • Configuration panel: Integration settings, brand voice management
  • Client portal (optional): Read-only view for clients who want web access

What Stays (Already Implemented)

  • ✅ Email template upload and processing
  • ✅ Client/campaign CRUD operations
  • ✅ User authentication (Supabase auth)
  • ✅ Base navigation structure
  • ✅ OAuth integration pages

What Changes (To Be Implemented)

  • [ ] Remove post generation forms (moved to Telegram)
  • [ ] Add cross-client analytics dashboard
  • [ ] Add live conversation monitoring
  • [ ] Add content calendar view
  • [ ] Enhance integration management pages

What's New (To Be Implemented)

  • [ ] Real-time Telegram message feed
  • [ ] Advanced brand voice management UI
  • [ ] URL monitoring configuration
  • [ ] Publishing queue visualization
  • [ ] Performance analytics charts

5.2 — New Dashboard Pages (Planned)

Tasks

  • [ ] Create Analytics Dashboard page (/app/analytics)
  • [ ] Create Content Calendar page (/app/calendar)
  • [ ] Create Conversation Log page (/app/client/:id/conversations)
  • [ ] Enhance Brand Voice Manager (in /app/client/:id/settings)
  • [ ] Enhance Integration Settings (in /app/client/:id/integrations)
  • [ ] Create Watched URLs Manager page (/app/client/:id/monitoring)

Notes

  • v-calendar is already installed (package.json)
  • ApexCharts is already installed (vue3-apexcharts)
  • Supabase Realtime is available (@supabase/supabase-js)
  • Base component library (VCard, VButton, etc.) already exists

1. Analytics Dashboard (/app/analytics) - TO BE IMPLEMENTED

Layout:

  • Top metrics cards: Total posts this month, Approval rate, Avg engagement
  • Performance by platform chart (bar chart)
  • Top performing content table
  • Recent activity timeline
  • Client breakdown pie chart

Create front-end/src/pages/app/analytics.vue:

<script setup lang="ts">
  import { ref, onMounted } from 'vue'
  import { useFetch } from '/@src/composables/fetch'
  import ApexChart from 'vue3-apexcharts'

  const analytics = ref<any>(null)
  const loading = ref(true)

  onMounted(async () => {
    const { data, error } = await useFetch('/analytics/dashboard', { method: 'GET' })
    if (!error && data) {
      analytics.value = data
    }
    loading.value = false
  })

  const platformChartOptions = {
    chart: { type: 'bar' },
    xaxis: { categories: ['Instagram', 'Twitter', 'LinkedIn', 'Email', 'Blog'] },
  }
</script>

<template>
  <div class="analytics-dashboard">
    <div class="dashboard-header">
      <h1 class="title is-3">Analytics Dashboard</h1>
      <div class="subtitle">Last 30 days</div>
    </div>

    <!-- Top Metrics -->
    <div class="columns is-multiline">
      <div class="column is-3">
        <div class="metric-card">
          <div class="metric-value">{{ analytics?.total_posts || 0 }}</div>
          <div class="metric-label">Posts Published</div>
        </div>
      </div>
      <div class="column is-3">
        <div class="metric-card">
          <div class="metric-value">{{ analytics?.approval_rate || 0 }}%</div>
          <div class="metric-label">Approval Rate</div>
        </div>
      </div>
      <div class="column is-3">
        <div class="metric-card">
          <div class="metric-value">{{ analytics?.avg_engagement || 0 }}</div>
          <div class="metric-label">Avg Engagement</div>
        </div>
      </div>
      <div class="column is-3">
        <div class="metric-card">
          <div class="metric-value">{{ analytics?.active_clients || 0 }}</div>
          <div class="metric-label">Active Clients</div>
        </div>
      </div>
    </div>

    <!-- Performance Chart -->
    <div class="chart-container">
      <h3 class="subtitle is-5">Performance by Platform</h3>
      <ApexChart
        v-if="analytics"
        type="bar"
        :options="platformChartOptions"
        :series="analytics.platform_series"
        height="350"
      />
    </div>

    <!-- Top Performers Table -->
    <div class="top-performers">
      <h3 class="subtitle is-5">Top Performing Posts</h3>
      <table class="table is-fullwidth">
        <thead>
          <tr>
            <th>Client</th>
            <th>Platform</th>
            <th>Content</th>
            <th>Engagement</th>
            <th>Published</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="post in analytics?.top_posts" :key="post.id">
            <td>{{ post.client_name }}</td>
            <td>{{ post.platform }}</td>
            <td>{{ post.content_preview }}</td>
            <td>{{ post.total_engagement }}</td>
            <td>{{ post.published_at }}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<style scoped>
  .metric-card {
    background: var(--card-bg-color);
    padding: 2rem;
    border-radius: 8px;
    text-align: center;
  }

  .metric-value {
    font-size: 2.5rem;
    font-weight: bold;
    color: var(--primary);
  }

  .metric-label {
    margin-top: 0.5rem;
    color: var(--light-text);
  }

  .chart-container {
    background: var(--card-bg-color);
    padding: 2rem;
    border-radius: 8px;
    margin-top: 2rem;
  }
</style>

Add API endpoint api/src/index.ts:

app.get('/analytics/dashboard', async (request, reply) => {
  // Aggregate cross-client analytics
  const { data: posts, error } = await supabase
    .from('customer_platform_post')
    .select(
      `
      id,
      platform,
      content,
      created_at,
      customer_post:customer_posts!inner(
        customer_id,
        status,
        customer:customer_customer(name)
      ),
      analytics:post_analytics(metric_type, metric_value)
    `
    )
    .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString())

  if (error || !posts) {
    return reply.code(500).send({ error: error?.message })
  }

  // Calculate metrics
  const total_posts = posts.length
  const approved_posts = posts.filter((p) => p.customer_post.status === 'approved').length
  const approval_rate = ((approved_posts / total_posts) * 100).toFixed(1)

  // Group by platform
  const platformCounts = posts.reduce(
    (acc, p) => {
      acc[p.platform] = (acc[p.platform] || 0) + 1
      return acc
    },
    {} as Record<string, number>
  )

  // Top performers
  const top_posts = posts
    .map((p) => ({
      id: p.id,
      client_name: p.customer_post.customer.name,
      platform: p.platform,
      content_preview: p.content.substring(0, 100),
      total_engagement: p.analytics.reduce((sum: number, a: any) => sum + (a.metric_value || 0), 0),
      published_at: p.created_at,
    }))
    .sort((a, b) => b.total_engagement - a.total_engagement)
    .slice(0, 10)

  return {
    total_posts,
    approval_rate,
    avg_engagement: (
      top_posts.reduce((sum, p) => sum + p.total_engagement, 0) / top_posts.length
    ).toFixed(1),
    active_clients: new Set(posts.map((p) => p.customer_post.customer_id)).size,
    platform_series: [
      {
        name: 'Posts',
        data: Object.values(platformCounts),
      },
    ],
    top_posts,
  }
})

2. Content Calendar (/app/calendar)

Install: pnpm add v-calendar

Create front-end/src/pages/app/calendar.vue:

<script setup lang="ts">
  import { ref, onMounted } from 'vue'
  import { Calendar } from 'v-calendar'
  import 'v-calendar/style.css'

  const posts = ref<any[]>([])
  const selectedDate = ref(new Date())

  onMounted(async () => {
    // Fetch scheduled/published posts
    const { data } = await useFetch('/posts/calendar', { method: 'GET' })
    posts.value = data || []
  })

  const attributes = computed(() => {
    return posts.value.map((post) => ({
      key: post.id,
      dates: new Date(post.scheduled_at || post.created_at),
      dot: {
        color: post.status === 'sent' ? 'green' : 'orange',
        class: 'post-dot',
      },
      popover: {
        label: `${post.customer_name}: ${post.title}`,
      },
    }))
  })
</script>

<template>
  <div class="content-calendar">
    <div class="calendar-header">
      <h1 class="title is-3">Content Calendar</h1>
      <div class="filters">
        <button class="button">All Clients</button>
        <button class="button">All Platforms</button>
      </div>
    </div>

    <div class="columns">
      <div class="column is-8">
        <Calendar :attributes="attributes" v-model="selectedDate" expanded />
      </div>

      <div class="column is-4">
        <div class="day-details">
          <h3 class="subtitle is-5">{{ selectedDate.toLocaleDateString() }}</h3>
          <div v-for="post in getPostsForDate(selectedDate)" :key="post.id" class="post-card">
            <div class="post-platform">{{ post.platform }}</div>
            <div class="post-client">{{ post.customer_name }}</div>
            <div class="post-content">{{ post.content_preview }}</div>
            <div class="post-status">{{ post.status }}</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

API endpoint:

app.get('/posts/calendar', async (request, reply) => {
  const { data, error } = await supabase
    .from('customer_posts')
    .select(
      `
      id,
      title,
      status,
      created_at,
      scheduled_at,
      customer:customer_customer(name),
      platform_posts:customer_platform_post(platform, content)
    `
    )
    .gte('created_at', new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString())
    .order('created_at', { ascending: true })

  if (error) {
    return reply.code(500).send({ error: error.message })
  }

  return data?.map((p) => ({
    id: p.id,
    title: p.title,
    status: p.status,
    scheduled_at: p.scheduled_at || p.created_at,
    customer_name: p.customer.name,
    platforms: p.platform_posts?.map((pp: any) => pp.platform) || [],
    content_preview: p.platform_posts?.[0]?.content.substring(0, 100) || '',
  }))
})

3. Conversation Log (/app/client/:id/conversations)

Real-time Telegram message feed from GoClaw sessions.

Create front-end/src/pages/app/client/[id]/conversations.vue:

<script setup lang="ts">
  import { ref, onMounted } from 'vue'
  import { useRoute } from 'vue-router'

  const route = useRoute()
  const clientId = ref(route.params.id)
  const conversations = ref<any[]>([])

  onMounted(async () => {
    const { data } = await useFetch(`/client/${clientId.value}/conversations`)
    conversations.value = data || []
  })
</script>

<template>
  <div class="conversation-log">
    <h2 class="title is-4">Telegram Conversations</h2>

    <div class="message-list">
      <div v-for="msg in conversations" :key="msg.id" :class="['message', msg.role]">
        <div class="message-meta">
          <span class="message-role">{{ msg.role }}</span>
          <span class="message-time">{{ formatTime(msg.timestamp) }}</span>
        </div>
        <div class="message-content">{{ msg.content }}</div>
      </div>
    </div>
  </div>
</template>

4-6. Additional Pages

Create similar pages for:

  • Brand Voice Manager — CRUD interface for client_brand_voice table
  • Integration Settings — OAuth flows, API key management
  • Watched URLs — Manage watched_urls, view change history

5.3 — Real-time Updates

Tasks

  • [ ] Add Supabase Realtime subscriptions for post status
  • [ ] Add WebSocket for live Telegram feed (optional)
  • [ ] Implement toast notifications for events

Supabase Realtime Setup

Install (likely already present):

cd front-end
pnpm add @supabase/supabase-js

Create front-end/src/composables/realtime.ts:

import { ref, onUnmounted } from 'vue'
import { useSupabase } from './supabase'

export function useRealtimePostUpdates() {
  const supabase = useSupabase()
  const posts = ref<any[]>([])

  const channel = supabase
    .channel('post-updates')
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'customer_posts',
      },
      (payload) => {
        console.log('Post update:', payload)

        if (payload.eventType === 'INSERT') {
          posts.value.push(payload.new)
        } else if (payload.eventType === 'UPDATE') {
          const index = posts.value.findIndex((p) => p.id === payload.new.id)
          if (index !== -1) {
            posts.value[index] = payload.new
          }
        }

        // Show toast notification
        useNotyf().success(`Post ${payload.new.status}`)
      }
    )
    .subscribe()

  onUnmounted(() => {
    supabase.removeChannel(channel)
  })

  return { posts }
}

Use in analytics dashboard:

<script setup lang="ts">
  import { useRealtimePostUpdates } from '/@src/composables/realtime'

  const { posts } = useRealtimePostUpdates()
</script>

Toast Notifications

Already have notyf composable. Use it for events:

import { useNotyf } from '/@src/composables/notyf'

const notyf = useNotyf()

// On post approved
notyf.success('Post approved and scheduled!')

// On post published
notyf.info('Instagram post just went live!')

New Tooling Required

  • apexcharts + vue3-apexcharts — Analytics charts
  • v-calendar — Content calendar view
  • Supabase Realtime client (already included)

Verification Checklist

  • [ ] Analytics dashboard shows cross-client metrics
  • [ ] Content calendar displays scheduled/published posts
  • [ ] Conversation log shows Telegram message history
  • [ ] Brand voice manager allows CRUD operations
  • [ ] Integration settings page connects social accounts
  • [ ] Watched URLs page shows monitoring status
  • [ ] Real-time updates trigger when post status changes
  • [ ] Toast notifications appear for key events
  • [ ] Navigation updated to reflect new structure

Next Steps

Proceed to Phase 6: Production Hardening for multi-tenancy, security, deployment, and testing.