Files
corrosion-admin-panel/frontend/src/router/index.ts
Vantz Stockwell 7a07d600e7 feat(fleet): Phase B — fleet overview UI + GET /api/fleet read endpoint
Tenant-scoped fleet read: GET /api/fleet returns agent_hosts (host
metrics) each with their game_instances, plus a summary
(host/instance/online counts). FleetView lists host cards (status, CPU/
mem/disk/uptime/last-heartbeat) with their instances (game, state badge,
uptime); honest empty state -> Server page when no hosts. New 'Fleet'
sidebar nav item across all four game profiles, /fleet route. Store
follows the no-throw-on-fetch pattern (error state, never bricks). The
marketing hero made real from the live fleet tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:32:55 -04:00

520 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
// Extend vue-router's RouteMeta so title/description are typed throughout
declare module 'vue-router' {
interface RouteMeta {
title?: string
description?: string
requiresAuth?: boolean
guest?: boolean
superAdmin?: boolean
}
}
// ---------------------------------------------------------------------------
// Domain detection — runs once at module load
// Env-driven so www./staging hosts route correctly; an exact-match literal
// here once meant any non-canonical marketing host silently got the panel.
// ---------------------------------------------------------------------------
const hostname = typeof window !== 'undefined' ? window.location.hostname : ''
const marketingHosts = (import.meta.env.VITE_MARKETING_HOSTS ?? 'corrosionmgmt.com,www.corrosionmgmt.com')
.split(',')
.map((h: string) => h.trim().toLowerCase())
.filter(Boolean)
const isMarketingDomain = marketingHosts.includes(hostname.toLowerCase())
// ---------------------------------------------------------------------------
// Marketing page children — shared between both domain route sets
// ---------------------------------------------------------------------------
const marketingChildren: RouteRecordRaw[] = [
{
path: '',
name: 'landing',
component: () => import('@/views/marketing/LandingView.vue'),
meta: {
title: 'Corrosion — Game Server Operations for Self-Hosted Communities',
description: 'Management panel for self-hosted survival game servers — Rust, Dune: Awakening, Conan Exiles, Soulmask. Wipe automation, plugins, monitoring. Bring your own server.',
},
},
{
path: 'pricing',
name: 'pricing',
component: () => import('@/views/marketing/PricingView.vue'),
meta: {
title: 'Pricing — Corrosion',
description: 'Plans from $9.99/mo (Hobby, 15 servers) to Network ($99.99+/mo, 50+ servers). Non-commercial and commercial tiers. No hosting fees — bring your own server.',
},
},
{
path: 'how-it-works',
name: 'how-it-works',
component: () => import('@/views/marketing/HowItWorksView.vue'),
meta: {
title: 'How It Works — Corrosion',
description: 'Install one host agent on Windows or Linux. It connects outbound-only to Corrosion — no inbound ports, no SSH. Manage every game instance from the browser.',
},
},
{
path: 'faq',
name: 'faq',
component: () => import('@/views/marketing/FaqView.vue'),
meta: {
title: 'FAQ — Corrosion',
description: 'Honest answers: Corrosion is self-service (BYOS, no hosting). Support is docs + community; 1:1 at $125/hr. Supports Rust, Dune, Conan Exiles, Soulmask.',
},
},
{
path: 'roadmap',
name: 'roadmap',
component: () => import('@/views/marketing/RoadmapView.vue'),
meta: {
title: 'Roadmap — Corrosion',
description: 'Phase 1 shipped: core control plane, auto-wiper, plugin management. In progress: Dune, Conan, Soulmask multi-game blueprints. Planned: API access, integrations.',
},
},
{
path: 'early-access',
name: 'early-access',
component: () => import('@/views/marketing/EarlyAccessView.vue'),
meta: {
title: 'Early Access — Corrosion',
description: 'Join the early access list. Get full control plane access — wipe automation, plugin management, real-time console — and lock in launch pricing.',
},
},
]
// ---------------------------------------------------------------------------
// Panel domain routes — panel.corrosionmgmt.com, localhost, etc.
// Existing behavior, unchanged.
// ---------------------------------------------------------------------------
const panelRoutes: RouteRecordRaw[] = [
// Auth routes (no layout)
{
path: '/login',
name: 'login',
component: () => import('@/views/auth/LoginView.vue'),
meta: { guest: true, title: 'Sign in — Corrosion' },
},
{
path: '/register',
name: 'register',
component: () => import('@/views/auth/RegisterView.vue'),
meta: { guest: true, title: 'Create account — Corrosion' },
},
{
path: '/forgot-password',
name: 'forgot-password',
component: () => import('@/views/auth/ForgotPasswordView.vue'),
meta: { guest: true, title: 'Reset password — Corrosion' },
},
{
path: '/setup',
name: 'setup-wizard',
component: () => import('@/views/auth/SetupWizardView.vue'),
meta: { requiresAuth: true, title: 'Setup — Corrosion' },
},
// Admin dashboard routes (with sidebar layout)
{
path: '/',
component: () => import('@/components/layout/DashboardLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'dashboard',
component: () => import('@/views/admin/DashboardView.vue'),
meta: { title: 'Dashboard — Corrosion' },
},
{
path: 'server',
name: 'server',
component: () => import('@/views/admin/ServerView.vue'),
meta: { title: 'Server — Corrosion' },
},
{
path: 'console',
name: 'console',
component: () => import('@/views/admin/ConsoleView.vue'),
meta: { title: 'Console — Corrosion' },
},
{
path: 'players',
name: 'players',
component: () => import('@/views/admin/PlayersView.vue'),
meta: { title: 'Players — Corrosion' },
},
{
path: 'plugins',
name: 'plugins',
component: () => import('@/views/admin/PluginsView.vue'),
meta: { title: 'Plugins — Corrosion' },
},
{
path: 'files',
name: 'files',
component: () => import('@/views/admin/FileManagerView.vue'),
meta: { title: 'Files — Corrosion' },
},
{
path: 'plugin-configs',
name: 'plugin-configs',
component: () => import('@/views/admin/PluginConfigsView.vue'),
meta: { title: 'Plugin Configs — Corrosion' },
},
{
path: 'loot-builder',
name: 'loot-builder',
component: () => import('@/views/admin/LootBuilderView.vue'),
meta: { title: 'Loot Builder — Corrosion' },
},
{
path: 'teleport-config',
name: 'teleport-config',
component: () => import('@/views/admin/TeleportConfigView.vue'),
meta: { title: 'Teleport Config — Corrosion' },
},
{
path: 'gather-manager',
name: 'gather-manager',
component: () => import('@/views/admin/GatherManagerView.vue'),
meta: { title: 'Gather Manager — Corrosion' },
},
{
path: 'autodoors',
name: 'autodoors',
component: () => import('@/views/admin/AutoDoorsView.vue'),
meta: { title: 'Auto Doors — Corrosion' },
},
{
path: 'kits',
name: 'kits-config',
component: () => import('@/views/admin/KitsView.vue'),
meta: { title: 'Kits — Corrosion' },
},
{
path: 'furnace-splitter',
name: 'furnace-splitter',
component: () => import('@/views/admin/FurnaceSplitterView.vue'),
meta: { title: 'Furnace Splitter — Corrosion' },
},
{
path: 'better-chat',
name: 'better-chat',
component: () => import('@/views/admin/BetterChatView.vue'),
meta: { title: 'Better Chat — Corrosion' },
},
{
path: 'timed-execute',
name: 'timed-execute',
component: () => import('@/views/admin/TimedExecuteView.vue'),
meta: { title: 'Timed Execute — Corrosion' },
},
{
path: 'raidable-bases',
name: 'raidable-bases',
component: () => import('@/views/admin/RaidableBasesView.vue'),
meta: { title: 'Raidable Bases — Corrosion' },
},
{
path: 'wipes',
name: 'wipes',
component: () => import('@/views/admin/WipesView.vue'),
meta: { title: 'Wipes — Corrosion' },
},
{
path: 'wipes/profiles',
name: 'wipe-profiles',
component: () => import('@/views/admin/WipeProfilesView.vue'),
meta: { title: 'Wipe Profiles — Corrosion' },
},
{
path: 'wipes/calendar',
name: 'wipe-calendar',
component: () => import('@/views/admin/WipeCalendarView.vue'),
meta: { title: 'Wipe Calendar — Corrosion' },
},
{
path: 'wipes/history',
name: 'wipe-history',
component: () => import('@/views/admin/WipeHistoryView.vue'),
meta: { title: 'Wipe History — Corrosion' },
},
{
path: 'wipes/analytics',
name: 'wipe-analytics',
component: () => import('@/views/admin/WipeAnalyticsView.vue'),
meta: { title: 'Wipe Analytics — Corrosion' },
},
{
path: 'maps',
name: 'maps',
component: () => import('@/views/admin/MapsView.vue'),
meta: { title: 'Maps — Corrosion' },
},
{
path: 'maps/analytics',
name: 'map-analytics',
component: () => import('@/views/admin/MapAnalyticsView.vue'),
meta: { title: 'Map Analytics — Corrosion' },
},
{
path: 'chat',
name: 'chat',
component: () => import('@/views/admin/ChatLogView.vue'),
meta: { title: 'Chat Log — Corrosion' },
},
{
path: 'analytics',
name: 'analytics',
component: () => import('@/views/admin/AnalyticsView.vue'),
meta: { title: 'Analytics — Corrosion' },
},
{
path: 'retention',
name: 'retention',
component: () => import('@/views/admin/PlayerRetentionView.vue'),
meta: { title: 'Player Retention — Corrosion' },
},
{
path: 'notifications',
name: 'notifications',
component: () => import('@/views/admin/NotificationsView.vue'),
meta: { title: 'Notifications — Corrosion' },
},
{
path: 'team',
name: 'team',
component: () => import('@/views/admin/TeamView.vue'),
meta: { title: 'Team — Corrosion' },
},
{
path: 'store/config',
name: 'store-config',
component: () => import('@/views/admin/StoreConfigView.vue'),
meta: { title: 'Store Config — Corrosion' },
},
{
path: 'store/items',
name: 'store-items',
component: () => import('@/views/admin/StoreItemsView.vue'),
meta: { title: 'Store Items — Corrosion' },
},
{
path: 'store/revenue',
name: 'store-revenue',
component: () => import('@/views/admin/StoreRevenueView.vue'),
meta: { title: 'Store Revenue — Corrosion' },
},
{
path: 'modules',
name: 'modules',
component: () => import('@/views/admin/ModuleStoreView.vue'),
meta: { title: 'Modules — Corrosion' },
},
{
path: 'settings',
name: 'settings',
component: () => import('@/views/admin/SettingsView.vue'),
meta: { title: 'Settings — Corrosion' },
},
{
path: 'schedules',
name: 'schedules',
component: () => import('@/views/admin/SchedulesView.vue'),
meta: { title: 'Schedules — Corrosion' },
},
{
path: 'migration',
name: 'migration',
component: () => import('@/views/admin/MigrationView.vue'),
meta: { title: 'Migration — Corrosion' },
},
{
path: 'changelog',
name: 'changelog',
component: () => import('@/views/admin/ChangelogView.vue'),
meta: { title: 'Changelog — Corrosion' },
},
{
path: 'alerts',
name: 'alerts',
component: () => import('@/views/admin/AlertsView.vue'),
meta: { title: 'Alerts — Corrosion' },
},
{
path: 'fleet',
name: 'fleet',
component: () => import('@/views/admin/FleetView.vue'),
meta: { title: 'Fleet — Corrosion', requiresAuth: true },
},
// Platform Admin views (super-admin only)
{
path: 'admin',
name: 'platform-admin',
component: () => import('@/views/platform-admin/AdminDashboard.vue'),
meta: { superAdmin: true, title: 'Admin — Corrosion' },
},
{
path: 'admin/licenses',
name: 'platform-licenses',
component: () => import('@/views/platform-admin/AdminLicenses.vue'),
meta: { superAdmin: true, title: 'Admin: Licenses — Corrosion' },
},
{
path: 'admin/subscriptions',
name: 'platform-subscriptions',
component: () => import('@/views/platform-admin/AdminSubscriptions.vue'),
meta: { superAdmin: true, title: 'Admin: Subscriptions — Corrosion' },
},
{
path: 'admin/users',
name: 'platform-users',
component: () => import('@/views/platform-admin/AdminUsers.vue'),
meta: { superAdmin: true, title: 'Admin: Users — Corrosion' },
},
{
path: 'admin/servers',
name: 'platform-servers',
component: () => import('@/views/platform-admin/AdminServers.vue'),
meta: { superAdmin: true, title: 'Admin: Servers — Corrosion' },
},
],
},
// Public server site routes (different layout)
{
path: '/s/:subdomain',
component: () => import('@/components/layout/PublicLayout.vue'),
children: [
{
path: '',
name: 'public-server',
component: () => import('@/views/public/ServerInfoView.vue'),
},
{
path: 'store',
name: 'public-store',
component: () => import('@/views/public/StoreView.vue'),
},
],
},
// Marketing site (accessible on panel domain at /site/*)
{
path: '/site',
component: () => import('@/components/layout/MarketingLayout.vue'),
children: marketingChildren,
},
// Status page
{
path: '/status',
name: 'status',
component: () => import('@/views/public/StatusPageView.vue'),
meta: { title: 'Status — Corrosion' },
},
// Catch-all
{
path: '/:pathMatch(.*)*',
redirect: '/',
},
]
// ---------------------------------------------------------------------------
// Marketing domain routes — corrosionmgmt.com (bare domain)
// Marketing pages at root. No admin/auth routes.
// ---------------------------------------------------------------------------
const marketingRoutes: RouteRecordRaw[] = [
// Marketing layout at /
{
path: '/',
component: () => import('@/components/layout/MarketingLayout.vue'),
children: marketingChildren,
},
// Backward compat: /site/* → root-level equivalents
{
path: '/site/:pathMatch(.*)*',
redirect: (to) => {
const sub = to.params.pathMatch
if (!sub || (Array.isArray(sub) && sub.length === 0)) return '/'
const subPath = Array.isArray(sub) ? sub.join('/') : sub
return subPath ? `/${subPath}` : '/'
},
},
// Status page
{
path: '/status',
name: 'status',
component: () => import('@/views/public/StatusPageView.vue'),
meta: { title: 'Status — Corrosion' },
},
// Catch-all: unknown routes → landing page
{
path: '/:pathMatch(.*)*',
redirect: '/',
},
]
// ---------------------------------------------------------------------------
// Router instance
// ---------------------------------------------------------------------------
const router = createRouter({
history: createWebHistory(),
routes: isMarketingDomain ? marketingRoutes : panelRoutes,
})
// ---------------------------------------------------------------------------
// Document title + meta description/OG update on every navigation
// ---------------------------------------------------------------------------
function setOrClearMeta(selector: string, attr: string, value: string): void {
let el = document.querySelector<HTMLMetaElement>(selector)
if (!el) {
el = document.createElement('meta')
// Parse the selector to set the right attribute (name="..." or property="...")
const nameMatch = selector.match(/\[name="([^"]+)"\]/)
const propMatch = selector.match(/\[property="([^"]+)"\]/)
if (nameMatch?.[1]) el.setAttribute('name', nameMatch[1])
if (propMatch?.[1]) el.setAttribute('property', propMatch[1])
document.head.appendChild(el)
}
el.setAttribute(attr, value)
}
router.afterEach((to) => {
// Title
document.title = to.meta.title ?? 'Corrosion Management'
// Description
const desc = to.meta.description ?? ''
setOrClearMeta('meta[name="description"]', 'content', desc)
// OG title
setOrClearMeta('meta[property="og:title"]', 'content', to.meta.title ?? 'Corrosion Management')
// OG description
setOrClearMeta('meta[property="og:description"]', 'content', desc)
})
// Auth guard — only meaningful on panel domain (marketing has no requiresAuth routes)
router.beforeEach((to, _from, next) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else if (to.meta.superAdmin && !auth.isSuperAdmin) {
next({ name: 'dashboard' })
} else if (to.meta.guest && auth.isAuthenticated) {
next({ name: 'dashboard' })
} else {
next()
}
})
export default router