diff --git a/src/components/dashboard/LineChartStats.vue b/src/components/dashboard/LineChartStats.vue
index 2476d880d9..e1fac72e19 100644
--- a/src/components/dashboard/LineChartStats.vue
+++ b/src/components/dashboard/LineChartStats.vue
@@ -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(() => {
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),
diff --git a/src/components/dashboard/UpdateStatsCard.vue b/src/components/dashboard/UpdateStatsCard.vue
index 99f1fbe924..eb5a9ffe08 100644
--- a/src/components/dashboard/UpdateStatsCard.vue
+++ b/src/components/dashboard/UpdateStatsCard.vue
@@ -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)
@@ -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()
diff --git a/src/components/dashboard/Usage.vue b/src/components/dashboard/Usage.vue
index d8d6620ded..b16cb6c8ea 100644
--- a/src/components/dashboard/Usage.vue
+++ b/src/components/dashboard/Usage.vue
@@ -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) {
@@ -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())
@@ -725,6 +734,7 @@ 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"
/>
{
: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"
/>
{
: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"
/>
diff --git a/src/components/dashboard/UsageCard.vue b/src/components/dashboard/UsageCard.vue
index e25232998a..8556992df9 100644
--- a/src/components/dashboard/UsageCard.vue
+++ b/src/components/dashboard/UsageCard.vue
@@ -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'
@@ -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
@@ -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)
@@ -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)
+})
@@ -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"
>
diff --git a/src/pages/app/[package].vue b/src/pages/app/[package].vue
index 2ed8c6c367..ea7c8a0bf2 100644
--- a/src/pages/app/[package].vue
+++ b/src/pages/app/[package].vue
@@ -1,9 +1,8 @@