scaffold: Vue 3 frontend — router, stores, views, composables, layouts

Complete frontend skeleton: Vite + Vue 3 + TypeScript + Tailwind CSS,
Pinia stores (auth, server, wipe, plugins), authenticated API composable,
full route tree with auth guards, DashboardLayout with sidebar nav,
23 view stubs across auth/admin/public, all TypeScript interfaces.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-14 21:42:21 -05:00
parent 175d6f0a7b
commit e2f2f64d33
46 changed files with 3335 additions and 0 deletions

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
frontend/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2150
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
frontend/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.25"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"tailwindcss": "^4.1.18",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^3.1.5"
}
}

1
frontend/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

7
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
import { RouterView, RouterLink, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useServerStore } from '@/stores/server'
import {
LayoutDashboard,
Server,
Terminal,
Users,
Puzzle,
RefreshCw,
Map,
MessageSquare,
BarChart3,
Bell,
UserPlus,
ShoppingBag,
Package,
Settings,
LogOut,
} from 'lucide-vue-next'
const route = useRoute()
const auth = useAuthStore()
const server = useServerStore()
const navItems = [
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
{ name: 'Server', path: '/server', icon: Server },
{ name: 'Console', path: '/console', icon: Terminal },
{ name: 'Players', path: '/players', icon: Users },
{ name: 'Plugins', path: '/plugins', icon: Puzzle },
{ name: 'Auto-Wiper', path: '/wipes', icon: RefreshCw },
{ name: 'Maps', path: '/maps', icon: Map },
{ name: 'Chat Log', path: '/chat', icon: MessageSquare },
{ name: 'Analytics', path: '/analytics', icon: BarChart3 },
{ name: 'Notifications', path: '/notifications', icon: Bell },
{ name: 'Team', path: '/team', icon: UserPlus },
{ name: 'Store', path: '/store/manage', icon: ShoppingBag },
{ name: 'Modules', path: '/modules', icon: Package },
{ name: 'Settings', path: '/settings', icon: Settings },
]
function isActive(path: string): boolean {
if (path === '/') return route.path === '/'
return route.path.startsWith(path)
}
function handleLogout() {
auth.logout()
}
</script>
<template>
<div class="flex h-screen bg-neutral-950">
<!-- Sidebar -->
<aside class="w-64 bg-neutral-900 border-r border-neutral-800 flex flex-col">
<!-- Logo -->
<div class="p-4 border-b border-neutral-800">
<h1 class="text-xl font-bold text-red-500 tracking-wider">CORROSION</h1>
<p class="text-xs text-neutral-500 mt-1">{{ auth.license?.server_name || 'Server Management' }}</p>
</div>
<!-- Server Status Indicator -->
<div class="px-4 py-3 border-b border-neutral-800">
<div class="flex items-center gap-2">
<div
class="w-2 h-2 rounded-full"
:class="{
'bg-green-500': server.connection?.connection_status === 'connected',
'bg-yellow-500': server.connection?.connection_status === 'degraded',
'bg-red-500': server.connection?.connection_status === 'offline' || !server.connection,
}"
/>
<span class="text-sm text-neutral-400">
{{ server.stats?.player_count ?? 0 }}/{{ server.stats?.max_players ?? 0 }} players
</span>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 overflow-y-auto py-2">
<RouterLink
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="flex items-center gap-3 px-4 py-2 mx-2 rounded-lg text-sm transition-colors"
:class="isActive(item.path)
? 'bg-red-500/10 text-red-400'
: 'text-neutral-400 hover:bg-neutral-800 hover:text-neutral-200'"
>
<component :is="item.icon" class="w-4 h-4" />
{{ item.name }}
</RouterLink>
</nav>
<!-- User -->
<div class="p-4 border-t border-neutral-800">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-neutral-300">{{ auth.user?.username }}</p>
<p class="text-xs text-neutral-500">{{ auth.user?.email }}</p>
</div>
<button
@click="handleLogout"
class="text-neutral-500 hover:text-red-400 transition-colors"
>
<LogOut class="w-4 h-4" />
</button>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-y-auto">
<RouterView />
</main>
</div>
</template>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<div class="min-h-screen bg-neutral-950">
<RouterView />
<footer class="py-6 text-center text-neutral-600 text-sm border-t border-neutral-800">
Powered by <span class="text-red-500 font-semibold">Corrosion</span>
</footer>
</div>
</template>

