- | {{ new Date(alert.triggered_at).toLocaleString() }} |
+ {{ safeDate(alert.triggered_at) }} |
{{ alert.alert_type }} |
{
Avg Players
- {{ summary.avg_players.toFixed(1) }}
+ {{ safeFixed(summary?.avg_players, 1) }}
Last {{ timeRange }}
@@ -276,7 +277,7 @@ onMounted(() => {
Uptime
- {{ summary.uptime_percentage.toFixed(1) }}%
+ {{ safeFixed(summary?.uptime_percentage, 1) }}%
Last {{ timeRange }}
diff --git a/frontend/src/views/admin/MapAnalyticsView.vue b/frontend/src/views/admin/MapAnalyticsView.vue
index 3d9d02b..ce41893 100644
--- a/frontend/src/views/admin/MapAnalyticsView.vue
+++ b/frontend/src/views/admin/MapAnalyticsView.vue
@@ -5,6 +5,7 @@ import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import { useApi } from '@/composables/useApi'
import type { MapAnalyticsSummary } from '@/types'
+import { safeFixed } from '@/utils/formatters'
const api = useApi()
@@ -112,9 +113,9 @@ const downloadCSV = () => {
m.map_name,
m.seed ?? 'N/A',
m.times_used,
- m.avg_players.toFixed(1),
+ safeFixed(m.avg_players, 1),
m.peak_players,
- m.effectiveness_score.toFixed(1)
+ safeFixed(m.effectiveness_score, 1)
])
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n')
@@ -183,7 +184,7 @@ onMounted(() => {
{{ analytics.best_performing_map ?? 'No data' }}
- Avg {{ analytics.maps[0]?.avg_players.toFixed(1) }} players
+ Avg {{ safeFixed(analytics?.maps?.[0]?.avg_players, 1) }} players
@@ -193,7 +194,7 @@ onMounted(() => {
Rotation Effectiveness
- {{ analytics.rotation_effectiveness.toFixed(1) }}%
+ {{ safeFixed(analytics?.rotation_effectiveness, 1) }}%
Overall rotation health
@@ -245,7 +246,7 @@ onMounted(() => {
| {{ map.map_name }} |
{{ map.seed ?? '—' }} |
{{ map.times_used }} |
- {{ map.avg_players.toFixed(1) }} |
+ {{ safeFixed(map.avg_players, 1) }} |
{{ map.peak_players }} |
{
'bg-red-500/10 text-red-400': map.effectiveness_score < 60
}"
>
- {{ map.effectiveness_score.toFixed(1) }}%
+ {{ safeFixed(map.effectiveness_score, 1) }}%
|
diff --git a/frontend/src/views/admin/MapsView.vue b/frontend/src/views/admin/MapsView.vue
index 7f5e346..6378f7c 100644
--- a/frontend/src/views/admin/MapsView.vue
+++ b/frontend/src/views/admin/MapsView.vue
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import type { MapEntry } from '@/types'
import { Map, Upload, Trash2, RefreshCw } from 'lucide-vue-next'
+import { safeFileSize } from '@/utils/formatters'
const api = useApi()
@@ -10,8 +11,7 @@ const maps = ref([])
const isLoading = ref(false)
function formatSize(bytes: number): string {
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+ return safeFileSize(bytes)
}
function typeBadgeClass(type: string): string {
diff --git a/frontend/src/views/admin/MigrationView.vue b/frontend/src/views/admin/MigrationView.vue
index 5d735c9..05a9507 100644
--- a/frontend/src/views/admin/MigrationView.vue
+++ b/frontend/src/views/admin/MigrationView.vue
@@ -2,6 +2,7 @@
import { ref, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import { Download, Upload, FileText, Loader2 } from 'lucide-vue-next'
+import { safeFileSize, safeDate } from '@/utils/formatters'
interface ExportRecord {
id: string
@@ -69,11 +70,7 @@ async function importData() {
}
function formatBytes(bytes: number): string {
- if (bytes === 0) return '0 B'
- const k = 1024
- const sizes = ['B', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
+ return safeFileSize(bytes)
}
onMounted(() => {
@@ -139,7 +136,7 @@ onMounted(() => {
| {{ exp.export_type.replace('_', ' ') }} |
- {{ new Date(exp.created_at).toLocaleString() }} |
+ {{ safeDate(exp.created_at) }} |
{{ formatBytes(exp.file_size_bytes) }} |
{
{{ module.name }}
- ${{ module.price.toFixed(2) }}
+ ${{ safeFixed(module.price, 2) }}
{{ module.description }}
@@ -385,7 +386,7 @@ onMounted(() => {
One-time purchase
- ${{ selectedModule.price.toFixed(2) }}
+ ${{ safeFixed(selectedModule.price, 2) }}
Total
- ${{ selectedModule.price.toFixed(2) }}
+ ${{ safeFixed(selectedModule.price, 2) }}
diff --git a/frontend/src/views/admin/PlayerRetentionView.vue b/frontend/src/views/admin/PlayerRetentionView.vue
index 971b37f..fcc0c14 100644
--- a/frontend/src/views/admin/PlayerRetentionView.vue
+++ b/frontend/src/views/admin/PlayerRetentionView.vue
@@ -4,6 +4,7 @@ import { Users, TrendingUp, Clock, Download, BarChart3 } from 'lucide-vue-next'
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import { useApi } from '@/composables/useApi'
+import { safeFixed } from '@/utils/formatters'
const api = useApi()
@@ -88,7 +89,7 @@ const renderCharts = () => {
formatter: (params: any) => {
let tooltip = `${params[0].axisValue} `
params.forEach((param: any) => {
- tooltip += `${param.marker} ${param.seriesName}: ${param.value.toFixed(1)}% `
+ tooltip += `${param.marker} ${param.seriesName}: ${safeFixed(param.value, 1)}% `
})
return tooltip
}
@@ -236,7 +237,7 @@ onMounted(() => {
Avg Session
- {{ retentionData.summary.avg_session_duration_minutes.toFixed(0) }}m
+ {{ safeFixed(retentionData?.summary?.avg_session_duration_minutes, 0) }}m
Duration
@@ -308,15 +309,15 @@ onMounted(() => {
|
{{ wipe.returned_24h }}
- ({{ wipe.retention_24h_percent.toFixed(1) }}%)
+ ({{ safeFixed(wipe.retention_24h_percent, 1) }}%)
|
{{ wipe.returned_48h }}
- ({{ wipe.retention_48h_percent.toFixed(1) }}%)
+ ({{ safeFixed(wipe.retention_48h_percent, 1) }}%)
|
{{ wipe.returned_72h }}
- ({{ wipe.retention_72h_percent.toFixed(1) }}%)
+ ({{ safeFixed(wipe.retention_72h_percent, 1) }}%)
|
diff --git a/frontend/src/views/admin/StoreItemsView.vue b/frontend/src/views/admin/StoreItemsView.vue
index 0e3dbf2..efc0c5e 100644
--- a/frontend/src/views/admin/StoreItemsView.vue
+++ b/frontend/src/views/admin/StoreItemsView.vue
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
import type { StoreCategory, StoreItem } from '@/types'
import { ShoppingBag, Plus, Trash2, RefreshCw, Edit2, DollarSign, X } from 'lucide-vue-next'
+import { safeFixed } from '@/utils/formatters'
const api = useApi()
@@ -405,7 +406,7 @@ onMounted(() => {
- {{ item.price.toFixed(2) }}
+ {{ safeFixed(item.price, 2) }}
|
diff --git a/frontend/src/views/admin/StoreRevenueView.vue b/frontend/src/views/admin/StoreRevenueView.vue
index 3d22cac..7ae481c 100644
--- a/frontend/src/views/admin/StoreRevenueView.vue
+++ b/frontend/src/views/admin/StoreRevenueView.vue
@@ -5,6 +5,7 @@ import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import { useApi } from '@/composables/useApi'
import type { StoreTransaction } from '@/types'
+import { safeCurrency, safeDate } from '@/utils/formatters'
const api = useApi()
@@ -41,7 +42,7 @@ const filteredTransactions = computed(() => {
// Format currency properly
const formatCurrency = (amount: number, currency: string = 'USD'): string => {
const symbol = currency === 'USD' ? '$' : currency
- return `${symbol}${amount.toFixed(2)}`
+ return safeCurrency(amount, symbol)
}
// Status badge color classes
@@ -58,13 +59,7 @@ const statusBadgeClass = (status: string): string => {
// Format date for display
const formatDate = (dateStr: string): string => {
- return new Date(dateStr).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })
+ return safeDate(dateStr, '—')
}
// Load transactions
@@ -122,8 +117,8 @@ const renderRevenueChart = () => {
borderColor: '#2a2a2a',
textStyle: { color: '#e5e5e5' },
formatter: (params: any) => {
- const value = params[0].data
- return `${params[0].axisValue} Revenue: $${value.toFixed(2)}`
+ const value = params[0]?.data
+ return `${params[0]?.axisValue ?? 'Unknown'} Revenue: ${safeCurrency(value, '$')}`
}
},
grid: {
diff --git a/frontend/src/views/admin/WipeAnalyticsView.vue b/frontend/src/views/admin/WipeAnalyticsView.vue
index 59ad7d9..1bd6374 100644
--- a/frontend/src/views/admin/WipeAnalyticsView.vue
+++ b/frontend/src/views/admin/WipeAnalyticsView.vue
@@ -5,6 +5,7 @@ import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import { useApi } from '@/composables/useApi'
import type { WipePerformanceMetrics } from '@/types'
+import { safeFixed } from '@/utils/formatters'
const api = useApi()
@@ -173,7 +174,7 @@ const renderCharts = () => {
}
durationTrendChartInstance = echarts.init(durationTrendChart.value)
- const durations = metrics.value.wipes.map(w => (w.duration_seconds / 60).toFixed(1))
+ const durations = metrics.value.wipes.map(w => safeFixed(w.duration_seconds / 60, 1))
const dates = metrics.value.wipes.map(w => new Date(w.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
@@ -241,7 +242,7 @@ const downloadCSV = async () => {
let csv = 'Date,Duration (seconds),Peak Population,Hours to Peak,Success\n'
metrics.value.wipes.forEach(wipe => {
- csv += `${new Date(wipe.date).toISOString()},${wipe.duration_seconds},${wipe.peak_population},${wipe.hours_to_peak.toFixed(2)},${wipe.success}\n`
+ csv += `${new Date(wipe.date).toISOString()},${wipe.duration_seconds},${wipe.peak_population},${safeFixed(wipe.hours_to_peak, 2)},${wipe.success}\n`
})
const blob = new Blob([csv], { type: 'text/csv' })
@@ -305,7 +306,7 @@ onMounted(() => {
Success Rate
- {{ metrics.success_rate_percent.toFixed(1) }}%
+ {{ safeFixed(metrics?.success_rate_percent, 1) }}%
{{ metrics.successful_wipes }}/{{ metrics.total_wipes }} wipes
@@ -324,7 +325,7 @@ onMounted(() => {
Peak Population
Day 1
- {{ metrics.population_curve.day_1_avg.toFixed(1) }} avg players
+ {{ safeFixed(metrics?.population_curve?.day_1_avg, 1) }} avg players
@@ -346,7 +347,7 @@ onMounted(() => {
- • Your best wipe day is {{ metrics.optimal_wipe_day }} at {{ metrics.optimal_wipe_hour }}:00 based on post-wipe population peaks.
-
- • Players peak on Day 1 ({{ metrics.population_curve.day_1_avg.toFixed(0) }} avg). Consider weekly wipes to maintain engagement.
+ • Players peak on Day 1 ({{ safeFixed(metrics?.population_curve?.day_1_avg, 0) }} avg). Consider weekly wipes to maintain engagement.
-
• Average wipe duration is {{ formatDuration(metrics.avg_duration_seconds) }}. Review pre-wipe commands for optimization opportunities.
diff --git a/frontend/src/views/admin/WipeHistoryView.vue b/frontend/src/views/admin/WipeHistoryView.vue
index f0c18e2..722b493 100644
--- a/frontend/src/views/admin/WipeHistoryView.vue
+++ b/frontend/src/views/admin/WipeHistoryView.vue
@@ -2,6 +2,7 @@
import { onMounted } from 'vue'
import { useWipeStore } from '@/stores/wipe'
import { History, RefreshCw } from 'lucide-vue-next'
+import { safeDate } from '@/utils/formatters'
const wipeStore = useWipeStore()
@@ -85,7 +86,7 @@ onMounted(() => {
|
- {{ wipe.started_at ? new Date(wipe.started_at).toLocaleString() : '\u2014' }}
+ {{ safeDate(wipe.started_at, '\u2014') }}
|
{{ duration(wipe.started_at, wipe.completed_at) }}
diff --git a/frontend/src/views/admin/WipesView.vue b/frontend/src/views/admin/WipesView.vue
index d0c291e..93e3692 100644
--- a/frontend/src/views/admin/WipesView.vue
+++ b/frontend/src/views/admin/WipesView.vue
@@ -4,6 +4,7 @@ import { useWipeStore } from '@/stores/wipe'
import { useServerStore } from '@/stores/server'
import { RefreshCw, Zap, Clock, AlertTriangle, Loader2 } from 'lucide-vue-next'
import { RouterLink } from 'vue-router'
+import { safeDate } from '@/utils/formatters'
const wipeStore = useWipeStore()
const server = useServerStore()
@@ -160,7 +161,7 @@ onMounted(() => {
>
{{ wipe.wipe_type }} wipe
- {{ wipe.trigger_type }} · {{ wipe.started_at ? new Date(wipe.started_at).toLocaleString() : 'Pending' }}
+ {{ wipe.trigger_type }} · {{ safeDate(wipe.started_at, 'Pending') }}
{
Total Subscribers
- {{ totalSubscribers.toLocaleString() }}
+ {{ safeLocaleString(totalSubscribers) }}
@@ -90,7 +91,7 @@ onMounted(() => {
Total MRR
- ${{ totalMrr.toFixed(2) }}
+ {{ safeCurrency(totalMrr, '$') }}
diff --git a/frontend/src/views/public/StatusPageView.vue b/frontend/src/views/public/StatusPageView.vue
index 3436435..cb93612 100644
--- a/frontend/src/views/public/StatusPageView.vue
+++ b/frontend/src/views/public/StatusPageView.vue
@@ -2,6 +2,7 @@
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { Server, Users, Activity, TrendingUp, Search } from 'lucide-vue-next'
import { useApi } from '@/composables/useApi'
+import { safeFixed } from '@/utils/formatters'
interface ServerStatus {
server_name: string
@@ -199,7 +200,7 @@ onUnmounted(() => {
Platform Uptime
- {{ platformHealth.uptime_percent.toFixed(1) }}%
+ {{ safeFixed(platformHealth.uptime_percent, 1) }}%
@@ -297,15 +298,15 @@ onUnmounted(() => {
- {{ server.uptime_24h_percent.toFixed(1) }}%
+ {{ safeFixed(server.uptime_24h_percent, 1) }}%
24h
- {{ server.uptime_7d_percent.toFixed(1) }}%
+ {{ safeFixed(server.uptime_7d_percent, 1) }}%
7d
- {{ server.uptime_30d_percent.toFixed(1) }}%
+ {{ safeFixed(server.uptime_30d_percent, 1) }}%
30d
diff --git a/frontend/src/views/public/StoreView.vue b/frontend/src/views/public/StoreView.vue
index 62599cc..8988a67 100644
--- a/frontend/src/views/public/StoreView.vue
+++ b/frontend/src/views/public/StoreView.vue
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import type { PublicStoreInfo, PublicStoreItem, StorePurchaseRequest, StorePurchaseResponse } from '@/types'
import { ShoppingCart, Package, Filter, X, AlertCircle, ExternalLink, Check } from 'lucide-vue-next'
+import { safeCurrency } from '@/utils/formatters'
const route = useRoute()
const subdomain = computed(() => route.params.subdomain as string)
@@ -138,7 +139,7 @@ async function confirmPurchase() {
}
function formatPrice(price: number): string {
- return `$${price.toFixed(2)}`
+ return safeCurrency(price, '$')
}
function itemTypeBadgeClass(itemType: string): string {
|