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_voicetable - 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 chartsv-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.