View File

@@ -0,0 +1,66 @@
import { useAuthStore } from '@/stores/auth'
const API_BASE = '/api'
interface RequestOptions {
method?: string
body?: unknown
headers?: Record<string, string>
}
/**
* Composable for making authenticated API requests.
* Automatically attaches JWT token and handles token refresh.
*/
export function useApi() {
const auth = useAuthStore()
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...options.headers,
}
if (auth.accessToken) {
headers['Authorization'] = `Bearer ${auth.accessToken}`
}
const response = await fetch(`${API_BASE}${path}`, {
method: options.method || 'GET',
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
})
if (response.status === 401) {
// TODO: Attempt token refresh, retry, or redirect to login
auth.logout()
window.location.href = '/login'
throw new Error('Unauthorized')
}
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Request failed' }))
throw new Error(error.message || `HTTP ${response.status}`)
}
return response.json()
}
function get<T>(path: string) {
return request<T>(path)
}
function post<T>(path: string, body?: unknown) {
return request<T>(path, { method: 'POST', body })
}
function put<T>(path: string, body?: unknown) {
return request<T>(path, { method: 'PUT', body })
}
function del<T>(path: string) {
return request<T>(path, { method: 'DELETE' })
}
return { request, get, post, put, del }
}

17
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,17 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
import './style.css'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,163 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes: RouteRecordRaw[] = [
// Auth routes (no layout)
{
path: '/login',
name: 'login',
component: () => import('@/views/auth/LoginView.vue'),
meta: { guest: true },
},
{
path: '/register',
name: 'register',
component: () => import('@/views/auth/RegisterView.vue'),
meta: { guest: true },
},
{
path: '/setup',
name: 'setup-wizard',
component: () => import('@/views/auth/SetupWizardView.vue'),
meta: { requiresAuth: true },
},
// 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'),
},
{
path: 'server',
name: 'server',
component: () => import('@/views/admin/ServerView.vue'),
},
{
path: 'console',
name: 'console',
component: () => import('@/views/admin/ConsoleView.vue'),
},
{
path: 'players',
name: 'players',
component: () => import('@/views/admin/PlayersView.vue'),
},
{
path: 'plugins',
name: 'plugins',
component: () => import('@/views/admin/PluginsView.vue'),
},
{
path: 'wipes',
name: 'wipes',
component: () => import('@/views/admin/WipesView.vue'),
},
{
path: 'wipes/profiles',
name: 'wipe-profiles',
component: () => import('@/views/admin/WipeProfilesView.vue'),
},
{
path: 'wipes/calendar',
name: 'wipe-calendar',
component: () => import('@/views/admin/WipeCalendarView.vue'),
},
{
path: 'wipes/history',
name: 'wipe-history',
component: () => import('@/views/admin/WipeHistoryView.vue'),
},
{
path: 'maps',
name: 'maps',
component: () => import('@/views/admin/MapsView.vue'),
},
{
path: 'chat',
name: 'chat',
component: () => import('@/views/admin/ChatLogView.vue'),
},
{
path: 'analytics',
name: 'analytics',
component: () => import('@/views/admin/AnalyticsView.vue'),
},
{
path: 'notifications',
name: 'notifications',
component: () => import('@/views/admin/NotificationsView.vue'),
},
{
path: 'team',
name: 'team',
component: () => import('@/views/admin/TeamView.vue'),
},
{
path: 'store/manage',
name: 'store-manage',
component: () => import('@/views/admin/StoreManageView.vue'),
},
{
path: 'modules',
name: 'modules',
component: () => import('@/views/admin/ModuleStoreView.vue'),
},
{
path: 'settings',
name: 'settings',
component: () => import('@/views/admin/SettingsView.vue'),
},
],
},
// 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'),
},
],
},
// Status page
{
path: '/status',
name: 'status',
component: () => import('@/views/public/StatusPageView.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// Auth guard
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.guest && auth.isAuthenticated) {
next({ name: 'dashboard' })
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,53 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, License } from '@/types'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const license = ref<License | null>(null)
const accessToken = ref<string | null>(null)
const refreshToken = ref<string | null>(null)
const isAuthenticated = computed(() => !!accessToken.value)
const hasLicense = computed(() => !!license.value)
const isLicenseActive = computed(() => license.value?.status === 'active')
function setAuth(data: { access_token: string; refresh_token: string; user: User }) {
accessToken.value = data.access_token
refreshToken.value = data.refresh_token
user.value = data.user
}
function setLicense(data: License) {
license.value = data
}
function logout() {
user.value = null
license.value = null
accessToken.value = null
refreshToken.value = null
}
function hasModule(moduleSlug: string): boolean {
return license.value?.modules_enabled?.includes(moduleSlug) ?? false
}
return {
user,
license,
accessToken,
refreshToken,
isAuthenticated,
hasLicense,
isLicenseActive,
setAuth,
setLicense,
logout,
hasModule,
}
}, {
persist: {
pick: ['accessToken', 'refreshToken', 'user', 'license'],
},
})

