Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare module 'vue' {
AdminMultiLineChart: typeof import('./components/admin/AdminMultiLineChart.vue')['default']
AdminStatsCard: typeof import('./components/admin/AdminStatsCard.vue')['default']
AdminTrendChart: typeof import('./components/admin/AdminTrendChart.vue')['default']
AppNotFoundModal: typeof import('./components/AppNotFoundModal.vue')['default']
AppSetting: typeof import('./components/dashboard/AppSetting.vue')['default']
AppTable: typeof import('./components/tables/AppTable.vue')['default']
AuditLogTable: typeof import('./components/tables/AuditLogTable.vue')['default']
Expand Down Expand Up @@ -80,6 +81,7 @@ declare global {
const AdminMultiLineChart: typeof import('./components/admin/AdminMultiLineChart.vue')['default']
const AdminStatsCard: typeof import('./components/admin/AdminStatsCard.vue')['default']
const AdminTrendChart: typeof import('./components/admin/AdminTrendChart.vue')['default']
const AppNotFoundModal: typeof import('./components/AppNotFoundModal.vue')['default']
const AppSetting: typeof import('./components/dashboard/AppSetting.vue')['default']
const AppTable: typeof import('./components/tables/AppTable.vue')['default']
const AuditLogTable: typeof import('./components/tables/AuditLogTable.vue')['default']
Expand Down
39 changes: 39 additions & 0 deletions src/components/AppNotFoundModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import IconAlertCircle from '~icons/lucide/alert-circle'

const { t } = useI18n()
const router = useRouter()

function goToApps() {
router.push('/app')
}
</script>

<template>
<div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60">
<div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-red-200 dark:border-red-700">
<div class="flex justify-center mb-4">
<div class="flex justify-center items-center w-16 h-16 bg-red-100 rounded-full dark:bg-red-900/30">
<IconAlertCircle class="w-8 h-8 text-red-500" />
</div>
</div>
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
{{ t('app-not-found') }}
</h2>
<p class="mb-6 max-w-sm text-gray-600 dark:text-gray-400">
{{ t('app-not-found-description') }}
</p>
<button
class="inline-flex gap-2 items-center px-6 py-3 text-white bg-primary rounded-lg transition-colors cursor-pointer hover:bg-primary/90 focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:outline-none"
@click="goToApps"
>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
</svg>
{{ t('back-to-apps') }}
</button>
</div>
</div>
</template>
19 changes: 13 additions & 6 deletions src/components/dashboard/BundleUploadsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@ const currentCacheOrgId = ref<string | null>(null)
// Cache for single app name to avoid refetching
const singleAppNameCache = new Map<string, string>()

// Check if we have real data
const hasRealData = computed(() => total.value > 0)

// Generate consistent demo data where total is derived from per-app breakdown
const consistentDemoData = computed(() => {
const days = getDemoDayCount(props.useBillingPeriod, bundleData.value.length)
Expand All @@ -106,8 +103,18 @@ const consistentDemoData = computed(() => {
const demoBundleData = computed(() => consistentDemoData.value.total)
const demoDataByApp = computed(() => consistentDemoData.value.byApp)

// Demo mode detection - also force demo when forceDemo is true
const isDemoMode = computed(() => props.forceDemo || (!hasRealData.value && !isLoading.value))
// Demo mode: show demo data only when forceDemo is true OR user has no apps
// If user has apps, ALWAYS show real data (even if empty)
const isDemoMode = computed(() => {
if (props.forceDemo)
return true
// If user has apps, never show demo data
const dashboardAppsStore = useDashboardAppsStore()
if (dashboardAppsStore.apps.length > 0)
return false
// No apps and store is loaded = show demo
return dashboardAppsStore.isLoaded
})

// Effective values for display
const effectiveBundleData = computed(() => isDemoMode.value ? demoBundleData.value : bundleData.value)
Expand All @@ -116,7 +123,7 @@ const effectiveAppNames = computed(() => isDemoMode.value ? DEMO_APP_NAMES : app
const effectiveTotal = computed(() => isDemoMode.value ? calculateDemoTotal(demoBundleData.value) : total.value)
const effectiveLastDayEvolution = computed(() => isDemoMode.value ? calculateDemoEvolution(demoBundleData.value) : lastDayEvolution.value)

const hasData = computed(() => effectiveBundleData.value.length > 0)
const hasData = computed(() => effectiveTotal.value > 0 || isDemoMode.value)

async function calculateStats(forceRefetch = false) {
const startTime = Date.now()
Expand Down
16 changes: 11 additions & 5 deletions src/components/dashboard/DeploymentStatsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ const deploymentDataByApp = ref<{ [appId: string]: number[] }>({})
const appNames = ref<{ [appId: string]: string }>({})
const isLoading = ref(true)

// Check if we have real data
const hasRealData = computed(() => totalDeployments.value > 0)

// Generate consistent demo data where total is derived from per-app breakdown
const consistentDemoData = computed(() => {
const days = getDemoDayCount(props.useBillingPeriod, deploymentData.value.length)
Expand All @@ -107,8 +104,17 @@ const consistentDemoData = computed(() => {
const demoDeploymentData = computed(() => consistentDemoData.value.total)
const demoDataByApp = computed(() => consistentDemoData.value.byApp)

// Demo mode detection - also force demo when forceDemo is true
const isDemoMode = computed(() => props.forceDemo || (!hasRealData.value && !isLoading.value))
// Demo mode: show demo data only when forceDemo is true OR user has no apps
// If user has apps, ALWAYS show real data (even if empty)
const isDemoMode = computed(() => {
if (props.forceDemo)
return true
// If user has apps, never show demo data
if (dashboardAppsStore.apps.length > 0)
return false
// No apps and store is loaded = show demo
return dashboardAppsStore.isLoaded
})

// Effective values for display
const effectiveDeploymentData = computed(() => isDemoMode.value ? demoDeploymentData.value : deploymentData.value)
Expand Down
19 changes: 17 additions & 2 deletions src/components/dashboard/DevicesStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useChartData } from '~/services/chartDataService'
import { createTooltipConfig, todayLinePlugin, verticalLinePlugin } from '~/services/chartTooltip'
import { generateChartDayLabels, getChartDateRange, normalizeToStartOfDay } from '~/services/date'
import { useSupabase } from '~/services/supabase'
import { useDashboardAppsStore } from '~/stores/dashboardApps'
import { useOrganizationStore } from '~/stores/organization'
import ChartCard from './ChartCard.vue'

Expand Down Expand Up @@ -421,7 +422,20 @@ const processedChartData = computed<ChartData<'line'> | null>(() => {
}
})

const hasData = computed(() => !!(processedChartData.value && processedChartData.value.datasets.length > 0))
// Demo mode: show demo data only when forceDemo is true OR user has no apps
// If user has apps, ALWAYS show real data (even if empty)
const dashboardAppsStore = useDashboardAppsStore()
const isDemoMode = computed(() => {
if (props.forceDemo)
return true
// If user has apps, never show demo data
if (dashboardAppsStore.apps.length > 0)
return false
// No apps and store is loaded = show demo
return dashboardAppsStore.isLoaded
})

const hasData = computed(() => !!(processedChartData.value && processedChartData.value.datasets.length > 0) || isDemoMode.value)

const todayLineOptions = computed(() => {
if (!props.useBillingPeriod || !currentRange.value)
Expand Down Expand Up @@ -513,7 +527,7 @@ async function loadData(forceRefetch = false) {
return
}

// If forceDemo is true, use demo data instead of fetching
// If forceDemo is true (payment failed), use demo data instead of fetching
if (props.forceDemo) {
const { startDate, endDate } = getDateRange()
const days = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1
Expand Down Expand Up @@ -647,6 +661,7 @@ watch(
:title="t('active_users_by_version')"
:is-loading="isLoading"
:has-data="hasData"
:is-demo-data="isDemoMode"
>
Comment on lines 661 to 665
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard demo rendering when chart data is null

When the devices chart fails to load (e.g., API error or empty response), rawChartData stays null, which makes isDemoMode true once loading finishes, so ChartCard renders the slot as “demo data.” However processedChartData is still null, and the <Line :data="processedChartData!"> call now executes under that demo branch, which can throw or render a broken chart while showing the demo overlay. Consider only passing is-demo-data when demo data was actually loaded (e.g., forceDemo) or guarding the slot render on processedChartData being non-null.

Useful? React with 👍 / 👎.

<template #header>
<div class="flex items-start justify-between flex-1 gap-2">
Expand Down
36 changes: 35 additions & 1 deletion src/components/dashboard/LineChartStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,46 @@ const todayLineOptions = computed(() => {
}
})

// Calculate appropriate Y-axis max based on actual data values
const dataMax = computed(() => {
const allValues: number[] = []

// Collect values from main data
const mainData = accumulateData.value.filter((v): v is number => typeof v === 'number' && Number.isFinite(v))
allValues.push(...mainData)

// Collect values from per-app data
Object.values(props.dataByApp || {}).forEach((appData: any) => {
if (Array.isArray(appData)) {
const filtered = appData.filter((v): v is number => typeof v === 'number' && Number.isFinite(v))
allValues.push(...filtered)
}
})

if (allValues.length === 0)
return undefined

const max = Math.max(...allValues)
// Add 20% padding to the max so the line isn't at the very top
// Also ensure a minimum visible range
if (max <= 0)
return undefined

return max * 1.2
})

const chartOptions = computed<ChartOptions & { plugins: { inlineAnnotationPlugin: AnnotationOptions, todayLine?: any } }>(() => {
const hasAppData = Object.keys(props.dataByApp || {}).length > 0
const scales = createStackedChartScales(isDark.value, hasAppData)

// If we have a calculated max, use it to ensure small values are visible
if (dataMax.value !== undefined) {
(scales.y as any).suggestedMax = dataMax.value
}

return {
maintainAspectRatio: false,
scales: createStackedChartScales(isDark.value, hasAppData),
scales,
plugins: {
inlineAnnotationPlugin: generateAnnotations.value,
legend: createLegendConfig(isDark.value, hasAppData),
Expand Down
22 changes: 13 additions & 9 deletions src/components/dashboard/UpdateStatsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,20 @@ const actionDisplayNames = computed(() => ({
fail: capitalize(t('failed')),
}))

// Check if we have real data (at least one non-zero value)
const hasRealData = computed(() => {
return totalInstalled.value > 0 || totalFailed.value > 0 || totalRequested.value > 0
})

// Generate demo data when no real data
// Generate demo data when forceDemo is true
const demoStats = computed(() => generateDemoUpdateStatsData(30))

// Demo mode detection - also force demo when forceDemo is true
const isDemoMode = computed(() => props.forceDemo || (!hasRealData.value && !isLoading.value))
// Demo mode: show demo data only when forceDemo is true OR user has no apps
// If user has apps, ALWAYS show real data (even if empty)
const isDemoMode = computed(() => {
if (props.forceDemo)
return true
// If user has apps, never show demo data
if (dashboardAppsStore.apps.length > 0)
return false
// No apps and store is loaded = show demo
return dashboardAppsStore.isLoaded
})

// Effective values for display
const effectiveChartData = computed(() => isDemoMode.value ? demoStats.value.total : chartUpdateData.value)
Expand All @@ -108,7 +112,7 @@ const effectiveTotalRequested = computed(() => isDemoMode.value ? calculateDemoT
const effectiveTotalUpdates = computed(() => effectiveTotalInstalled.value + effectiveTotalFailed.value + effectiveTotalRequested.value)
const effectiveLastDayEvolution = computed(() => isDemoMode.value ? calculateDemoEvolution(demoStats.value.total) : lastDayEvolution.value)

const hasData = computed(() => effectiveChartData.value?.length > 0)
const hasData = computed(() => effectiveTotalUpdates.value > 0 || isDemoMode.value)

async function calculateStats(forceRefetch = false) {
const startTime = Date.now()
Expand Down
20 changes: 16 additions & 4 deletions src/components/dashboard/Usage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,14 @@ function clearDashboardParams() {
// Function to reload all chart data
async function reloadAllCharts() {
// Force reload of main dashboard data
// End date should be tomorrow at midnight to include all of today's data
const last30DaysEnd = new Date()
last30DaysEnd.setHours(0, 0, 0, 0)
last30DaysEnd.setDate(last30DaysEnd.getDate() + 1) // Tomorrow midnight
// Start date should be 29 days ago at midnight (to get 30 days total including today)
const last30DaysStart = new Date()
last30DaysStart.setDate(last30DaysStart.getDate() - 29) // 30 days including today
last30DaysStart.setHours(0, 0, 0, 0)
last30DaysStart.setDate(last30DaysStart.getDate() - 29)

const orgId = organizationStore.currentOrganization?.gid
if (orgId) {
Expand Down Expand Up @@ -285,11 +290,15 @@ function filterToBillingPeriod(fullData: { mau: number[], storage: number[], ban
}

async function getUsages(forceRefetch = false) {
// Always work with last 30 days of data - normalize first for consistency
// Always work with last 30 days of data
// End date should be tomorrow at midnight to include all of today's data
const last30DaysEnd = new Date()
last30DaysEnd.setHours(0, 0, 0, 0)
const last30DaysStart = new Date(last30DaysEnd)
last30DaysStart.setDate(last30DaysStart.getDate() - 29) // 30 days including today
last30DaysEnd.setDate(last30DaysEnd.getDate() + 1) // Tomorrow midnight
// Start date should be 29 days ago at midnight (to get 30 days total including today)
const last30DaysStart = new Date()
last30DaysStart.setHours(0, 0, 0, 0)
last30DaysStart.setDate(last30DaysStart.getDate() - 29)

// Get billing period dates for filtering
const billingStart = new Date(organizationStore.currentOrganization?.subscription_start ?? new Date())
Expand Down Expand Up @@ -725,20 +734,23 @@ onMounted(() => {
:data="mauData" :data-by-app="mauDataByApp" :app-names="appNames" :title="`${t('monthly-active')}`" :unit="t('units-users')"
:use-billing-period="useBillingPeriod"
:is-loading="isLoading"
:force-demo="forceDemo"
class="col-span-full sm:col-span-6 xl:col-span-4"
/>
<UsageCard
:limits="allLimits.storage" :colors="colors.blue" :data="storageData" :data-by-app="storageDataByApp" :app-names="appNames" :accumulated="useBillingPeriod && showCumulative"
:title="t('Storage')" :unit="storageUnit"
:use-billing-period="useBillingPeriod"
:is-loading="isLoading"
:force-demo="forceDemo"
class="col-span-full sm:col-span-6 xl:col-span-4"
/>
<UsageCard
:limits="allLimits.bandwidth" :colors="colors.orange" :data="bandwidthData" :data-by-app="bandwidthDataByApp" :app-names="appNames" :accumulated="useBillingPeriod && showCumulative"
:title="t('Bandwidth')" :unit="t('units-gb')"
:use-billing-period="useBillingPeriod"
:is-loading="isLoading"
:force-demo="forceDemo"
class="col-span-full sm:col-span-6 xl:col-span-4"
/>
<DevicesStats v-show="appId" :use-billing-period="useBillingPeriod" :accumulated="useBillingPeriod && showCumulative" :reload-trigger="reloadTrigger" :force-demo="forceDemo" class="col-span-full sm:col-span-6 xl:col-span-4" />
Expand Down
44 changes: 28 additions & 16 deletions src/components/dashboard/UsageCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
generateDemoStorageData,
getDemoDayCount,
} from '~/services/demoChartData'
import { useDashboardAppsStore } from '~/stores/dashboardApps'
import ChartCard from './ChartCard.vue'
import LineChartStats from './LineChartStats.vue'

Expand Down Expand Up @@ -46,18 +47,11 @@ const props = defineProps({
type: Boolean,
default: false,
},
})

// Check if we have real data
const hasRealData = computed(() => {
const dataArray = props.data as number[]
// Has data if there's at least one defined, non-zero value
const hasDefinedData = dataArray.some(val => val !== undefined && val !== null && val > 0)
// Or has data by app with at least one defined value
const hasAppData = props.dataByApp && Object.values(props.dataByApp).some((appValues: any) =>
appValues.some((val: any) => val !== undefined && val !== null && val > 0),
)
return hasDefinedData || hasAppData
// When true, show demo data (payment failed state)
forceDemo: {
type: Boolean,
default: false,
},
})

// Get the appropriate data generator based on chart type
Expand Down Expand Up @@ -88,8 +82,18 @@ const consistentDemoData = computed(() => {
const demoData = computed(() => consistentDemoData.value.total)
const demoDataByApp = computed(() => consistentDemoData.value.byApp)

// Use real data or demo data
const isDemoMode = computed(() => !hasRealData.value && !props.isLoading)
// Demo mode: show demo data only when forceDemo is true OR user has no apps
// If user has apps, ALWAYS show real data (even if empty)
const dashboardAppsStore = useDashboardAppsStore()
const isDemoMode = computed(() => {
if (props.forceDemo)
return true
// If user has apps, never show demo data
if (dashboardAppsStore.apps.length > 0)
return false
// No apps and store is loaded = show demo
return dashboardAppsStore.isLoaded
})
const effectiveData = computed(() => isDemoMode.value ? demoData.value : props.data as number[])
const effectiveDataByApp = computed(() => isDemoMode.value ? demoDataByApp.value : props.dataByApp)
const effectiveAppNames = computed(() => isDemoMode.value ? DEMO_APP_NAMES : props.appNames)
Expand Down Expand Up @@ -134,7 +138,15 @@ const lastDayEvolution = computed(() => {
return ((lastValue - previousValue) / previousValue) * 100
})

const hasData = computed(() => effectiveData.value.length > 0)
// Check if there's actual chart data (values in the array), not just a total
// This handles cases like Storage where total can be > 0 but no activity in current period
const hasChartData = computed(() => {
if (isDemoMode.value)
return true
const dataArray = effectiveData.value
// Check if any value in the array is defined and > 0
return dataArray.some(val => typeof val === 'number' && val > 0)
})
</script>

<template>
Expand All @@ -143,7 +155,7 @@ const hasData = computed(() => effectiveData.value.length > 0)
:total="total"
:unit="unit"
:last-day-evolution="lastDayEvolution"
:has-data="hasData"
:has-data="hasChartData"
:is-loading="isLoading"
:is-demo-data="isDemoMode"
>
Expand Down
Loading