View File

@@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { PluginEntry } from '@/types'
export const usePluginStore = defineStore('plugins', () => {
const plugins = ref<PluginEntry[]>([])
const isLoading = ref(false)
async function fetchPlugins() {
// TODO: GET /api/plugins
}
async function installPlugin(slug: string) {
// TODO: POST /api/plugins/install
}
async function reloadPlugin(pluginId: string) {
// TODO: POST /api/plugins/:id/reload
}
async function searchUmod(query: string) {
// TODO: GET /api/plugins/search?q=query
}
return {
plugins,
isLoading,
fetchPlugins,
installPlugin,
reloadPlugin,
searchUmod,
}
})

View File

@@ -0,0 +1,52 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { ServerConnection, ServerConfig, ServerStats } from '@/types'
export const useServerStore = defineStore('server', () => {
const connection = ref<ServerConnection | null>(null)
const config = ref<ServerConfig | null>(null)
const stats = ref<ServerStats | null>(null)
const isLoading = ref(false)
async function fetchServerStatus() {
// TODO: Fetch from API
}
async function fetchServerConfig() {
// TODO: Fetch from API
}
async function startServer() {
// TODO: POST /api/servers/:id/start
}
async function stopServer() {
// TODO: POST /api/servers/:id/stop
}
async function restartServer() {
// TODO: POST /api/servers/:id/restart
}
async function sendCommand(command: string) {
// TODO: POST /api/servers/:id/command
}
function updateStats(newStats: ServerStats) {
stats.value = newStats
}
return {
connection,
config,
stats,
isLoading,
fetchServerStatus,
fetchServerConfig,
startServer,
stopServer,
restartServer,
sendCommand,
updateStats,
}
})

View File

@@ -0,0 +1,42 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { WipeProfile, WipeSchedule, WipeHistory } from '@/types'
export const useWipeStore = defineStore('wipe', () => {
const profiles = ref<WipeProfile[]>([])
const schedules = ref<WipeSchedule[]>([])
const history = ref<WipeHistory[]>([])
const isLoading = ref(false)
async function fetchProfiles() {
// TODO: GET /api/profiles
}
async function fetchSchedules() {
// TODO: GET /api/schedules
}
async function fetchHistory() {
// TODO: GET /api/wipes/history
}
async function triggerWipe(wipeType: string, profileId: string) {
// TODO: POST /api/wipes/:server_id/trigger
}
async function triggerDryRun(wipeType: string, profileId: string) {
// TODO: POST /api/wipes/:server_id/dry-run
}
return {
profiles,
schedules,
history,
isLoading,
fetchProfiles,
fetchSchedules,
fetchHistory,
triggerWipe,
triggerDryRun,
}
})

22
frontend/src/style.css Normal file
View File

@@ -0,0 +1,22 @@
@import "tailwindcss";
/* Corrosion Platform — Custom Styles */
/* Dark mode is default — Rust servers run at night */
:root {
--corrosion-red: #ef4444;
--corrosion-orange: #f97316;
--corrosion-dark: #0f0f0f;
--corrosion-surface: #1a1a1a;
--corrosion-border: #2a2a2a;
}
body {
@apply bg-neutral-950 text-neutral-100 antialiased;
margin: 0;
min-height: 100vh;
}
#app {
min-height: 100vh;
}

225
frontend/src/types/index.ts Normal file
View File

@@ -0,0 +1,225 @@
// Corrosion Platform — TypeScript Interfaces
export interface User {
id: string
email: string
username: string
totp_enabled: boolean
email_verified: boolean
}
export interface License {
id: string
license_key: string
status: 'active' | 'suspended' | 'expired' | 'revoked'
server_name: string | null
subdomain: string | null
custom_domain: string | null
modules_enabled: string[]
webstore_active: boolean
created_at: string
expires_at: string | null
}
export interface AuthResponse {
access_token: string
refresh_token: string
requires_totp: boolean
user: User
}
export interface ServerConnection {
id: string
license_id: string
connection_type: 'amp' | 'pterodactyl' | 'bare_metal'
server_ip: string | null
server_port: number | null
game_port: number | null
connection_status: 'connected' | 'degraded' | 'offline'
plugin_last_seen: string | null
companion_last_seen: string | null
}
export interface ServerConfig {
id: string
license_id: string
server_name: string
max_players: number | null
world_size: number | null
current_seed: number | null
auto_restart_enabled: boolean
auto_restart_cron: string | null
crash_recovery_enabled: boolean
force_wipe_eligible: boolean
auto_update_on_force_wipe: boolean
config_overrides: Record<string, string>
}
export interface ServerStats {
player_count: number
max_players: number
fps: number
entity_count: number
uptime_seconds: number
memory_usage_mb: number
recorded_at: string
}
export interface WipeProfile {
id: string
license_id: string
profile_name: string
description: string | null
pre_wipe_config: PreWipeConfig
post_wipe_config: PostWipeConfig
}
export interface PreWipeConfig {
enabled: boolean
backup_before_wipe: boolean
countdown_warnings: number[]
countdown_unit: string
countdown_messages: Record<string, string>
kick_players_before_wipe: boolean
kick_message: string
run_final_save: boolean
discord_pre_announce: boolean
pushbullet_notify: boolean
custom_commands_before: string[]
}
export interface PostWipeConfig {
enabled: boolean
verify_server_started: boolean
verify_correct_map: boolean
verify_plugins_loaded: boolean
verify_player_slots_open: boolean
max_restart_attempts: number
health_check_timeout_seconds: number
discord_post_announce: boolean
pushbullet_notify: boolean
rollback_on_failure: boolean
post_wipe_commands: string[]
}
export interface WipeSchedule {
id: string
license_id: string
wipe_profile_id: string
schedule_name: string
wipe_type: 'map' | 'blueprint' | 'full'
cron_expression: string
timezone: string
wipe_blueprints: boolean
is_active: boolean
next_scheduled_run: string | null
}
export interface WipeHistory {
id: string
wipe_type: string
trigger_type: 'scheduled' | 'manual' | 'force_wipe'
status: 'pending' | 'pre_wipe' | 'wiping' | 'post_wipe' | 'success' | 'failed' | 'rolled_back'
started_at: string | null
completed_at: string | null
map_used: string | null
plugins_wiped: string[]
plugins_preserved: string[]
error_message: string | null
}
export interface MapEntry {
id: string
filename: string
display_name: string
file_size_bytes: number
map_type: 'custom' | 'procedural'
seed: number | null
world_size: number | null
thumbnail_path: string | null
checksum: string
}
export interface PluginEntry {
id: string
plugin_name: string
plugin_version: string | null
source: 'umod' | 'corrosion_module' | 'manual'
is_installed: boolean
is_loaded: boolean
wipe_on_map: boolean
wipe_on_bp: boolean
wipe_on_full: boolean
never_wipe: boolean
}
export interface GameAdmin {
id: string
steam_id: string
display_name: string
admin_level: 'owner' | 'admin' | 'moderator'
}
export interface TeamMember {
id: string
user_id: string
username: string
email: string
role_name: string
accepted_at: string | null
}
export interface Role {
id: string
role_name: string
is_system_default: boolean
permissions: Record<string, boolean>
}
export interface ChatMessage {
id: string
steam_id: string
player_name: string
channel: 'global' | 'team' | 'server'
message: string
flagged: boolean
created_at: string
}
export interface NotificationConfig {
discord_webhook_url: string | null
discord_enabled: boolean
pushbullet_api_key: string | null
pushbullet_enabled: boolean
email_alerts_enabled: boolean
notify_wipe_start: boolean
notify_wipe_complete: boolean
notify_wipe_failed: boolean
notify_server_crash: boolean
notify_server_offline: boolean
notify_store_purchase: boolean
}
export interface WebstoreItem {
id: string
category_id: string
item_name: string
description: string | null
price: number
image_url: string | null
item_type: 'kit' | 'rank' | 'currency' | 'custom_command'
delivery_config: { commands: string[] }
is_active: boolean
}
export interface WebstoreTransaction {
id: string
item_id: string
buyer_steam_id: string
buyer_name: string | null
amount: number
currency: string
status: 'pending' | 'paid' | 'delivered' | 'failed' | 'refunded'
delivered_at: string | null
created_at: string
}

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Phase 2 — Implement analytics dashboard
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Analytics</h1>
<p class="text-neutral-400">Coming soon player trends, performance metrics, and server analytics.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement real-time chat feed from the game server
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Chat Log</h1>
<p class="text-neutral-400">Real-time chat feed from your Rust server with search and filtering.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement live server console output with command input
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Server Console</h1>
<p class="text-neutral-400">Live console output and command interface for your Rust server.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement server overview dashboard with key metrics and status
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Dashboard</h1>
<p class="text-neutral-400">Server overview players online, performance metrics, and quick actions.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement map library with upload and rotation management
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Map Library</h1>
<p class="text-neutral-400">Upload custom maps and manage map rotation for your server.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement module store browser for purchasing add-on modules
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Module Store</h1>
<p class="text-neutral-400">Browse and purchase add-on modules to extend your panel capabilities.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement notification channel configuration for Discord and Pushbullet
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Notifications</h1>
<p class="text-neutral-400">Configure Discord webhooks, Pushbullet, and other notification channels.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement player list with kick, ban, and moderation actions
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Player Management</h1>
<p class="text-neutral-400">View connected players and manage kick, ban, and moderation actions.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement installed plugin list and uMod plugin browser
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Plugin Management</h1>
<p class="text-neutral-400">Manage installed plugins and browse the uMod plugin library.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement server configuration and start/stop/restart controls
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Server Management</h1>
<p class="text-neutral-400">Configure server settings and control start, stop, and restart operations.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement settings for license, subdomain, and account management
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Settings</h1>
<p class="text-neutral-400">Manage your license, subdomain configuration, and account settings.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement webstore item and category management
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Webstore Management</h1>
<p class="text-neutral-400">Manage store items, categories, and pricing for your server webstore.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement team management with role-based access control
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Team Management</h1>
<p class="text-neutral-400">Invite team members and manage role-based access to the panel.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement calendar view of scheduled and past wipes
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Wipe Calendar</h1>
<p class="text-neutral-400">Calendar view of upcoming and completed server wipes.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement wipe execution log with status and details
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Wipe History</h1>
<p class="text-neutral-400">Execution logs for all past wipes with status and details.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement wipe profile creation and management
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Wipe Profiles</h1>
<p class="text-neutral-400">Create and manage reusable wipe profiles for different reset configurations.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement auto-wiper with schedules and manual trigger
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Auto-Wiper</h1>
<p class="text-neutral-400">Configure wipe schedules and trigger manual wipes.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement sign-in form with email/password and license validation
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Sign In</h1>
<p class="text-neutral-400">Sign in to your Corrosion server panel.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement registration form with license key field
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Create Account</h1>
<p class="text-neutral-400">Register a new account with your license key.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement multi-step setup wizard for initial server configuration
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Setup Your Server</h1>
<p class="text-neutral-400">Multi-step wizard to configure your Rust server for the first time.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement public-facing server information page
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Server Info</h1>
<p class="text-neutral-400">Public server information rules, description, and connection details.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement public server status page with uptime and metrics
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Server Status</h1>
<p class="text-neutral-400">Live server status, uptime history, and current player count.</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
// TODO: Implement public-facing webstore for player purchases
</script>
<template>
<div class="p-6">
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Store</h1>
<p class="text-neutral-400">Browse and purchase items from the server store.</p>
</div>
</template>

View File

@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

7
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

25
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 5174,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
})