Compare commits
5 Commits
1edaaf985d
...
v1.0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
180631989a | ||
|
|
23decd9b08 | ||
|
|
8b84bba165 | ||
|
|
9a5b93dd08 | ||
|
|
3545e6f5c8 |
@@ -1,4 +1,4 @@
|
|||||||
name: Build Companion Agent
|
name: Build Host Agent
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -26,19 +26,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd companion-agent
|
cd companion-agent
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-companion-linux-amd64 ./cmd/agent
|
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-host-agent-linux-amd64 ./cmd/agent
|
||||||
chmod +x bin/corrosion-companion-linux-amd64
|
chmod +x bin/corrosion-host-agent-linux-amd64
|
||||||
|
|
||||||
- name: Build Windows AMD64
|
- name: Build Windows AMD64
|
||||||
run: |
|
run: |
|
||||||
cd companion-agent
|
cd companion-agent
|
||||||
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-companion-windows-amd64.exe ./cmd/agent
|
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" -o bin/corrosion-host-agent-windows-amd64.exe ./cmd/agent
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
run: |
|
run: |
|
||||||
cd companion-agent/bin
|
cd companion-agent/bin
|
||||||
sha256sum corrosion-companion-linux-amd64 > checksums.txt
|
sha256sum corrosion-host-agent-linux-amd64 > checksums.txt
|
||||||
sha256sum corrosion-companion-windows-amd64.exe >> checksums.txt
|
sha256sum corrosion-host-agent-windows-amd64.exe >> checksums.txt
|
||||||
cat checksums.txt
|
cat checksums.txt
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
RESPONSE=$(curl -s -X POST \
|
RESPONSE=$(curl -s -X POST \
|
||||||
-H "Authorization: token ${RELEASE_TOKEN}" \
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\": \"${VERSION}\", \"name\": \"Companion Agent ${VERSION}\", \"body\": \"Companion Agent release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
-d "{\"tag_name\": \"${VERSION}\", \"name\": \"Corrosion Host Agent ${VERSION}\", \"body\": \"Corrosion Host Agent release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
|
||||||
"${API_URL}/repos/${REPO}/releases")
|
"${API_URL}/repos/${REPO}/releases")
|
||||||
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||||
|
|
||||||
@@ -68,15 +68,15 @@ jobs:
|
|||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-H "Authorization: token ${RELEASE_TOKEN}" \
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
--data-binary @companion-agent/bin/corrosion-companion-linux-amd64 \
|
--data-binary @companion-agent/bin/corrosion-host-agent-linux-amd64 \
|
||||||
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-companion-linux-amd64"
|
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-host-agent-linux-amd64"
|
||||||
|
|
||||||
# Upload Windows binary
|
# Upload Windows binary
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-H "Authorization: token ${RELEASE_TOKEN}" \
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
--data-binary @companion-agent/bin/corrosion-companion-windows-amd64.exe \
|
--data-binary @companion-agent/bin/corrosion-host-agent-windows-amd64.exe \
|
||||||
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-companion-windows-amd64.exe"
|
"${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=corrosion-host-agent-windows-amd64.exe"
|
||||||
|
|
||||||
# Upload checksums
|
# Upload checksums
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
@@ -89,43 +89,43 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
CDN_URL="https://cdn.corrosionmgmt.com"
|
CDN_URL="https://cdn.corrosionmgmt.com"
|
||||||
|
|
||||||
# Upload Linux binary to /companion/latest/
|
# Upload Linux binary to /host-agent/latest/
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-F "file=@companion-agent/bin/corrosion-companion-linux-amd64" \
|
-F "file=@companion-agent/bin/corrosion-host-agent-linux-amd64" \
|
||||||
"${CDN_URL}/companion/latest/corrosion-companion-linux-amd64"
|
"${CDN_URL}/host-agent/latest/corrosion-host-agent-linux-amd64"
|
||||||
|
|
||||||
# Upload Windows binary to /companion/latest/
|
# Upload Windows binary to /host-agent/latest/
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-F "file=@companion-agent/bin/corrosion-companion-windows-amd64.exe" \
|
-F "file=@companion-agent/bin/corrosion-host-agent-windows-amd64.exe" \
|
||||||
"${CDN_URL}/companion/latest/corrosion-companion-windows-amd64.exe"
|
"${CDN_URL}/host-agent/latest/corrosion-host-agent-windows-amd64.exe"
|
||||||
|
|
||||||
# Upload checksums
|
# Upload checksums
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-F "file=@companion-agent/bin/checksums.txt" \
|
-F "file=@companion-agent/bin/checksums.txt" \
|
||||||
"${CDN_URL}/companion/latest/checksums.txt"
|
"${CDN_URL}/host-agent/latest/checksums.txt"
|
||||||
|
|
||||||
# Also upload versioned copies
|
# Also upload versioned copies
|
||||||
VERSION=${{ steps.version.outputs.VERSION }}
|
VERSION=${{ steps.version.outputs.VERSION }}
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-F "file=@companion-agent/bin/corrosion-companion-linux-amd64" \
|
-F "file=@companion-agent/bin/corrosion-host-agent-linux-amd64" \
|
||||||
"${CDN_URL}/companion/${VERSION}/corrosion-companion-linux-amd64"
|
"${CDN_URL}/host-agent/${VERSION}/corrosion-host-agent-linux-amd64"
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-F "file=@companion-agent/bin/corrosion-companion-windows-amd64.exe" \
|
-F "file=@companion-agent/bin/corrosion-host-agent-windows-amd64.exe" \
|
||||||
"${CDN_URL}/companion/${VERSION}/corrosion-companion-windows-amd64.exe"
|
"${CDN_URL}/host-agent/${VERSION}/corrosion-host-agent-windows-amd64.exe"
|
||||||
curl -s -X POST \
|
curl -s -X POST \
|
||||||
-F "file=@companion-agent/bin/checksums.txt" \
|
-F "file=@companion-agent/bin/checksums.txt" \
|
||||||
"${CDN_URL}/companion/${VERSION}/checksums.txt"
|
"${CDN_URL}/host-agent/${VERSION}/checksums.txt"
|
||||||
|
|
||||||
echo "CDN upload complete: ${CDN_URL}/companion/latest/"
|
echo "CDN upload complete: ${CDN_URL}/host-agent/latest/"
|
||||||
|
|
||||||
- name: Build Summary
|
- name: Build Summary
|
||||||
run: |
|
run: |
|
||||||
echo "## Companion Agent Build Complete" >> $GITHUB_STEP_SUMMARY
|
echo "## Corrosion Host Agent Build Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "**Version:** ${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
echo "**Version:** ${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "**Commit:** ${GITHUB_SHA:0:7}" >> $GITHUB_STEP_SUMMARY
|
echo "**Commit:** ${GITHUB_SHA:0:7}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Built Artifacts:" >> $GITHUB_STEP_SUMMARY
|
echo "### Built Artifacts:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- Linux AMD64 ($(stat -c%s companion-agent/bin/corrosion-companion-linux-amd64) bytes)" >> $GITHUB_STEP_SUMMARY
|
echo "- Linux AMD64 ($(stat -c%s companion-agent/bin/corrosion-host-agent-linux-amd64) bytes)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- Windows AMD64 ($(stat -c%s companion-agent/bin/corrosion-companion-windows-amd64.exe) bytes)" >> $GITHUB_STEP_SUMMARY
|
echo "- Windows AMD64 ($(stat -c%s companion-agent/bin/corrosion-host-agent-windows-amd64.exe) bytes)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- SHA256 checksums" >> $GITHUB_STEP_SUMMARY
|
echo "- SHA256 checksums" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { FurnaceSplitterModule } from './modules/furnacesplitter/furnacesplitter
|
|||||||
import { BetterChatModule } from './modules/betterchat/betterchat.module';
|
import { BetterChatModule } from './modules/betterchat/betterchat.module';
|
||||||
import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module';
|
import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module';
|
||||||
import { RaidableBasesModule } from './modules/raidablebases/raidablebases.module';
|
import { RaidableBasesModule } from './modules/raidablebases/raidablebases.module';
|
||||||
|
import { EarlyAccessModule } from './modules/early-access/early-access.module';
|
||||||
|
|
||||||
// Shared Services
|
// Shared Services
|
||||||
import { NatsService } from './services/nats.service';
|
import { NatsService } from './services/nats.service';
|
||||||
@@ -123,6 +124,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway';
|
|||||||
BetterChatModule,
|
BetterChatModule,
|
||||||
TimedExecuteModule,
|
TimedExecuteModule,
|
||||||
RaidableBasesModule,
|
RaidableBasesModule,
|
||||||
|
EarlyAccessModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Global guards (order matters: auth first, then license, then permissions)
|
// Global guards (order matters: auth first, then license, then permissions)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { IsEmail, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateEarlyAccessDto {
|
||||||
|
@ApiProperty({ example: 'admin@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'rust', description: 'Primary game interest or server count' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(10)
|
||||||
|
server_count?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Public } from '../../common/decorators/public.decorator';
|
||||||
|
import { EarlyAccessService } from './early-access.service';
|
||||||
|
import { CreateEarlyAccessDto } from './dto/create-early-access.dto';
|
||||||
|
|
||||||
|
@ApiTags('early-access')
|
||||||
|
@Controller()
|
||||||
|
export class EarlyAccessController {
|
||||||
|
constructor(private readonly earlyAccessService: EarlyAccessService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('early-access')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: 'Register for early access' })
|
||||||
|
async register(@Body() dto: CreateEarlyAccessDto) {
|
||||||
|
return this.earlyAccessService.register(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend-nest/src/modules/early-access/early-access.module.ts
Normal file
12
backend-nest/src/modules/early-access/early-access.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { EarlyAccessSignup } from '../../entities/early-access-signup.entity';
|
||||||
|
import { EarlyAccessController } from './early-access.controller';
|
||||||
|
import { EarlyAccessService } from './early-access.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([EarlyAccessSignup])],
|
||||||
|
controllers: [EarlyAccessController],
|
||||||
|
providers: [EarlyAccessService],
|
||||||
|
})
|
||||||
|
export class EarlyAccessModule {}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { EarlyAccessSignup } from '../../entities/early-access-signup.entity';
|
||||||
|
import { CreateEarlyAccessDto } from './dto/create-early-access.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EarlyAccessService {
|
||||||
|
private readonly logger = new Logger(EarlyAccessService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(EarlyAccessSignup)
|
||||||
|
private readonly repo: Repository<EarlyAccessSignup>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async register(dto: CreateEarlyAccessDto): Promise<{ success: true; alreadyRegistered: boolean }> {
|
||||||
|
const existing = await this.repo.findOne({ where: { email: dto.email } });
|
||||||
|
if (existing) {
|
||||||
|
// Duplicate email — return friendly success rather than a 409 that would break the UX
|
||||||
|
return { success: true, alreadyRegistered: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const signup = this.repo.create({
|
||||||
|
email: dto.email,
|
||||||
|
server_count: dto.server_count ?? 'not specified',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.repo.save(signup);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
// Guard against a race-condition duplicate (unique constraint violation)
|
||||||
|
const pg = err as { code?: string };
|
||||||
|
if (pg.code === '23505') {
|
||||||
|
return { success: true, alreadyRegistered: true };
|
||||||
|
}
|
||||||
|
this.logger.error('Failed to save early-access signup', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, alreadyRegistered: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.PHONY: all build build-linux build-windows clean test run
|
.PHONY: all build build-linux build-windows clean test run
|
||||||
|
|
||||||
# Binary names
|
# Binary names
|
||||||
BINARY_NAME=corrosion-companion
|
BINARY_NAME=corrosion-host-agent
|
||||||
BINARY_LINUX=$(BINARY_NAME)-linux-amd64
|
BINARY_LINUX=$(BINARY_NAME)-linux-amd64
|
||||||
BINARY_WINDOWS=$(BINARY_NAME)-windows-amd64.exe
|
BINARY_WINDOWS=$(BINARY_NAME)-windows-amd64.exe
|
||||||
|
|
||||||
@@ -66,10 +66,10 @@ run: build-local
|
|||||||
install-service:
|
install-service:
|
||||||
@echo "Installing systemd service..."
|
@echo "Installing systemd service..."
|
||||||
@sudo cp $(BUILD_DIR)/$(BINARY_LINUX) /usr/local/bin/$(BINARY_NAME)
|
@sudo cp $(BUILD_DIR)/$(BINARY_LINUX) /usr/local/bin/$(BINARY_NAME)
|
||||||
@sudo cp deployment/corrosion-companion.service /etc/systemd/system/
|
@sudo cp deployment/corrosion-host-agent.service /etc/systemd/system/
|
||||||
@sudo systemctl daemon-reload
|
@sudo systemctl daemon-reload
|
||||||
@sudo systemctl enable corrosion-companion
|
@sudo systemctl enable corrosion-host-agent
|
||||||
@echo "Service installed. Configure /etc/corrosion-companion/.env then start with: sudo systemctl start corrosion-companion"
|
@echo "Service installed. Configure /etc/corrosion-host-agent/.env then start with: sudo systemctl start corrosion-host-agent"
|
||||||
|
|
||||||
# Development helpers
|
# Development helpers
|
||||||
dev: build-local
|
dev: build-local
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-corrosion_dev}
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-corrosion_dev}
|
||||||
volumes:
|
volumes:
|
||||||
- pg_data:/var/lib/postgresql/data
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
# Auto-build the schema on a FRESH database. Postgres runs these ONLY when
|
||||||
|
# the data dir is empty (first boot or after a volume reset), so it never
|
||||||
|
# touches an existing volume — it just makes a fresh DB self-heal: the full
|
||||||
|
# schema is applied in order from the sqlx migrations (001..NNN), then the
|
||||||
|
# API's bootstrap seeds the admin. Rebuilds (with the volume kept) are a
|
||||||
|
# no-op here; the data persists. Only `down -v` / volume prune loses data.
|
||||||
|
- ../backend/migrations:/docker-entrypoint-initdb.d:ro
|
||||||
ports:
|
ports:
|
||||||
- "8101:5432"
|
- "8101:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
1
frontend/src/app-version.d.ts
vendored
Normal file
1
frontend/src/app-version.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare const __APP_VERSION__: string
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
Pencil, Save, ShoppingBag, Target, User,
|
Pencil, Save, ShoppingBag, Target, User,
|
||||||
// Marketing site additions
|
// Marketing site additions
|
||||||
Route, Timer, Megaphone, DatabaseBackup, Store, Undo2,
|
Route, Timer, Megaphone, DatabaseBackup, Store, Undo2,
|
||||||
|
Circle, Send, HelpCircle,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -63,6 +64,7 @@ const registry: Record<string, Component> = {
|
|||||||
// Marketing site additions
|
// Marketing site additions
|
||||||
route: Route, timer: Timer, megaphone: Megaphone,
|
route: Route, timer: Timer, megaphone: Megaphone,
|
||||||
'database-backup': DatabaseBackup, store: Store, 'undo-2': Undo2,
|
'database-backup': DatabaseBackup, store: Store, 'undo-2': Undo2,
|
||||||
|
circle: Circle, send: Send, 'help-circle': HelpCircle,
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmp = computed<Component | null>(() => registry[props.name] ?? null)
|
const cmp = computed<Component | null>(() => registry[props.name] ?? null)
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* DashboardLayout — game-aware app shell (Phase C redesign).
|
* DashboardLayout — game-aware app shell (Phase C redesign).
|
||||||
* Replaces the old Tailwind-only sidebar with the DS component set.
|
* Nav is driven by GAME_PROFILES[activeGame].nav — switching the GameSwitcher
|
||||||
* Preserves: navSections, permission gating, super-admin section, logout, RouterView.
|
* visibly changes nav items, labels, and sections per game.
|
||||||
* Adds: GameSwitcher, Logo, DS NavItem, agent-health footer, topbar w/ search + theme toggle.
|
* Preserves: permission gating, super-admin section, logout, mobile sidebar,
|
||||||
|
* GameSwitcher, agent-health footer, topbar.
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useServerStore } from '@/stores/server'
|
import { useServerStore } from '@/stores/server'
|
||||||
import { useThemeGame } from '@/composables/useThemeGame'
|
import { useThemeGame } from '@/composables/useThemeGame'
|
||||||
|
import { useGameProfile } from '@/config/gameProfiles'
|
||||||
|
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
|
||||||
|
import { safeDate } from '@/utils/formatters'
|
||||||
import Logo from '@/components/ds/brand/Logo.vue'
|
import Logo from '@/components/ds/brand/Logo.vue'
|
||||||
import Badge from '@/components/ds/core/Badge.vue'
|
import Badge from '@/components/ds/core/Badge.vue'
|
||||||
import StatusDot from '@/components/ds/core/StatusDot.vue'
|
import StatusDot from '@/components/ds/core/StatusDot.vue'
|
||||||
@@ -33,7 +37,7 @@ const sidebarOpen = ref(false)
|
|||||||
function closeSidebar() { sidebarOpen.value = false }
|
function closeSidebar() { sidebarOpen.value = false }
|
||||||
|
|
||||||
// ---- App version ----
|
// ---- App version ----
|
||||||
const APP_VERSION = '1.0.8'
|
const APP_VERSION = __APP_VERSION__
|
||||||
|
|
||||||
// ---- Game switcher ----
|
// ---- Game switcher ----
|
||||||
const GAME_OPTIONS: GameOption[] = [
|
const GAME_OPTIONS: GameOption[] = [
|
||||||
@@ -53,61 +57,15 @@ function onActiveGame(val: string) {
|
|||||||
setActiveGame(val as ActiveGame)
|
setActiveGame(val as ActiveGame)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Navigation ----
|
// ---- Navigation — driven by the game profile registry ----
|
||||||
type NavItemDef = { name: string; path: string; icon: string; permission: string | null }
|
/**
|
||||||
type NavSection = { label: string; items: NavItemDef[] }
|
* For 'all', fall back to rust (superset nav). For a specific game, look up
|
||||||
|
* its profile. noUncheckedIndexedAccess-safe: always ?? GAME_PROFILES.rust.
|
||||||
const navSections: NavSection[] = [
|
*/
|
||||||
{
|
const activeNavSections = computed<NavSection[]>(() => {
|
||||||
label: '',
|
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
|
||||||
items: [
|
return (useGameProfile(game)).nav
|
||||||
{ name: 'Dashboard', path: '/', icon: 'layout-dashboard', permission: null },
|
})
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Server',
|
|
||||||
items: [
|
|
||||||
{ name: 'Server', path: '/server', icon: 'server', permission: 'server.view' },
|
|
||||||
{ name: 'Console', path: '/console', icon: 'terminal', permission: 'console.view' },
|
|
||||||
{ name: 'Players', path: '/players', icon: 'users', permission: 'players.view' },
|
|
||||||
{ name: 'Plugins', path: '/plugins', icon: 'puzzle', permission: 'plugins.view' },
|
|
||||||
{ name: 'File manager', path: '/files', icon: 'folder-open', permission: 'files.view' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Plugin configs',
|
|
||||||
items: [
|
|
||||||
{ name: 'Plugin configs', path: '/plugin-configs', icon: 'puzzle', permission: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Operations',
|
|
||||||
items: [
|
|
||||||
{ name: 'Wipe manager', path: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
|
||||||
{ name: 'Maps', path: '/maps', icon: 'map', permission: 'maps.view' },
|
|
||||||
{ name: 'Schedules', path: '/schedules', icon: 'calendar-clock', permission: 'schedules.view' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Monitoring',
|
|
||||||
items: [
|
|
||||||
{ name: 'Chat log', path: '/chat', icon: 'message-square', permission: 'chat.view' },
|
|
||||||
{ name: 'Analytics', path: '/analytics', icon: 'bar-chart-3', permission: 'analytics.view' },
|
|
||||||
{ name: 'Alerts', path: '/alerts', icon: 'triangle-alert', permission: 'alerts.view' },
|
|
||||||
{ name: 'Notifications', path: '/notifications', icon: 'bell', permission: 'notifications.view' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Management',
|
|
||||||
items: [
|
|
||||||
{ name: 'Team', path: '/team', icon: 'users', permission: null },
|
|
||||||
{ name: 'Store', path: '/store/config', icon: 'shopping-cart', permission: 'store.view' },
|
|
||||||
{ name: 'Modules', path: '/modules', icon: 'layers', permission: 'modules.view' },
|
|
||||||
{ name: 'Changelog', path: '/changelog', icon: 'file-text', permission: 'changelog.view' },
|
|
||||||
{ name: 'Settings', path: '/settings', icon: 'settings', permission: 'settings.view' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const adminNavItems = [
|
const adminNavItems = [
|
||||||
{ name: 'Admin home', path: '/admin', icon: 'shield' },
|
{ name: 'Admin home', path: '/admin', icon: 'shield' },
|
||||||
@@ -137,6 +95,8 @@ function hasVisibleItems(section: NavSection): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Agent health ----
|
// ---- Agent health ----
|
||||||
|
const hasAgent = computed(() => server.connection !== null)
|
||||||
|
|
||||||
const agentTone = computed(() => {
|
const agentTone = computed(() => {
|
||||||
const cs = server.connection?.connection_status
|
const cs = server.connection?.connection_status
|
||||||
if (cs === 'connected') return 'online' as const
|
if (cs === 'connected') return 'online' as const
|
||||||
@@ -149,18 +109,23 @@ const agentLabel = computed(() => {
|
|||||||
if (cs === 'degraded') return 'Degraded'
|
if (cs === 'degraded') return 'Degraded'
|
||||||
return 'Offline'
|
return 'Offline'
|
||||||
})
|
})
|
||||||
const agentName = computed(() => {
|
const agentName = computed(() => server.connection?.server_ip ?? 'Host agent')
|
||||||
const ip = server.connection?.server_ip
|
|
||||||
return ip ?? 'asgard-01'
|
const agentMetaLine = computed(() => {
|
||||||
|
const cs = server.connection?.connection_status
|
||||||
|
let line = cs === 'connected' ? 'Connected' : server.connection?.companion_last_seen
|
||||||
|
? `Last seen ${safeDate(server.connection.companion_last_seen)}`
|
||||||
|
: 'Awaiting first heartbeat'
|
||||||
|
if (server.stats) {
|
||||||
|
line += ` · ${server.stats.player_count}/${server.stats.max_players} players`
|
||||||
|
}
|
||||||
|
return line
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---- Topbar ----
|
// ---- Topbar ----
|
||||||
const serverName = computed(() => auth.license?.server_name ?? 'Your servers')
|
const serverName = computed(() => auth.license?.server_name ?? 'Your servers')
|
||||||
const userName = computed(() => auth.user?.username ?? '')
|
const userName = computed(() => auth.user?.username ?? '')
|
||||||
const themeIcon = computed(() => theme.value === 'dark' ? 'sun' : 'moon')
|
const themeIcon = computed(() => theme.value === 'dark' ? 'sun' : 'moon')
|
||||||
|
|
||||||
// ---- Import computed from vue (missed above) ----
|
|
||||||
import { computed } from 'vue'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -197,20 +162,20 @@ import { computed } from 'vue'
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation — sections driven by GAME_PROFILES[activeGame].nav -->
|
||||||
<nav class="side__nav">
|
<nav class="side__nav">
|
||||||
<template v-for="section in navSections" :key="section.label">
|
<template v-for="section in activeNavSections" :key="section.label">
|
||||||
<template v-if="hasVisibleItems(section)">
|
<template v-if="hasVisibleItems(section)">
|
||||||
<div class="side__sec">
|
<div class="side__sec">
|
||||||
<div v-if="section.label" class="t-eyebrow side__lbl">{{ section.label }}</div>
|
<div v-if="section.label" class="t-eyebrow side__lbl">{{ section.label }}</div>
|
||||||
<NavItem
|
<NavItem
|
||||||
v-for="item in section.items"
|
v-for="item in section.items"
|
||||||
v-show="canShowNavItem(item)"
|
v-show="canShowNavItem(item)"
|
||||||
:key="item.path"
|
:key="item.route"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
:label="item.name"
|
:label="item.label"
|
||||||
:active="isActive(item.path)"
|
:active="isActive(item.route)"
|
||||||
@click="navigate(item.path)"
|
@click="navigate(item.route)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -230,18 +195,24 @@ import { computed } from 'vue'
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Agent health footer -->
|
<!-- Host agent footer -->
|
||||||
<div class="side__foot">
|
<div class="side__foot">
|
||||||
<div class="agent">
|
<!-- Connected: real IP + status badge + meta line -->
|
||||||
|
<div v-if="hasAgent" class="agent">
|
||||||
<div class="agent__row">
|
<div class="agent__row">
|
||||||
<StatusDot :tone="agentTone" :pulse="agentTone === 'online'" />
|
<StatusDot :tone="agentTone" :pulse="agentTone === 'online'" />
|
||||||
<span class="agent__name">{{ agentName }}</span>
|
<span class="agent__name">{{ agentName }}</span>
|
||||||
<Badge :tone="agentTone" size="md">{{ agentLabel }}</Badge>
|
<Badge :tone="agentTone" size="md">{{ agentLabel }}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent__meta">
|
<div class="agent__meta">{{ agentMetaLine }}</div>
|
||||||
Agent v{{ APP_VERSION }}
|
</div>
|
||||||
<template v-if="server.stats"> · {{ server.stats.player_count }}/{{ server.stats.max_players }} players</template>
|
<!-- Not connected: honest empty state -->
|
||||||
|
<div v-else class="agent agent--empty">
|
||||||
|
<div class="agent__row">
|
||||||
|
<StatusDot tone="offline" />
|
||||||
|
<span class="agent__name agent__name--muted">No host agent connected</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="agent__meta">Install the Corrosion host agent from the Server page</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- User / logout row -->
|
<!-- User / logout row -->
|
||||||
<div class="side__user">
|
<div class="side__user">
|
||||||
@@ -419,6 +390,13 @@ body { margin: 0; overflow: hidden; }
|
|||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent--empty { opacity: 0.7; }
|
||||||
|
|
||||||
|
.agent__name--muted {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.side__user {
|
.side__user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* gameProfiles.ts — Source of truth for per-game UI adaptation.
|
* gameProfiles.ts — Source of truth for per-game UI adaptation.
|
||||||
*
|
*
|
||||||
* Every game-specific label, terminology, Steam app ID, management model,
|
* Every game-specific label, terminology, Steam app ID, management model,
|
||||||
* and stat field list lives here. The dashboard, server cards, wipe manager,
|
* stat field list, AND sidebar nav lives here. The dashboard, server cards,
|
||||||
* and any future multi-game surface should key off this registry — never
|
* wipe manager, sidebar, and any future multi-game surface should key off this
|
||||||
* hard-code game-specific strings in components.
|
* registry — never hard-code game-specific strings in components.
|
||||||
*
|
*
|
||||||
* Backend status: the backend has NO game field on licenses yet. Today every
|
* Backend status: the backend has NO game field on licenses yet. Today every
|
||||||
* license is implicitly Rust. This registry is ready: when the backend adds a
|
* license is implicitly Rust. This registry is ready: when the backend adds a
|
||||||
@@ -15,6 +15,26 @@
|
|||||||
* GAME_PROFILES. Nothing else changes.
|
* GAME_PROFILES. Nothing else changes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Nav structure — drives the per-game sidebar
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** A single sidebar nav item. route must be an existing panel route path. */
|
||||||
|
export interface NavItemDef {
|
||||||
|
label: string
|
||||||
|
route: string
|
||||||
|
icon: string
|
||||||
|
/** Permission key required to show this item (e.g. 'plugins.view'). Null = always visible. */
|
||||||
|
permission: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A labelled section grouping nav items in the sidebar. */
|
||||||
|
export interface NavSection {
|
||||||
|
/** Section heading (eyebrow text). Empty string = no heading. */
|
||||||
|
label: string
|
||||||
|
items: NavItemDef[]
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Union types — exhaustive, never widen to string
|
// Union types — exhaustive, never widen to string
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -87,12 +107,67 @@ export interface GameProfile {
|
|||||||
* First entry is always Players; subsequent entries are game-specific.
|
* First entry is always Players; subsequent entries are game-specific.
|
||||||
*/
|
*/
|
||||||
statFields: [string, string, string]
|
statFields: [string, string, string]
|
||||||
|
/**
|
||||||
|
* Per-game sidebar navigation. Ordered list of sections, each with items.
|
||||||
|
* Items MUST use only existing panel routes (see router/index.ts).
|
||||||
|
* The sidebar renders exactly these sections for the active game.
|
||||||
|
*/
|
||||||
|
nav: NavSection[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Registry
|
// Registry
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared nav building blocks — reused across game nav definitions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const NAV_DASHBOARD: NavItemDef = { label: 'Dashboard', route: '/', icon: 'layout-dashboard', permission: null }
|
||||||
|
const NAV_SERVER: NavItemDef = { label: 'Server', route: '/server', icon: 'server', permission: 'server.view' }
|
||||||
|
const NAV_CONSOLE: NavItemDef = { label: 'Console', route: '/console', icon: 'terminal', permission: 'console.view' }
|
||||||
|
const NAV_PLAYERS: NavItemDef = { label: 'Players', route: '/players', icon: 'users', permission: 'players.view' }
|
||||||
|
const NAV_PLUGINS: NavItemDef = { label: 'Plugins (uMod)', route: '/plugins', icon: 'puzzle', permission: 'plugins.view' }
|
||||||
|
const NAV_FILES: NavItemDef = { label: 'File manager', route: '/files', icon: 'folder-open', permission: 'files.view' }
|
||||||
|
const NAV_PLUGIN_CONFIGS: NavItemDef = { label: 'Plugin configs', route: '/plugin-configs', icon: 'sliders', permission: null }
|
||||||
|
const NAV_SCHEDULES: NavItemDef = { label: 'Schedules', route: '/schedules', icon: 'calendar-clock', permission: 'schedules.view' }
|
||||||
|
const NAV_CHAT: NavItemDef = { label: 'Chat log', route: '/chat', icon: 'message-square', permission: 'chat.view' }
|
||||||
|
const NAV_ANALYTICS: NavItemDef = { label: 'Analytics', route: '/analytics', icon: 'bar-chart-3', permission: 'analytics.view' }
|
||||||
|
const NAV_ALERTS: NavItemDef = { label: 'Alerts', route: '/alerts', icon: 'triangle-alert', permission: 'alerts.view' }
|
||||||
|
const NAV_NOTIFICATIONS: NavItemDef = { label: 'Notifications', route: '/notifications', icon: 'bell', permission: 'notifications.view' }
|
||||||
|
const NAV_TEAM: NavItemDef = { label: 'Team', route: '/team', icon: 'users', permission: null }
|
||||||
|
const NAV_STORE: NavItemDef = { label: 'Store', route: '/store/config', icon: 'shopping-cart', permission: 'store.view' }
|
||||||
|
const NAV_MODULES: NavItemDef = { label: 'Modules', route: '/modules', icon: 'layers', permission: 'modules.view' }
|
||||||
|
const NAV_CHANGELOG: NavItemDef = { label: 'Changelog', route: '/changelog', icon: 'file-text', permission: 'changelog.view' }
|
||||||
|
const NAV_SETTINGS: NavItemDef = { label: 'Settings', route: '/settings', icon: 'settings', permission: 'settings.view' }
|
||||||
|
const NAV_MAPS: NavItemDef = { label: 'Maps', route: '/maps', icon: 'map', permission: 'maps.view' }
|
||||||
|
|
||||||
|
/** Full Rust / 'all' nav — superset used as fallback. */
|
||||||
|
const RUST_NAV: NavSection[] = [
|
||||||
|
{ label: '', items: [NAV_DASHBOARD] },
|
||||||
|
{
|
||||||
|
label: 'Server',
|
||||||
|
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_PLUGINS, NAV_FILES],
|
||||||
|
},
|
||||||
|
{ label: 'Plugin configs', items: [NAV_PLUGIN_CONFIGS] },
|
||||||
|
{
|
||||||
|
label: 'Operations',
|
||||||
|
items: [
|
||||||
|
{ label: 'Wipe', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
||||||
|
NAV_MAPS,
|
||||||
|
NAV_SCHEDULES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monitoring',
|
||||||
|
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS, NAV_NOTIFICATIONS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Management',
|
||||||
|
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
||||||
rust: {
|
rust: {
|
||||||
label: 'Rust',
|
label: 'Rust',
|
||||||
@@ -109,6 +184,7 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
|||||||
group: 'Team',
|
group: 'Team',
|
||||||
},
|
},
|
||||||
statFields: ['Players', 'uMod', 'Wipe'],
|
statFields: ['Players', 'uMod', 'Wipe'],
|
||||||
|
nav: RUST_NAV,
|
||||||
},
|
},
|
||||||
|
|
||||||
conan: {
|
conan: {
|
||||||
@@ -130,6 +206,30 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
|||||||
},
|
},
|
||||||
special: ['Clans', 'Thralls', 'Avatars', 'Purge', 'PvP windows'],
|
special: ['Clans', 'Thralls', 'Avatars', 'Purge', 'PvP windows'],
|
||||||
statFields: ['Players', 'Clans', 'Purge'],
|
statFields: ['Players', 'Clans', 'Purge'],
|
||||||
|
nav: [
|
||||||
|
{ label: '', items: [NAV_DASHBOARD] },
|
||||||
|
{
|
||||||
|
label: 'Server',
|
||||||
|
// Conan: no uMod/Oxide; has RCON console, maps, players, files
|
||||||
|
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_FILES],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Operations',
|
||||||
|
items: [
|
||||||
|
{ label: 'Wipe World', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
||||||
|
NAV_MAPS,
|
||||||
|
NAV_SCHEDULES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monitoring',
|
||||||
|
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS, NAV_NOTIFICATIONS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Management',
|
||||||
|
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
soulmask: {
|
soulmask: {
|
||||||
@@ -151,6 +251,29 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
|||||||
},
|
},
|
||||||
special: ['Cluster', 'Tribes'],
|
special: ['Cluster', 'Tribes'],
|
||||||
statFields: ['Players', 'Tribe', 'Mask'],
|
statFields: ['Players', 'Tribe', 'Mask'],
|
||||||
|
nav: [
|
||||||
|
{ label: '', items: [NAV_DASHBOARD] },
|
||||||
|
{
|
||||||
|
label: 'Server',
|
||||||
|
// Soulmask: no uMod/Oxide; has RCON+GM console, players, files
|
||||||
|
items: [NAV_SERVER, NAV_CONSOLE, NAV_PLAYERS, NAV_FILES],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Operations',
|
||||||
|
items: [
|
||||||
|
{ label: 'World Reset', route: '/wipes', icon: 'trash-2', permission: 'wipes.view' },
|
||||||
|
NAV_SCHEDULES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monitoring',
|
||||||
|
items: [NAV_CHAT, NAV_ANALYTICS, NAV_ALERTS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Management',
|
||||||
|
items: [NAV_TEAM, NAV_STORE, NAV_MODULES, NAV_CHANGELOG, NAV_SETTINGS],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
dune: {
|
dune: {
|
||||||
@@ -170,6 +293,34 @@ export const GAME_PROFILES: Record<GameId, GameProfile> = {
|
|||||||
},
|
},
|
||||||
special: ['Sietches', 'Deep Desert', 'Bases', 'Landsraad'],
|
special: ['Sietches', 'Deep Desert', 'Bases', 'Landsraad'],
|
||||||
statFields: ['Players', 'Sietches', 'Control'],
|
statFields: ['Players', 'Sietches', 'Control'],
|
||||||
|
nav: [
|
||||||
|
{ label: '', items: [NAV_DASHBOARD] },
|
||||||
|
{
|
||||||
|
label: 'Server',
|
||||||
|
// Dune: no RCON (uses RabbitMQ); label console "Broadcast"; no maps route; no plugins
|
||||||
|
items: [
|
||||||
|
NAV_SERVER,
|
||||||
|
{ label: 'Broadcast', route: '/console', icon: 'radio', permission: 'console.view' },
|
||||||
|
NAV_PLAYERS,
|
||||||
|
NAV_FILES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Operations',
|
||||||
|
items: [
|
||||||
|
{ label: 'Deep Desert', route: '/wipes', icon: 'wind', permission: 'wipes.view' },
|
||||||
|
NAV_SCHEDULES,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monitoring',
|
||||||
|
items: [NAV_ANALYTICS, NAV_ALERTS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Management',
|
||||||
|
items: [NAV_TEAM, NAV_STORE, NAV_CHANGELOG, NAV_SETTINGS],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { useWipeStore } from '@/stores/wipe'
|
|||||||
import { useApi } from '@/composables/useApi'
|
import { useApi } from '@/composables/useApi'
|
||||||
import { useWebSocket, type WebSocketMessage } from '@/composables/useWebSocket'
|
import { useWebSocket, type WebSocketMessage } from '@/composables/useWebSocket'
|
||||||
import { useGameProfile } from '@/config/gameProfiles'
|
import { useGameProfile } from '@/config/gameProfiles'
|
||||||
|
import { useThemeGame } from '@/composables/useThemeGame'
|
||||||
import Panel from '@/components/ds/data/Panel.vue'
|
import Panel from '@/components/ds/data/Panel.vue'
|
||||||
import StatCard from '@/components/ds/data/StatCard.vue'
|
import StatCard from '@/components/ds/data/StatCard.vue'
|
||||||
import ConsoleLineDS from '@/components/ds/data/ConsoleLine.vue'
|
import ConsoleLineDS from '@/components/ds/data/ConsoleLine.vue'
|
||||||
@@ -44,10 +45,14 @@ const server = useServerStore()
|
|||||||
const wipeStore = useWipeStore()
|
const wipeStore = useWipeStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
const { activeGame } = useThemeGame()
|
||||||
|
|
||||||
// Today every license is Rust. When the backend adds a `game` field to the
|
// Profile follows the GameSwitcher selection. 'all' falls back to rust (neutral house skin).
|
||||||
// license or server_config, pass it here: useGameProfile(server.config?.game ?? 'rust')
|
// When the backend adds a `game` field on licenses, swap activeGame for server.config?.game.
|
||||||
const profile = computed(() => useGameProfile('rust'))
|
const profile = computed(() => {
|
||||||
|
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
|
||||||
|
return useGameProfile(game)
|
||||||
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Derived server state — all real, no fallbacks to fabricated values
|
// Derived server state — all real, no fallbacks to fabricated values
|
||||||
@@ -254,7 +259,7 @@ function navServer() { router.push('/server') }
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
icon="server"
|
icon="server"
|
||||||
title="No server connected"
|
title="No server connected"
|
||||||
description="Install the companion agent on your host machine to begin managing your server from Corrosion."
|
description="Install the Corrosion host agent on your host machine to begin managing your server from Corrosion."
|
||||||
>
|
>
|
||||||
<template #action>
|
<template #action>
|
||||||
<Button icon="server" @click="navServer">Set up server</Button>
|
<Button icon="server" @click="navServer">Set up server</Button>
|
||||||
@@ -298,7 +303,7 @@ function navServer() { router.push('/server') }
|
|||||||
<div class="dash__kpis">
|
<div class="dash__kpis">
|
||||||
<StatCard
|
<StatCard
|
||||||
icon="users"
|
icon="users"
|
||||||
:label="profile.statFields[0] + ' online'"
|
:label="(profile.statFields[0] ?? 'Players') + ' online'"
|
||||||
:value="soloPlayers !== null ? String(soloPlayers) : '—'"
|
:value="soloPlayers !== null ? String(soloPlayers) : '—'"
|
||||||
:unit="soloMaxPlayers !== null ? '/' + soloMaxPlayers : ''"
|
:unit="soloMaxPlayers !== null ? '/' + soloMaxPlayers : ''"
|
||||||
note="live via agent"
|
note="live via agent"
|
||||||
@@ -399,7 +404,7 @@ function navServer() { router.push('/server') }
|
|||||||
<div class="dash__col dash__col--side">
|
<div class="dash__col dash__col--side">
|
||||||
|
|
||||||
<!-- Resources — real stats from agent; null = '—' -->
|
<!-- Resources — real stats from agent; null = '—' -->
|
||||||
<Panel title="Resources" subtitle="Companion agent telemetry">
|
<Panel title="Resources" subtitle="Host agent telemetry">
|
||||||
<div class="solo-meters">
|
<div class="solo-meters">
|
||||||
<ResourceMeter
|
<ResourceMeter
|
||||||
label="CPU"
|
label="CPU"
|
||||||
@@ -413,15 +418,15 @@ function navServer() { router.push('/server') }
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="soloCpu === null && soloRamMb === null" class="meters-note">
|
<div v-if="soloCpu === null && soloRamMb === null" class="meters-note">
|
||||||
Resource metrics arrive via the companion agent heartbeat.
|
Resource metrics arrive via the host agent heartbeat.
|
||||||
<Button size="sm" variant="ghost" icon="server" class="meters-cta" @click="navServer">
|
<Button size="sm" variant="ghost" icon="server" class="meters-cta" @click="navServer">
|
||||||
Agent setup
|
Agent setup
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- Next wipe — real schedule from wipeStore -->
|
<!-- Next wipe/reset — title follows game terminology -->
|
||||||
<Panel title="Next wipe">
|
<Panel :title="'Next ' + profile.terminology.reset.toLowerCase()">
|
||||||
<div v-if="nextWipe" class="solo-wipe">
|
<div v-if="nextWipe" class="solo-wipe">
|
||||||
<div>
|
<div>
|
||||||
<div class="solo-wipe__type">{{ nextWipeType }}</div>
|
<div class="solo-wipe__type">{{ nextWipeType }}</div>
|
||||||
@@ -433,8 +438,8 @@ function navServer() { router.push('/server') }
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
v-else
|
v-else
|
||||||
icon="calendar"
|
icon="calendar"
|
||||||
title="No wipe scheduled"
|
:title="'No ' + profile.terminology.reset.toLowerCase() + ' scheduled'"
|
||||||
description="Configure automatic wipes in the wipe manager."
|
:description="'Configure automatic ' + profile.terminology.reset.toLowerCase() + 's in the wipe manager.'"
|
||||||
>
|
>
|
||||||
<template #action>
|
<template #action>
|
||||||
<Button size="sm" variant="outline" icon="calendar-clock" @click="navWipes">
|
<Button size="sm" variant="outline" icon="calendar-clock" @click="navWipes">
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ onMounted(() => {
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Alert tone="info">
|
<Alert tone="info">
|
||||||
The plugin will be registered in your plugin list immediately. Your companion agent must be connected
|
The plugin will be registered in your plugin list immediately. Your host agent must be connected
|
||||||
for the file to be delivered to the game server. If the agent is offline, re-upload once it reconnects.
|
for the file to be delivered to the game server. If the agent is offline, re-upload once it reconnects.
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { useServerStore } from '@/stores/server'
|
import { useServerStore } from '@/stores/server'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { useThemeGame } from '@/composables/useThemeGame'
|
||||||
|
import { useGameProfile } from '@/config/gameProfiles'
|
||||||
import type { DeploymentConfig, DeploymentStatus } from '@/types'
|
import type { DeploymentConfig, DeploymentStatus } from '@/types'
|
||||||
import { useWebSocket } from '@/composables/useWebSocket'
|
import { useWebSocket } from '@/composables/useWebSocket'
|
||||||
import Panel from '@/components/ds/data/Panel.vue'
|
import Panel from '@/components/ds/data/Panel.vue'
|
||||||
@@ -11,6 +13,7 @@ import Badge from '@/components/ds/core/Badge.vue'
|
|||||||
import StatusDot from '@/components/ds/core/StatusDot.vue'
|
import StatusDot from '@/components/ds/core/StatusDot.vue'
|
||||||
import Icon from '@/components/ds/core/Icon.vue'
|
import Icon from '@/components/ds/core/Icon.vue'
|
||||||
import Alert from '@/components/ds/feedback/Alert.vue'
|
import Alert from '@/components/ds/feedback/Alert.vue'
|
||||||
|
import EmptyState from '@/components/ds/feedback/EmptyState.vue'
|
||||||
import Input from '@/components/ds/forms/Input.vue'
|
import Input from '@/components/ds/forms/Input.vue'
|
||||||
import Switch from '@/components/ds/forms/Switch.vue'
|
import Switch from '@/components/ds/forms/Switch.vue'
|
||||||
import Tabs from '@/components/ds/navigation/Tabs.vue'
|
import Tabs from '@/components/ds/navigation/Tabs.vue'
|
||||||
@@ -18,6 +21,39 @@ import Tabs from '@/components/ds/navigation/Tabs.vue'
|
|||||||
const server = useServerStore()
|
const server = useServerStore()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const toast = useToastStore()
|
const toast = useToastStore()
|
||||||
|
const { activeGame } = useThemeGame()
|
||||||
|
|
||||||
|
// Profile follows the GameSwitcher. 'all' defaults to rust (neutral house skin).
|
||||||
|
const profile = computed(() => {
|
||||||
|
const game = activeGame.value === 'all' ? 'rust' : activeGame.value
|
||||||
|
return useGameProfile(game)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Game-specific derived flags
|
||||||
|
const isRust = computed(() => profile.value.mods === 'umod')
|
||||||
|
const hasPluginSystem = computed(() => profile.value.mods === 'umod')
|
||||||
|
const isDockerManaged = computed(() => profile.value.managementModel === 'docker-compose')
|
||||||
|
|
||||||
|
// Management model human label for the identity badge
|
||||||
|
const managementModelLabel = computed(() => {
|
||||||
|
const m = profile.value.managementModel
|
||||||
|
const c = profile.value.console
|
||||||
|
if (m === 'docker-compose') {
|
||||||
|
return profile.value.clustering === 'battlegroup' ? 'Docker · BattleGroup' : 'Docker · Compose'
|
||||||
|
}
|
||||||
|
if (c === 'rcon+ingame') return 'Process · RCON + In-game'
|
||||||
|
if (c === 'rcon+gm') return 'Process · RCON + GM'
|
||||||
|
return 'Process · RCON'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clustering section label per game
|
||||||
|
const clusterLabel = computed(() => {
|
||||||
|
const cl = profile.value.clustering
|
||||||
|
if (cl === 'battlegroup') return 'BattleGroups & Sietches'
|
||||||
|
if (cl === 'main-client') return 'Cluster'
|
||||||
|
if (cl === 'character-transfer') return 'Clans & Character Transfer'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const editMode = ref(false)
|
const editMode = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
@@ -64,22 +100,22 @@ const agentLastSeenLabel = computed(() => {
|
|||||||
const licenseKey = computed(() => auth.license?.license_key || 'YOUR-LICENSE-KEY')
|
const licenseKey = computed(() => auth.license?.license_key || 'YOUR-LICENSE-KEY')
|
||||||
|
|
||||||
const linuxCommands = computed(() => `# Download the agent
|
const linuxCommands = computed(() => `# Download the agent
|
||||||
curl -LO https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-linux-amd64
|
curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64
|
||||||
chmod +x corrosion-companion-linux-amd64
|
chmod +x corrosion-host-agent-linux-amd64
|
||||||
|
|
||||||
# Start with your license key
|
# Start with your license key
|
||||||
export LICENSE_ID="${licenseKey.value}"
|
export LICENSE_ID="${licenseKey.value}"
|
||||||
export NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
export NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
||||||
./corrosion-companion-linux-amd64`)
|
./corrosion-host-agent-linux-amd64`)
|
||||||
|
|
||||||
const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt)
|
const windowsCommands = computed(() => `# Requires PowerShell (not Command Prompt)
|
||||||
# Download the agent
|
# Download the agent
|
||||||
Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-windows-amd64.exe" -OutFile "corrosion-companion-windows-amd64.exe"
|
Invoke-WebRequest -Uri "https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe" -OutFile "corrosion-host-agent-windows-amd64.exe"
|
||||||
|
|
||||||
# Start with your license key
|
# Start with your license key
|
||||||
$env:LICENSE_ID="${licenseKey.value}"
|
$env:LICENSE_ID="${licenseKey.value}"
|
||||||
$env:NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
$env:NATS_URL="nats://nats.corrosionmgmt.com:4222"
|
||||||
.\\corrosion-companion-windows-amd64.exe`)
|
.\\corrosion-host-agent-windows-amd64.exe`)
|
||||||
|
|
||||||
async function copySetupCommands() {
|
async function copySetupCommands() {
|
||||||
try {
|
try {
|
||||||
@@ -278,17 +314,18 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sv">
|
<div class="sv">
|
||||||
<!-- Page head -->
|
<!-- Page head — game-aware identity -->
|
||||||
<div class="sv__head">
|
<div class="sv__head">
|
||||||
<div class="sv__head-id">
|
<div class="sv__head-id">
|
||||||
<div class="sv__head-chip">
|
<div class="sv__head-chip">
|
||||||
<Icon name="server" :size="20" :stroke-width="2" />
|
<Icon name="server" :size="20" :stroke-width="2" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="t-eyebrow">Server management</div>
|
<div class="t-eyebrow">{{ profile.label }} · Server management</div>
|
||||||
<h1 class="sv__title">Server</h1>
|
<h1 class="sv__title">Server</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Badge tone="neutral" :mono="true" class="sv__model-badge">{{ managementModelLabel }}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection -->
|
<!-- Connection -->
|
||||||
@@ -350,8 +387,8 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- Companion agent -->
|
<!-- Host agent -->
|
||||||
<Panel title="Companion agent" subtitle="Bare-metal server management binary">
|
<Panel title="Host agent" subtitle="Bare-metal server management binary">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<Badge :tone="isAgentConnected ? 'online' : 'offline'" :dot="true" :pulse="isAgentConnected">
|
<Badge :tone="isAgentConnected ? 'online' : 'offline'" :dot="true" :pulse="isAgentConnected">
|
||||||
{{ isAgentConnected ? 'Active' : 'Inactive' }}
|
{{ isAgentConnected ? 'Active' : 'Inactive' }}
|
||||||
@@ -380,20 +417,20 @@ onMounted(async () => {
|
|||||||
<!-- Download -->
|
<!-- Download -->
|
||||||
<div class="sv__section-head">
|
<div class="sv__section-head">
|
||||||
<Icon name="download" :size="14" />
|
<Icon name="download" :size="14" />
|
||||||
<span>Download companion agent</span>
|
<span>Download host agent</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sv__downloads sv__mb">
|
<div class="sv__downloads sv__mb">
|
||||||
<a
|
<a
|
||||||
href="https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-linux-amd64"
|
href="https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64"
|
||||||
download="corrosion-companion-linux-amd64"
|
download="corrosion-host-agent-linux-amd64"
|
||||||
class="sv__dl-link"
|
class="sv__dl-link"
|
||||||
>
|
>
|
||||||
<Icon name="download" :size="15" />
|
<Icon name="download" :size="15" />
|
||||||
Linux (amd64)
|
Linux (amd64)
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-windows-amd64.exe"
|
href="https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe"
|
||||||
download="corrosion-companion-windows-amd64.exe"
|
download="corrosion-host-agent-windows-amd64.exe"
|
||||||
class="sv__dl-link"
|
class="sv__dl-link"
|
||||||
>
|
>
|
||||||
<Icon name="download" :size="15" />
|
<Icon name="download" :size="15" />
|
||||||
@@ -424,28 +461,28 @@ onMounted(async () => {
|
|||||||
<!-- Linux commands -->
|
<!-- Linux commands -->
|
||||||
<div v-if="setupTab === 'linux'" class="sv__codeblock">
|
<div v-if="setupTab === 'linux'" class="sv__codeblock">
|
||||||
<p class="sv__cmt"># Download the agent</p>
|
<p class="sv__cmt"># Download the agent</p>
|
||||||
<p>curl -LO https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-linux-amd64</p>
|
<p>curl -LO https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-linux-amd64</p>
|
||||||
<p>chmod +x corrosion-companion-linux-amd64</p>
|
<p>chmod +x corrosion-host-agent-linux-amd64</p>
|
||||||
<p class="sv__cmt sv__mt"># Start with your license key</p>
|
<p class="sv__cmt sv__mt"># Start with your license key</p>
|
||||||
<p>export LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p>
|
<p>export LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p>
|
||||||
<p>export NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
<p>export NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
||||||
<p>./corrosion-companion-linux-amd64</p>
|
<p>./corrosion-host-agent-linux-amd64</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Windows commands -->
|
<!-- Windows commands -->
|
||||||
<div v-if="setupTab === 'windows'" class="sv__codeblock">
|
<div v-if="setupTab === 'windows'" class="sv__codeblock">
|
||||||
<p class="sv__cmt"># Requires PowerShell (not Command Prompt)</p>
|
<p class="sv__cmt"># Requires PowerShell (not Command Prompt)</p>
|
||||||
<p class="sv__cmt"># Download the agent</p>
|
<p class="sv__cmt"># Download the agent</p>
|
||||||
<p>Invoke-WebRequest -Uri <span class="sv__accent">"https://cdn.corrosionmgmt.com/companion/latest/corrosion-companion-windows-amd64.exe"</span> -OutFile <span class="sv__accent">"corrosion-companion-windows-amd64.exe"</span></p>
|
<p>Invoke-WebRequest -Uri <span class="sv__accent">"https://cdn.corrosionmgmt.com/host-agent/latest/corrosion-host-agent-windows-amd64.exe"</span> -OutFile <span class="sv__accent">"corrosion-host-agent-windows-amd64.exe"</span></p>
|
||||||
<p class="sv__cmt sv__mt"># Start with your license key</p>
|
<p class="sv__cmt sv__mt"># Start with your license key</p>
|
||||||
<p>$env:LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p>
|
<p>$env:LICENSE_ID=<span class="sv__accent">"{{ licenseKey }}"</span></p>
|
||||||
<p>$env:NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
<p>$env:NATS_URL=<span class="sv__accent">"nats://nats.corrosionmgmt.com:4222"</span></p>
|
||||||
<p>.\corrosion-companion-windows-amd64.exe</p>
|
<p>.\corrosion-host-agent-windows-amd64.exe</p>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- Deploy Rust Server -->
|
<!-- Deploy Server — Rust only (SteamCMD path). Other games use docker-compose or external tooling. -->
|
||||||
<Panel title="Deploy Rust server" subtitle="One-click: SteamCMD, download, configure, start">
|
<Panel v-if="isRust" title="Deploy Rust server" subtitle="One-click: SteamCMD, download, configure, start">
|
||||||
<template #title-append>
|
<template #title-append>
|
||||||
<Icon name="rocket" :size="15" />
|
<Icon name="rocket" :size="15" />
|
||||||
</template>
|
</template>
|
||||||
@@ -560,8 +597,28 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<!-- Install Oxide / uMod -->
|
<!-- Non-Rust: Docker-managed server note -->
|
||||||
<Panel title="Install Oxide / uMod" subtitle="Required for all plugins including CorrosionCompanion">
|
<Panel
|
||||||
|
v-if="isDockerManaged"
|
||||||
|
:title="profile.label + ' server deployment'"
|
||||||
|
subtitle="Managed via Docker Compose"
|
||||||
|
>
|
||||||
|
<template #title-append>
|
||||||
|
<Icon name="box" :size="15" />
|
||||||
|
</template>
|
||||||
|
<EmptyState
|
||||||
|
icon="box"
|
||||||
|
title="Docker-managed deployment"
|
||||||
|
:description="profile.label + ' servers are managed via Docker Compose. Connect the host agent on your Docker host to enable lifecycle management.'"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<Badge tone="info">Docker · Compose</Badge>
|
||||||
|
</template>
|
||||||
|
</EmptyState>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Install Oxide / uMod — Rust only -->
|
||||||
|
<Panel v-if="hasPluginSystem" title="Install Oxide / uMod" subtitle="Required for all plugins including CorrosionCompanion">
|
||||||
<template #title-append>
|
<template #title-append>
|
||||||
<Icon name="puzzle" :size="15" />
|
<Icon name="puzzle" :size="15" />
|
||||||
</template>
|
</template>
|
||||||
@@ -611,6 +668,79 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Workshop Mods info — Conan / Soulmask (Steam Workshop, no install step needed) -->
|
||||||
|
<Panel
|
||||||
|
v-else-if="profile.mods === 'workshop'"
|
||||||
|
:title="(profile.terminology.mods ?? 'Workshop Mods')"
|
||||||
|
:subtitle="profile.label + ' uses Steam Workshop — no manual install step required'"
|
||||||
|
>
|
||||||
|
<template #title-append>
|
||||||
|
<Icon name="layers" :size="15" />
|
||||||
|
</template>
|
||||||
|
<EmptyState
|
||||||
|
icon="layers"
|
||||||
|
:title="profile.label + ' mod management'"
|
||||||
|
:description="profile.label + ' loads mods directly from Steam Workshop. Manage your mod list in server config — no Corrosion install step needed.'"
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Conan Exiles special concepts (Clans / Thralls / Purge) -->
|
||||||
|
<Panel
|
||||||
|
v-if="profile.accent === 'conan'"
|
||||||
|
title="Conan Exiles concepts"
|
||||||
|
subtitle="Key admin mechanics for Conan Exiles servers"
|
||||||
|
>
|
||||||
|
<div class="sv__concept-grid">
|
||||||
|
<div class="sv__concept">
|
||||||
|
<Icon name="users" :size="16" />
|
||||||
|
<div>
|
||||||
|
<div class="sv__concept-label">Clans</div>
|
||||||
|
<div class="sv__concept-desc">Player factions. Clan management via in-game admin panel or RCON.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sv__concept">
|
||||||
|
<Icon name="zap" :size="16" />
|
||||||
|
<div>
|
||||||
|
<div class="sv__concept-label">Thralls & Avatars</div>
|
||||||
|
<div class="sv__concept-desc">Server-controlled NPCs and deity summons. Purge cycle managed via server settings.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sv__concept">
|
||||||
|
<Icon name="shield" :size="16" />
|
||||||
|
<div>
|
||||||
|
<div class="sv__concept-label">Purge</div>
|
||||||
|
<div class="sv__concept-desc">NPC raid events targeting player bases. Enable / tune via server config.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Soulmask clustering section -->
|
||||||
|
<Panel
|
||||||
|
v-if="profile.clustering === 'main-client'"
|
||||||
|
:title="clusterLabel"
|
||||||
|
subtitle="Main-client cluster topology for Soulmask"
|
||||||
|
>
|
||||||
|
<EmptyState
|
||||||
|
icon="network"
|
||||||
|
title="Cluster management coming soon"
|
||||||
|
:description="'Connect a ' + profile.label + ' host to manage the main-client cluster from this panel. Cluster configuration requires the host agent.'"
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- Dune BattleGroup / Sietches section -->
|
||||||
|
<Panel
|
||||||
|
v-if="profile.clustering === 'battlegroup'"
|
||||||
|
title="BattleGroups & Sietches"
|
||||||
|
subtitle="Dune: Awakening server cluster topology"
|
||||||
|
>
|
||||||
|
<EmptyState
|
||||||
|
icon="map"
|
||||||
|
title="Sietch management requires a connected Dune host"
|
||||||
|
description="Connect the host agent on your Dune: Awakening Docker host to manage BattleGroups and Sietches from this panel."
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<!-- Configuration -->
|
<!-- Configuration -->
|
||||||
<Panel title="Configuration">
|
<Panel title="Configuration">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -708,8 +838,13 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="sv__toggle-row">
|
<div class="sv__toggle-row">
|
||||||
<div class="sv__toggle-body">
|
<div class="sv__toggle-body">
|
||||||
<div class="sv__toggle-label">Auto-update on force wipe</div>
|
<div class="sv__toggle-label">
|
||||||
<div class="sv__toggle-sub">Update when Facepunch pushes</div>
|
<!-- Rust: "force wipe" is a Facepunch concept. Others: plain "auto-update" -->
|
||||||
|
{{ isRust ? 'Auto-update on force wipe' : 'Auto-update on patch' }}
|
||||||
|
</div>
|
||||||
|
<div class="sv__toggle-sub">
|
||||||
|
{{ isRust ? 'Update when Facepunch pushes' : 'Update when the developer pushes a patch' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
:model-value="server.config?.auto_update_on_force_wipe ?? false"
|
:model-value="server.config?.auto_update_on_force_wipe ?? false"
|
||||||
@@ -717,7 +852,8 @@ onMounted(async () => {
|
|||||||
@update:model-value="toggleAutomation('auto_update_on_force_wipe')"
|
@update:model-value="toggleAutomation('auto_update_on_force_wipe')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="sv__toggle-row">
|
<!-- Rust-only: force wipe eligibility is a Facepunch concept -->
|
||||||
|
<div v-if="isRust" class="sv__toggle-row">
|
||||||
<div class="sv__toggle-body">
|
<div class="sv__toggle-body">
|
||||||
<div class="sv__toggle-label">Force wipe eligible</div>
|
<div class="sv__toggle-label">Force wipe eligible</div>
|
||||||
<div class="sv__toggle-sub">Server participates in force wipes</div>
|
<div class="sv__toggle-sub">Server participates in force wipes</div>
|
||||||
@@ -848,4 +984,19 @@ onMounted(async () => {
|
|||||||
.sv__toggle-row:first-child { padding-top: 0; }
|
.sv__toggle-row:first-child { padding-top: 0; }
|
||||||
.sv__toggle-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
|
.sv__toggle-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
|
||||||
.sv__toggle-sub { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; }
|
.sv__toggle-sub { font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 2px; }
|
||||||
|
|
||||||
|
/* Management model badge in page head */
|
||||||
|
.sv__model-badge { align-self: center; }
|
||||||
|
|
||||||
|
/* Game concept cards (Conan Exiles special features) */
|
||||||
|
.sv__concept-grid { display: flex; flex-direction: column; gap: 14px; }
|
||||||
|
.sv__concept {
|
||||||
|
display: flex; align-items: flex-start; gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: var(--surface-raised); border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.sv__concept-label { font-size: var(--text-sm); font-weight: 600; color: var(--text-primary); margin-bottom: 2px; }
|
||||||
|
.sv__concept-desc { font-size: var(--text-xs); color: var(--text-tertiary); line-height: 1.5; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function syncPorts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connectionTypes = [
|
const connectionTypes = [
|
||||||
{ value: 'bare_metal', label: 'Bare metal / VPS', desc: 'Direct connection via Companion Agent' },
|
{ value: 'bare_metal', label: 'Bare metal / VPS', desc: 'Direct connection via Corrosion host agent' },
|
||||||
{ value: 'amp', label: 'AMP (CubeCoders)', desc: 'Connect through AMP panel API' },
|
{ value: 'amp', label: 'AMP (CubeCoders)', desc: 'Connect through AMP panel API' },
|
||||||
{ value: 'pterodactyl', label: 'Pterodactyl', desc: 'Connect through Pterodactyl panel API' },
|
{ value: 'pterodactyl', label: 'Pterodactyl', desc: 'Connect through Pterodactyl panel API' },
|
||||||
]
|
]
|
||||||
@@ -183,7 +183,7 @@ async function completeSetup() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: Companion agent install -->
|
<!-- Step 2: Corrosion host agent install -->
|
||||||
<div v-if="step === 2" class="setup-card">
|
<div v-if="step === 2" class="setup-card">
|
||||||
<div class="setup-card__head setup-card__head--center">
|
<div class="setup-card__head setup-card__head--center">
|
||||||
<div class="setup-icon">
|
<div class="setup-icon">
|
||||||
@@ -191,12 +191,12 @@ async function completeSetup() {
|
|||||||
<path d="M5 12.55a11 11 0 0 1 14.08 0M1.42 9a16 16 0 0 1 21.16 0M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01" />
|
<path d="M5 12.55a11 11 0 0 1 14.08 0M1.42 9a16 16 0 0 1 21.16 0M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="setup-card__title">Install the Companion Agent</h1>
|
<h1 class="setup-card__title">Install the Corrosion host agent</h1>
|
||||||
<p class="setup-card__sub">The agent runs on your server and connects to Corrosion — no inbound ports required.</p>
|
<p class="setup-card__sub">The agent runs on your server and connects to Corrosion — no inbound ports required.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setup-code">
|
<div class="setup-code">
|
||||||
<p class="setup-code__comment"># Download and install the Companion Agent</p>
|
<p class="setup-code__comment"># Download and install the Corrosion host agent</p>
|
||||||
<p class="setup-code__cmd">curl -sSL https://get.corrosionmgmt.com | sh</p>
|
<p class="setup-code__cmd">curl -sSL https://get.corrosionmgmt.com | sh</p>
|
||||||
<p class="setup-code__comment setup-code__comment--mt"># Start the agent with your license key</p>
|
<p class="setup-code__comment setup-code__comment--mt"># Start the agent with your license key</p>
|
||||||
<p class="setup-code__cmd">corrosion-agent start --key {{ auth.license?.license_key ?? 'YOUR-LICENSE-KEY' }}</p>
|
<p class="setup-code__cmd">corrosion-agent start --key {{ auth.license?.license_key ?? 'YOUR-LICENSE-KEY' }}</p>
|
||||||
|
|||||||
@@ -1,16 +1,39 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
/**
|
||||||
import { Shield, Users, Star, MessageCircle, Clock, ChevronRight, Check, Zap, Terminal, RefreshCw, LayoutDashboard } from 'lucide-vue-next'
|
* EarlyAccess signup page.
|
||||||
|
*
|
||||||
|
* Backend endpoint: POST /api/early-access
|
||||||
|
* The early_access_signups entity exists but no NestJS controller/module exposes it yet.
|
||||||
|
* TODO: Create backend-nest/src/modules/early-access/ with a @Public() POST /early-access
|
||||||
|
* controller that accepts { email, name?, game_interest? } and writes to early_access_signups.
|
||||||
|
* The server_count column on the entity is varchar(10) — map game_interest to it or add a
|
||||||
|
* migration adding a game_interest column.
|
||||||
|
*/
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import Icon from '@/components/ds/core/Icon.vue'
|
||||||
|
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
|
||||||
|
|
||||||
// ---------- Email capture ----------
|
const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
|
||||||
|
|
||||||
|
// Form state
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const serverCount = ref('')
|
const name = ref('')
|
||||||
|
const gameInterest = ref('')
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const submitted = ref(false)
|
const submitted = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
|
||||||
async function handleSubmit() {
|
const GAME_OPTIONS = [
|
||||||
if (!email.value || !serverCount.value) return
|
{ value: 'rust', label: 'Rust' },
|
||||||
|
{ value: 'dune', label: 'Dune: Awakening' },
|
||||||
|
{ value: 'conan', label: 'Conan Exiles' },
|
||||||
|
{ value: 'soulmask', label: 'Soulmask' },
|
||||||
|
{ value: 'multiple', label: 'Multiple games' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function handleSubmit(): Promise<void> {
|
||||||
|
if (!email.value) return
|
||||||
errorMsg.value = ''
|
errorMsg.value = ''
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
@@ -19,12 +42,13 @@ async function handleSubmit() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email.value,
|
email: email.value,
|
||||||
server_count: serverCount.value,
|
// server_count column stores game interest (varchar 10) — no dedicated name column in DB
|
||||||
|
server_count: gameInterest.value || 'not specified',
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({ message: 'Something went wrong' }))
|
const data = await res.json().catch(() => ({ message: 'Something went wrong' }))
|
||||||
throw new Error(data.message || `HTTP ${res.status}`)
|
throw new Error((data as { message?: string }).message ?? `HTTP ${res.status}`)
|
||||||
}
|
}
|
||||||
submitted.value = true
|
submitted.value = true
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -34,291 +58,393 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Demo panels ----------
|
// Scroll-reveal
|
||||||
const panels = [
|
let io: IntersectionObserver | null = null
|
||||||
{ label: 'Dashboard', icon: LayoutDashboard, desc: 'Server overview, player count, uptime, and alerts at a glance.' },
|
|
||||||
{ label: 'Wipe Scheduler', icon: RefreshCw, desc: 'Visual wipe timeline with pre-wipe backup, map rotation, and health verification.' },
|
|
||||||
{ label: 'Plugin Config', icon: Zap, desc: 'Edit plugin settings from your browser. No JSON. No SFTP.' },
|
|
||||||
{ label: 'Player Management', icon: Users, desc: 'Online players, session tracking, kick/ban controls, and playtime history.' },
|
|
||||||
{ label: 'Console', icon: Terminal, desc: 'Real-time RCON console with timestamped, color-coded output.' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// ---------- Roadmap voting ----------
|
function initReveal(): void {
|
||||||
interface VoteItem {
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||||
id: string
|
io = new IntersectionObserver(
|
||||||
label: string
|
(entries) => {
|
||||||
votes: number
|
entries.forEach((e) => {
|
||||||
voted: boolean
|
if (e.isIntersecting) {
|
||||||
|
e.target.classList.add('in')
|
||||||
|
io?.unobserve(e.target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
|
||||||
}
|
}
|
||||||
|
|
||||||
const voteItems = ref<VoteItem[]>([
|
onMounted(() => { initReveal() })
|
||||||
{ id: 'analytics', label: 'Analytics & Retention Insights', votes: 47, voted: false },
|
onUnmounted(() => { io?.disconnect() })
|
||||||
{ id: 'webstore', label: 'Integrated Webstore', votes: 38, voted: false },
|
|
||||||
{ id: 'modules', label: 'Module Marketplace', votes: 31, voted: false },
|
|
||||||
{ id: 'discord', label: 'Discord Bot Integration', votes: 28, voted: false },
|
|
||||||
{ id: 'hosting', label: 'Hosting Provider API', votes: 19, voted: false },
|
|
||||||
])
|
|
||||||
|
|
||||||
function vote(item: VoteItem) {
|
|
||||||
if (item.voted) return
|
|
||||||
item.votes++
|
|
||||||
item.voted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalVotes = computed(() => voteItems.value.reduce((sum, i) => sum + i.votes, 0))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- PAGE HEADER -->
|
||||||
<!-- Hero -->
|
<section class="hero" style="padding-bottom:0; border-bottom:none;">
|
||||||
<section class="relative overflow-hidden">
|
<div class="hero__atmo" />
|
||||||
<div class="max-w-4xl mx-auto px-6 pt-20 pb-16 text-center">
|
<div class="hero__grid" />
|
||||||
<span class="inline-block px-4 py-1.5 bg-green-500/10 border border-green-500/20 rounded-full text-green-400 text-sm font-medium mb-6">
|
<div class="hero__grain" />
|
||||||
Early Access Is Now Open
|
<div class="wrap hero__in" style="padding-bottom:52px;">
|
||||||
</span>
|
<div class="hero__mark">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4 tracking-tight">
|
<CorrosionMark :size="56" />
|
||||||
Wipe Night Just Got<br />
|
|
||||||
<span class="text-oxide-500">A Lot Easier.</span>
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-neutral-400 max-w-xl mx-auto mb-10">
|
|
||||||
Corrosion is live in limited early access. Install once. Automate everything. Never SSH again.
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center justify-center gap-4">
|
|
||||||
<a href="#join" class="px-8 py-3.5 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors">
|
|
||||||
Claim Your Spot
|
|
||||||
</a>
|
|
||||||
<a href="#demo" class="px-8 py-3.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-semibold rounded-lg border border-neutral-700 transition-colors">
|
|
||||||
View Demo Architecture
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-oxide-500/8 rounded-full blur-3xl pointer-events-none" />
|
<span class="eyebrow">Early access</span>
|
||||||
</section>
|
<h1 style="font-size:var(--text-5xl)">
|
||||||
|
Take control of your servers.
|
||||||
|
<span class="accent">Starting now.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero__sub">
|
||||||
|
Corrosion is in early access. Join the list to be notified when your access opens.
|
||||||
|
No spam. No fabricated scarcity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Early Access Live Banner -->
|
<!-- WHAT YOU GET -->
|
||||||
<section class="py-12 border-t border-neutral-800">
|
<section class="sec" id="access">
|
||||||
<div class="max-w-3xl mx-auto px-6 text-center">
|
<div class="wrap">
|
||||||
<div class="inline-flex items-center gap-3 px-6 py-4 bg-green-500/10 border border-green-500/20 rounded-2xl">
|
<div class="sec__head reveal">
|
||||||
<div class="w-2.5 h-2.5 bg-green-400 rounded-full animate-pulse shrink-0" />
|
<span class="eyebrow">What early access means</span>
|
||||||
<p class="text-green-300 font-semibold text-lg">Early Access is now live — founding admin spots are limited.</p>
|
<h2 class="title">Real access to a real platform.</h2>
|
||||||
</div>
|
<p class="lead">
|
||||||
<p class="text-neutral-500 text-sm mt-4">
|
Early access is not a waitlist gimmick. It is how we manage onboarding while the
|
||||||
Sign up below to lock in founding pricing before spots run out.
|
platform stabilizes. You get the full Corrosion control plane — one tier at a time
|
||||||
|
as capacity opens.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- What Early Access Means -->
|
<div class="infra reveal">
|
||||||
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800">
|
<div class="icard">
|
||||||
<div class="max-w-4xl mx-auto px-6">
|
<div class="icard__ic"><Icon name="cpu" :size="16" /></div>
|
||||||
<h2 class="text-2xl font-bold text-neutral-100 mb-8 text-center">What Early Access Means</h2>
|
<b>Full control plane</b>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<p>Agent, panel, wipes, console, plugins, schedules — all of it. Not a trimmed preview.</p>
|
||||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
</div>
|
||||||
<Shield class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
<div class="icard">
|
||||||
<p class="text-sm font-medium text-neutral-200">Limited Founding Licenses</p>
|
<div class="icard__ic"><Icon name="shield" :size="16" /></div>
|
||||||
<p class="text-xs text-neutral-500 mt-1">25–50 spots</p>
|
<b>Pricing you can lock in</b>
|
||||||
</div>
|
<p>Early access pricing is the live pricing. No bait-and-switch after launch.</p>
|
||||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
</div>
|
||||||
<MessageCircle class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
<div class="icard">
|
||||||
<p class="text-sm font-medium text-neutral-200">Direct Founder Discord</p>
|
<div class="icard__ic"><Icon name="message-square" :size="16" /></div>
|
||||||
<p class="text-xs text-neutral-500 mt-1">Private channel access</p>
|
<b>Direct feedback channel</b>
|
||||||
</div>
|
<p>Early access operators have a direct line for platform bug reports and feature input.</p>
|
||||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
</div>
|
||||||
<Star class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
<div class="icard">
|
||||||
<p class="text-sm font-medium text-neutral-200">Influence the Roadmap</p>
|
<div class="icard__ic"><Icon name="box" :size="16" /></div>
|
||||||
<p class="text-xs text-neutral-500 mt-1">Vote on features</p>
|
<b>Rust-first</b>
|
||||||
</div>
|
<p>Rust support is complete. Dune, Conan, and Soulmask are in active development.</p>
|
||||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
</div>
|
||||||
<Clock class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
<div class="icard">
|
||||||
<p class="text-sm font-medium text-neutral-200">Lifetime Pricing Lock</p>
|
<div class="icard__ic"><Icon name="users" :size="16" /></div>
|
||||||
<p class="text-xs text-neutral-500 mt-1">Never pay more</p>
|
<b>RBAC team access</b>
|
||||||
</div>
|
<p>Add your admin team from day one. Fine-grained permission roles are built in.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Email Capture -->
|
<!-- SIGNUP FORM -->
|
||||||
<section id="join" class="py-16 border-t border-neutral-800">
|
<section class="sec" id="join">
|
||||||
<div class="max-w-md mx-auto px-6">
|
<div class="wrap">
|
||||||
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">Claim Your Founding Spot</h2>
|
<div class="ea-form-wrap reveal">
|
||||||
<p class="text-neutral-400 text-center mb-8">Early access is open now. Spots are limited — lock in founding pricing today.</p>
|
<!-- Success state -->
|
||||||
|
<div v-if="submitted" class="ea-success">
|
||||||
<div v-if="submitted" class="bg-green-500/10 border border-green-500/20 rounded-xl p-8 text-center">
|
<div class="ea-success__ic">
|
||||||
<Check class="w-10 h-10 text-green-400 mx-auto mb-3" />
|
<Icon name="check" :size="28" />
|
||||||
<h3 class="text-lg font-semibold text-neutral-100 mb-1">You're in.</h3>
|
</div>
|
||||||
<p class="text-sm text-neutral-400">We'll be in touch shortly with your access details.</p>
|
<h2 class="ea-success__title">You are on the list.</h2>
|
||||||
|
<p class="ea-success__body">
|
||||||
|
We will reach out when your access slot opens. In the meantime, read the
|
||||||
|
<RouterLink :to="{ name: 'how-it-works' }" class="ea-link">how it works</RouterLink>
|
||||||
|
guide or review the
|
||||||
|
<RouterLink :to="{ name: 'faq' }" class="ea-link">FAQ</RouterLink>.
|
||||||
|
</p>
|
||||||
|
<RouterLink class="btn btn--ghost" :to="{ name: 'landing' }">
|
||||||
|
Back to home
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form v-else @submit.prevent="handleSubmit" class="space-y-4">
|
<!-- Form state -->
|
||||||
<div v-if="errorMsg" class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm">
|
<form v-else @submit.prevent="handleSubmit" class="ea-form">
|
||||||
|
<div class="ea-form__head">
|
||||||
|
<h2>Join the early access list</h2>
|
||||||
|
<p>Required: email address. Everything else is optional but helps us prioritise.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error banner -->
|
||||||
|
<div v-if="errorMsg" class="ea-error">
|
||||||
|
<Icon name="triangle-alert" :size="15" />
|
||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="ea-email" class="block text-sm font-medium text-neutral-400 mb-1.5">Email</label>
|
<!-- Email (required) -->
|
||||||
|
<div class="ea-field">
|
||||||
|
<label class="ea-field__label" for="ea-email">
|
||||||
|
Email address <span class="ea-field__req">*</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="ea-email"
|
id="ea-email"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
|
autocomplete="email"
|
||||||
placeholder="admin@example.com"
|
placeholder="admin@example.com"
|
||||||
class="w-full px-3 py-2.5 bg-neutral-900 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
class="ea-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="ea-servers" class="block text-sm font-medium text-neutral-400 mb-1.5">How many servers do you run?</label>
|
<!-- Name (optional) -->
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="ea-field">
|
||||||
|
<label class="ea-field__label" for="ea-name">
|
||||||
|
Your name <span class="ea-field__optional">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="ea-name"
|
||||||
|
v-model="name"
|
||||||
|
type="text"
|
||||||
|
autocomplete="name"
|
||||||
|
placeholder="Server admin name or handle"
|
||||||
|
class="ea-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game interest (optional) -->
|
||||||
|
<div class="ea-field">
|
||||||
|
<label class="ea-field__label">
|
||||||
|
Primary game interest <span class="ea-field__optional">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<div class="ea-pills">
|
||||||
<button
|
<button
|
||||||
v-for="option in ['1', '2-3', '4+']"
|
v-for="opt in GAME_OPTIONS"
|
||||||
:key="option"
|
:key="opt.value"
|
||||||
type="button"
|
type="button"
|
||||||
@click="serverCount = option"
|
class="ea-pill"
|
||||||
class="py-2.5 text-sm font-medium rounded-lg border transition-colors"
|
:class="{ 'ea-pill--on': gameInterest === opt.value }"
|
||||||
:class="serverCount === option
|
@click="gameInterest = gameInterest === opt.value ? '' : opt.value"
|
||||||
? 'bg-oxide-500/15 border-oxide-500/40 text-oxide-400'
|
|
||||||
: 'bg-neutral-900 border-neutral-700 text-neutral-400 hover:border-neutral-600'"
|
|
||||||
>
|
>
|
||||||
{{ option }}
|
{{ opt.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="submitting || !email || !serverCount"
|
class="btn btn--primary btn--lg ea-submit"
|
||||||
class="w-full py-3 bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors"
|
:disabled="submitting || !email"
|
||||||
>
|
>
|
||||||
{{ submitting ? 'Submitting...' : 'Join Early Access' }}
|
<Icon v-if="submitting" name="loader" :size="16" />
|
||||||
|
<Icon v-else name="send" :size="16" />
|
||||||
|
{{ submitting ? 'Submitting…' : 'Join early access' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<p class="ea-privacy">
|
||||||
|
We store your email to contact you when access opens. We do not sell or share it.
|
||||||
|
No newsletters unless you opt in separately.
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Founding Admin Program -->
|
<!-- HOW IT WORKS TEASER -->
|
||||||
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800">
|
<section class="sec" id="teaser">
|
||||||
<div class="max-w-3xl mx-auto px-6 text-center">
|
<div class="wrap">
|
||||||
<span class="inline-block px-3 py-1 bg-oxide-500/10 border border-oxide-500/20 rounded-full text-oxide-400 text-xs font-semibold uppercase tracking-wider mb-4">
|
<div class="sec__head reveal">
|
||||||
Limited to 25 Servers
|
<span class="eyebrow">How it works</span>
|
||||||
</span>
|
<h2 class="title">Install the agent. Never SSH again.</h2>
|
||||||
<h2 class="text-3xl font-bold text-neutral-100 mb-4">Founding Admin Program</h2>
|
</div>
|
||||||
<p class="text-neutral-400 mb-8">
|
<div class="steps reveal">
|
||||||
The first 25 servers to run Corrosion receive:
|
<div class="step">
|
||||||
</p>
|
<div class="step__n">1</div>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<b>Install the host agent</b>
|
||||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
<p>Download the Go binary from your dashboard. Run it on Windows or Linux. One agent per machine.</p>
|
||||||
<Star class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
</div>
|
||||||
<p class="text-sm font-medium text-neutral-200">Founding Admin Role</p>
|
<div class="step">
|
||||||
<p class="text-xs text-neutral-500 mt-1">Discord badge</p>
|
<div class="step__n">2</div>
|
||||||
</div>
|
<b>Agent connects to Corrosion</b>
|
||||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
<p>Single outbound NATS connection. No inbound ports. No exposed management panel on your machine.</p>
|
||||||
<Clock class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
</div>
|
||||||
<p class="text-sm font-medium text-neutral-200">Permanent Early Pricing</p>
|
<div class="step">
|
||||||
<p class="text-xs text-neutral-500 mt-1">Locked forever</p>
|
<div class="step__n">3</div>
|
||||||
</div>
|
<b>Manage from the browser</b>
|
||||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
<p>Console, wipes, plugins, schedules, file manager, player management — all at panel.corrosionmgmt.com.</p>
|
||||||
<Users class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
|
||||||
<p class="text-sm font-medium text-neutral-200">Public Recognition</p>
|
|
||||||
<p class="text-xs text-neutral-500 mt-1">Featured server</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
|
||||||
<MessageCircle class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
|
||||||
<p class="text-sm font-medium text-neutral-200">Direct Feature Input</p>
|
|
||||||
<p class="text-xs text-neutral-500 mt-1">Private channel</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="closing reveal">
|
||||||
|
<RouterLink :to="{ name: 'how-it-works' }" class="btn btn--ghost btn--lg">
|
||||||
<!-- Demo Dashboard Preview -->
|
<Icon name="chevron-right" :size="17" />Read the full walkthrough
|
||||||
<section id="demo" class="py-16 border-t border-neutral-800">
|
</RouterLink>
|
||||||
<div class="max-w-5xl mx-auto px-6">
|
|
||||||
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">Preview of Corrosion 1.0 Dashboard</h2>
|
|
||||||
<p class="text-neutral-500 text-center mb-10">Screenshots will replace these frames at launch.</p>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
<div
|
|
||||||
v-for="panel in panels"
|
|
||||||
:key="panel.label"
|
|
||||||
class="bg-neutral-900 border border-neutral-800 rounded-xl overflow-hidden hover:border-neutral-700 transition-colors"
|
|
||||||
>
|
|
||||||
<div class="h-36 bg-neutral-800/50 flex items-center justify-center border-b border-neutral-800">
|
|
||||||
<component :is="panel.icon" class="w-10 h-10 text-neutral-700" />
|
|
||||||
</div>
|
|
||||||
<div class="p-4">
|
|
||||||
<h3 class="text-sm font-semibold text-neutral-200 mb-1">{{ panel.label }}</h3>
|
|
||||||
<p class="text-xs text-neutral-500 leading-relaxed">{{ panel.desc }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Roadmap Voting -->
|
<!-- FINAL CTA -->
|
||||||
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800">
|
<section class="finalcta">
|
||||||
<div class="max-w-2xl mx-auto px-6">
|
<div class="finalcta__atmo" />
|
||||||
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">What Should We Build Next?</h2>
|
<div class="wrap finalcta__in reveal">
|
||||||
<p class="text-neutral-500 text-center mb-8">Vote on the features that matter most to you.</p>
|
<h2>Ready to stop babysitting<br>your servers?</h2>
|
||||||
<div class="space-y-3">
|
<div class="cta-row">
|
||||||
<button
|
<a href="#join" class="btn btn--primary btn--lg">
|
||||||
v-for="item in voteItems"
|
Sign up above
|
||||||
:key="item.id"
|
</a>
|
||||||
@click="vote(item)"
|
<a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
|
||||||
class="w-full flex items-center gap-4 p-4 bg-neutral-900 border rounded-xl transition-colors text-left"
|
<Icon name="play" :size="17" />View live demo
|
||||||
:class="item.voted ? 'border-oxide-500/30' : 'border-neutral-800 hover:border-neutral-700'"
|
</a>
|
||||||
>
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="text-sm font-medium" :class="item.voted ? 'text-oxide-400' : 'text-neutral-200'">{{ item.label }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3 shrink-0">
|
|
||||||
<div class="w-24 h-1.5 bg-neutral-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full transition-all duration-300"
|
|
||||||
:class="item.voted ? 'bg-oxide-500' : 'bg-neutral-600'"
|
|
||||||
:style="{ width: `${totalVotes ? (item.votes / totalVotes) * 100 : 0}%` }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs font-medium tabular-nums w-8 text-right" :class="item.voted ? 'text-oxide-400' : 'text-neutral-500'">
|
|
||||||
{{ item.votes }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
<!-- Timeline -->
|
|
||||||
<section class="py-16 border-t border-neutral-800">
|
|
||||||
<div class="max-w-2xl mx-auto px-6">
|
|
||||||
<h2 class="text-2xl font-bold text-neutral-100 mb-8 text-center">Launch Timeline</h2>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<div class="w-8 h-8 bg-green-500/10 border border-green-500/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
|
|
||||||
<Check class="w-4 h-4 text-green-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-200">Week 1 — Closed Beta Stabilization</p>
|
|
||||||
<p class="text-xs text-neutral-500 mt-0.5">Core platform hardening and testing.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4 w-px h-4 bg-neutral-800" />
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<div class="w-8 h-8 bg-green-500/10 border border-green-500/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
|
|
||||||
<Check class="w-4 h-4 text-green-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-200">Week 2 — Early Access Open</p>
|
|
||||||
<p class="text-xs text-neutral-500 mt-0.5">Founding Admin licenses are live — claim yours now.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4 w-px h-4 bg-neutral-800" />
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
<div class="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-full flex items-center justify-center shrink-0 mt-0.5">
|
|
||||||
<ChevronRight class="w-4 h-4 text-neutral-500" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-neutral-400">Public Release</p>
|
|
||||||
<p class="text-xs text-neutral-500 mt-0.5">Shortly after early access stabilization.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Form wrapper */
|
||||||
|
.ea-form-wrap {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success state */
|
||||||
|
.ea-success {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 32px;
|
||||||
|
background: var(--surface-base);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
}
|
||||||
|
.ea-success__ic {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-text);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
margin: 0 auto 18px;
|
||||||
|
}
|
||||||
|
.ea-success__title {
|
||||||
|
font-family: var(--font-brand);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.ea-success__body {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
}
|
||||||
|
.ea-link {
|
||||||
|
color: var(--accent-text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ea-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Form */
|
||||||
|
.ea-form {
|
||||||
|
background: var(--surface-base);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
padding: 36px 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 22px;
|
||||||
|
}
|
||||||
|
.ea-form__head h2 {
|
||||||
|
font-family: var(--font-brand);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
.ea-form__head p {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error banner */
|
||||||
|
.ea-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: color-mix(in srgb, var(--status-offline) 10%, transparent);
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--status-offline) 25%, transparent);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--status-offline);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fields */
|
||||||
|
.ea-field { display: flex; flex-direction: column; gap: 7px; }
|
||||||
|
.ea-field__label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.ea-field__req { color: var(--status-offline); }
|
||||||
|
.ea-field__optional { color: var(--text-muted); font-weight: 400; }
|
||||||
|
|
||||||
|
.ea-input {
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 13px;
|
||||||
|
background: var(--surface-inset);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.ea-input::placeholder { color: var(--text-muted); }
|
||||||
|
.ea-input:focus {
|
||||||
|
border-color: var(--accent-border);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game pills */
|
||||||
|
.ea-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ea-pill {
|
||||||
|
height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: var(--transition-colors);
|
||||||
|
}
|
||||||
|
.ea-pill--on {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-text);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit */
|
||||||
|
.ea-submit {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.ea-submit:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* Privacy note */
|
||||||
|
.ea-privacy {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,101 +1,353 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { ChevronDown } from 'lucide-vue-next'
|
import { RouterLink } from 'vue-router'
|
||||||
|
import Icon from '@/components/ds/core/Icon.vue'
|
||||||
|
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
|
||||||
|
|
||||||
interface FaqItem {
|
interface FaqItem {
|
||||||
question: string
|
question: string
|
||||||
answer: string
|
answer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const faqs: FaqItem[] = [
|
interface FaqGroup {
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
items: FaqItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: FaqGroup[] = [
|
||||||
{
|
{
|
||||||
question: 'Do I need to open any firewall ports?',
|
label: 'Support',
|
||||||
answer: 'No. All connections are outbound from your server to Corrosion\'s cloud. No inbound ports required.',
|
icon: 'help-circle',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: 'Do you provide direct support?',
|
||||||
|
answer:
|
||||||
|
'Corrosion is a self-service tool. Every plan includes documentation, community forum access, diagnostics, and structured platform bug reports. We do not provide 1:1 setup assistance, Discord DMs, video calls, server administration, hosting-provider troubleshooting, firewall configuration, mod installation, or emergency wipe-day support.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What if Corrosion itself is broken?',
|
||||||
|
answer:
|
||||||
|
'Platform bugs and agent issues go through structured bug reports in the panel. Operator and Network customers receive priority bug triage for platform-level issues. Direct server administration is not included in any support tier.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do you manage my server for me?',
|
||||||
|
answer:
|
||||||
|
'No. Corrosion provides the panel, the agent, automation workflows, diagnostics, and the control plane. You remain responsible for your host machine, operating system, network, firewall, game files, mods, and community rules.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Is hands-on help available?',
|
||||||
|
answer:
|
||||||
|
'Yes — separately. Direct 1:1 support is available at $125/hour, prepaid in 1-hour blocks. This is billed time with a human, not a support tier. It is available to any customer who needs it.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What does community support include?',
|
||||||
|
answer:
|
||||||
|
'Documentation (setup guides, architecture reference, troubleshooting walkthroughs), a community forum for operator-to-operator knowledge sharing, in-panel diagnostics (agent health, log access), and a structured bug report system for platform issues.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Does Corrosion replace my hosting panel (AMP / Pterodactyl)?',
|
label: 'Product',
|
||||||
answer: 'No. Corrosion integrates with them via API or companion agent. Your existing panel remains intact.',
|
icon: 'server',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: 'Do I need my own server?',
|
||||||
|
answer:
|
||||||
|
'Yes. Corrosion is bring-your-own-server. You supply the host machine — a VPS, dedicated server, or bare metal box running Windows or Linux. Corrosion provides the control plane, the agent, and the panel.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Does Corrosion host my game server for me?',
|
||||||
|
answer:
|
||||||
|
'No. Corrosion is not a hosting provider. It is a management layer that runs on top of a server you already own or rent. If you need hosting, you need a separate hosting provider.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Do I need to open inbound firewall ports for Corrosion?',
|
||||||
|
answer:
|
||||||
|
'No. The host agent establishes a single outbound NATS connection to Corrosion\'s cloud. No inbound management ports are required. Your game server\'s player ports (RCON, game ports) remain as they have always been.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Does Corrosion replace AMP or Pterodactyl?',
|
||||||
|
answer:
|
||||||
|
'It can, depending on your use case. Corrosion is an independent control plane — it does not require an existing game panel. If you are currently using AMP or Pterodactyl, migration tooling is available in the panel.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What happens if Corrosion goes offline?',
|
||||||
|
answer:
|
||||||
|
'Your game servers continue running normally. Corrosion does not proxy gameplay traffic — it only handles management operations. If the panel or cloud is unreachable, your players are unaffected.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Can multiple admins manage the same server?',
|
||||||
|
answer:
|
||||||
|
'Yes. Role-based access control (RBAC) is built in. You can grant team members specific permissions — from full admin access down to read-only viewer — without sharing credentials.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'What OS does the agent run on?',
|
||||||
|
answer:
|
||||||
|
'Both Windows and Linux are supported for the host agent. The agent binary is downloaded directly from your dashboard — there is no manual build or dependency setup required.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Is my data isolated from other customers?',
|
||||||
|
answer:
|
||||||
|
'Yes. All data is scoped by license ID at the database level. No server, config, or player data is shared across tenant boundaries.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'What happens if Corrosion goes offline?',
|
label: 'Games',
|
||||||
answer: 'Your Rust server continues running normally. Corrosion does not proxy gameplay traffic.',
|
icon: 'box',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
question: 'Which games are supported?',
|
||||||
|
answer:
|
||||||
|
'Currently in active development or shipped: Rust, Dune: Awakening, Conan Exiles, and Soulmask. Each game has a purpose-built blueprint — not a generic template — that models its specific operational reality (wipe types, mod systems, cluster configurations). More games are planned.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Does Corrosion support Rust plugin management?',
|
||||||
|
answer:
|
||||||
|
'Yes. Corrosion integrates with uMod (Oxide) for Rust. You can browse the plugin registry, install plugins, manage configuration profiles, and push config changes to the server — all from the browser.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Can I run multiple game types on the same host machine?',
|
||||||
|
answer:
|
||||||
|
'Yes. A single host agent can supervise multiple game server processes — across different games — on the same machine. Each game instance has its own lifecycle, configuration, and wipe schedule.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Does Corrosion handle Rust wipes?',
|
||||||
|
answer:
|
||||||
|
'Yes. Rust wipes are a first-class feature: map wipes, blueprint wipes, and full wipes. Wipes run as verified, logged sequences — pre-warning, backup, stop, update, map rotation, restart, health check, announce. Rollback is available when supported.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'Is my data shared with other servers?',
|
label: 'Billing',
|
||||||
answer: 'No. All data is isolated by license ID. Multi-tenant database queries are scoped per license.',
|
icon: 'credit-card',
|
||||||
},
|
items: [
|
||||||
{
|
{
|
||||||
question: 'What if a wipe fails?',
|
question: 'What counts as commercial use?',
|
||||||
answer: 'Corrosion can automatically retry and optionally roll back using the pre-wipe backup.',
|
answer:
|
||||||
},
|
'Commercial use includes monetized communities, paid access, VIP slots, donation-funded servers, sponsorship-supported servers, hosting providers, or managing servers for others. Hobby and Community plans are non-commercial only. Operator and Network plans permit commercial use.',
|
||||||
{
|
},
|
||||||
question: 'Does this work on bare metal?',
|
{
|
||||||
answer: 'Yes. Use the Companion Agent — no SSH required after initial setup.',
|
question: 'What is the Fleet Block on the Network plan?',
|
||||||
},
|
answer:
|
||||||
{
|
'The Network plan base includes 50 server instances at $99.99/mo. Each additional Fleet Block adds 50 more server slots at $49.99/mo. Stack as many Fleet Blocks as your operation requires.',
|
||||||
question: 'Can I manage multiple admins?',
|
},
|
||||||
answer: 'Yes. Multi-Admin Role-Based Access Control is built in. Grant granular permissions per team member.',
|
{
|
||||||
},
|
question: 'Can I upgrade my plan?',
|
||||||
{
|
answer:
|
||||||
question: 'Is this beginner friendly?',
|
'Yes. You can upgrade at any time. Pricing is prorated from the upgrade date.',
|
||||||
answer: 'Yes. If you can install a uMod plugin, you can use Corrosion.',
|
},
|
||||||
},
|
{
|
||||||
{
|
question: 'Is there a free trial?',
|
||||||
question: 'Does this replace Tebex?',
|
answer:
|
||||||
answer: 'Corrosion includes an optional integrated store (Phase 5 roadmap), but does not require Tebex.',
|
'Corrosion is currently in early access. Join the early access list to be notified when access opens.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: 'How is licensing handled?',
|
question: 'Are there annual billing discounts?',
|
||||||
answer: 'One license per server. License validation occurs on plugin startup and periodically.',
|
answer:
|
||||||
|
'Not at this time. All plans are billed monthly.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const openIndex = ref<number | null>(null)
|
const openKey = ref<string | null>(null)
|
||||||
|
|
||||||
function toggle(index: number) {
|
function toggle(key: string): void {
|
||||||
openIndex.value = openIndex.value === index ? null : index
|
openKey.value = openKey.value === key ? null : key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function itemKey(groupLabel: string, idx: number): string {
|
||||||
|
return `${groupLabel}-${idx}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll-reveal
|
||||||
|
let io: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
function initReveal(): void {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||||
|
io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
e.target.classList.add('in')
|
||||||
|
io?.unobserve(e.target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { initReveal() })
|
||||||
|
onUnmounted(() => { io?.disconnect() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- PAGE HEADER -->
|
||||||
<!-- Header -->
|
<section class="hero" style="padding-bottom:0; border-bottom:none;">
|
||||||
<section class="pt-20 pb-12">
|
<div class="hero__atmo" />
|
||||||
<div class="max-w-4xl mx-auto px-6 text-center">
|
<div class="hero__grid" />
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">Frequently Asked Questions</h1>
|
<div class="hero__grain" />
|
||||||
<p class="text-lg text-neutral-400">Everything you need to know about Corrosion.</p>
|
<div class="wrap hero__in" style="padding-bottom:52px;">
|
||||||
|
<div class="hero__mark">
|
||||||
|
<CorrosionMark :size="56" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<span class="eyebrow">FAQ</span>
|
||||||
|
<h1 style="font-size:var(--text-5xl)">
|
||||||
|
Honest answers.
|
||||||
|
<span class="accent">No marketing fluff.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero__sub">
|
||||||
|
Common questions about support, the product, supported games, and billing — answered
|
||||||
|
plainly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- FAQ Accordion -->
|
<!-- FAQ GROUPS -->
|
||||||
<section class="pb-20">
|
<section class="sec" id="faq">
|
||||||
<div class="max-w-3xl mx-auto px-6">
|
<div class="wrap">
|
||||||
<div class="space-y-3">
|
<div
|
||||||
|
v-for="group in groups"
|
||||||
|
:key="group.label"
|
||||||
|
class="faq-group reveal"
|
||||||
|
>
|
||||||
|
<div class="faq-group__head">
|
||||||
|
<span class="faq-group__ic">
|
||||||
|
<Icon :name="group.icon" :size="16" />
|
||||||
|
</span>
|
||||||
|
<span class="eyebrow">{{ group.label }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-list">
|
||||||
<div
|
<div
|
||||||
v-for="(faq, index) in faqs"
|
v-for="(item, idx) in group.items"
|
||||||
:key="index"
|
:key="idx"
|
||||||
class="bg-neutral-900 border rounded-xl overflow-hidden transition-colors"
|
class="faq-item"
|
||||||
:class="openIndex === index ? 'border-oxide-500/30' : 'border-neutral-800'"
|
:class="{ 'faq-item--open': openKey === itemKey(group.label, idx) }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="toggle(index)"
|
class="faq-item__q"
|
||||||
class="w-full flex items-center justify-between p-6 text-left"
|
@click="toggle(itemKey(group.label, idx))"
|
||||||
>
|
>
|
||||||
<span class="text-neutral-100 font-medium pr-4">{{ faq.question }}</span>
|
<span>{{ item.question }}</span>
|
||||||
<ChevronDown
|
<span class="faq-item__chevron">
|
||||||
class="w-5 h-5 text-neutral-500 shrink-0 transition-transform duration-200"
|
<Icon
|
||||||
:class="{ 'rotate-180': openIndex === index }"
|
name="chevron-down"
|
||||||
/>
|
:size="16"
|
||||||
|
:class="{ 'faq-item__chevron--open': openKey === itemKey(group.label, idx) }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="openIndex === index"
|
v-if="openKey === itemKey(group.label, idx)"
|
||||||
class="px-6 pb-6 -mt-2"
|
class="faq-item__a"
|
||||||
>
|
>
|
||||||
<p class="text-neutral-400 leading-relaxed">{{ faq.answer }}</p>
|
{{ item.answer }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
<!-- SUPPORT CTA -->
|
||||||
|
<section class="sec" id="support-cta" style="border-bottom:none">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec__head reveal">
|
||||||
|
<span class="eyebrow">Still have questions?</span>
|
||||||
|
<h2 class="title">Check the docs or join the community</h2>
|
||||||
|
<p class="lead">
|
||||||
|
The documentation covers setup, architecture, troubleshooting, and every supported
|
||||||
|
game. The community forum is where operators share configs, ask questions, and help
|
||||||
|
each other.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero__cta reveal">
|
||||||
|
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
|
||||||
|
Join early access
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink class="btn btn--ghost btn--lg" :to="{ name: 'pricing' }">
|
||||||
|
<Icon name="credit-card" :size="17" />View pricing
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.faq-group {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.faq-group__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.faq-group__ic {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--accent-text);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.faq-item {
|
||||||
|
background: var(--surface-base);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow var(--dur-fast);
|
||||||
|
}
|
||||||
|
.faq-item--open {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
}
|
||||||
|
.faq-item__q {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: color var(--dur-fast);
|
||||||
|
}
|
||||||
|
.faq-item__q:hover { color: var(--accent-text); }
|
||||||
|
.faq-item__chevron {
|
||||||
|
flex: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color var(--dur-fast);
|
||||||
|
}
|
||||||
|
.faq-item__chevron--open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
color: var(--accent-text);
|
||||||
|
}
|
||||||
|
.faq-item__a {
|
||||||
|
padding: 0 20px 18px;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,150 +1,358 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Download, Globe, Wifi, LayoutDashboard, ArrowDown } from 'lucide-vue-next'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import Icon from '@/components/ds/core/Icon.vue'
|
||||||
|
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
|
||||||
|
|
||||||
|
const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
|
||||||
|
|
||||||
|
// Scroll-reveal
|
||||||
|
let io: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
function initReveal(): void {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||||
|
io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
e.target.classList.add('in')
|
||||||
|
io?.unobserve(e.target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { initReveal() })
|
||||||
|
onUnmounted(() => { io?.disconnect() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- PAGE HEADER -->
|
||||||
<!-- Header -->
|
<section class="hero" style="padding-bottom:0; border-bottom:none;">
|
||||||
<section class="pt-20 pb-12">
|
<div class="hero__atmo" />
|
||||||
<div class="max-w-4xl mx-auto px-6 text-center">
|
<div class="hero__grid" />
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">How Corrosion Works</h1>
|
<div class="hero__grain" />
|
||||||
<p class="text-lg text-neutral-400">
|
<div class="wrap hero__in" style="padding-bottom:52px;">
|
||||||
Corrosion connects your Rust server to a hosted control plane — securely, outbound-only.
|
<div class="hero__mark">
|
||||||
|
<CorrosionMark :size="56" />
|
||||||
|
</div>
|
||||||
|
<span class="eyebrow">How it works</span>
|
||||||
|
<h1 style="font-size:var(--text-5xl)">
|
||||||
|
One agent.
|
||||||
|
<span class="accent">Every game. No SSH.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero__sub">
|
||||||
|
Install the host agent once on your Windows or Linux machine. Corrosion connects
|
||||||
|
securely, outbound-only. You manage every game instance from the browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- THE MODEL: 3-STEP OVERVIEW -->
|
||||||
|
<section class="sec" id="model">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec__head reveal">
|
||||||
|
<span class="eyebrow">The agent model</span>
|
||||||
|
<h2 class="title">Bring your own server.<br>We provide the control plane.</h2>
|
||||||
|
<p class="lead">
|
||||||
|
Corrosion is not a hosting provider. You supply the hardware or the VPS. The host
|
||||||
|
agent runs on that machine and bridges your game instances to Corrosion's control
|
||||||
|
plane — securely, without opening inbound firewall ports.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="steps reveal">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__n">1</div>
|
||||||
|
<b>Install the host agent</b>
|
||||||
|
<p>
|
||||||
|
Download the Corrosion agent binary from your dashboard. Run it on any Windows
|
||||||
|
or Linux host. One agent per machine — it manages every game instance you assign
|
||||||
|
to it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__n">2</div>
|
||||||
|
<b>It connects to Corrosion</b>
|
||||||
|
<p>
|
||||||
|
The agent makes a single outbound NATS connection to Corrosion's cloud. No
|
||||||
|
inbound ports. No open panels. No SSH required after initial setup.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step__n">3</div>
|
||||||
|
<b>Deploy and manage from the browser</b>
|
||||||
|
<p>
|
||||||
|
Create game instances, run wipes, manage plugins, schedule maintenance, and
|
||||||
|
monitor players — all from the Corrosion panel at panel.corrosionmgmt.com.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nots reveal">
|
||||||
|
<span><Icon name="check" :size="16" style="color:var(--status-online)" />No open inbound ports</span>
|
||||||
|
<span><Icon name="check" :size="16" style="color:var(--status-online)" />No permanent SSH sessions</span>
|
||||||
|
<span><Icon name="check" :size="16" style="color:var(--status-online)" />No config file spelunking</span>
|
||||||
|
<span><Icon name="check" :size="16" style="color:var(--status-online)" />Windows and Linux supported</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Steps -->
|
<!-- MULTI-GAME RUNTIME -->
|
||||||
<section class="pb-20">
|
<section class="sec" id="multi-game">
|
||||||
<div class="max-w-3xl mx-auto px-6">
|
<div class="wrap">
|
||||||
<div class="space-y-2">
|
<div class="sec__head reveal">
|
||||||
<!-- Step 1 -->
|
<span class="eyebrow">Multi-game host runtime</span>
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8">
|
<h2 class="title">One agent. Multiple game worlds<br>on the same machine.</h2>
|
||||||
<div class="flex items-start gap-6">
|
<p class="lead">
|
||||||
<div class="w-12 h-12 bg-oxide-500/10 border border-oxide-500/20 rounded-xl flex items-center justify-center shrink-0">
|
The host agent is not a per-game process. It is a general-purpose ops runtime. One
|
||||||
<Download class="w-6 h-6 text-oxide-500" />
|
agent on a single machine can supervise multiple game server processes across
|
||||||
</div>
|
different games — each with its own configuration, lifecycle, and wipe schedule.
|
||||||
<div>
|
</p>
|
||||||
<div class="flex items-center gap-3 mb-2">
|
</div>
|
||||||
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 1</span>
|
|
||||||
</div>
|
<div class="blueprints reveal">
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Install the Plugin</h3>
|
<div class="bp" data-game="rust">
|
||||||
<p class="text-neutral-400">
|
<div class="bp__head">
|
||||||
Drop the Corrosion plugin into <code class="px-2 py-0.5 bg-neutral-800 rounded text-oxide-300 text-sm">oxide/plugins</code>.
|
<span class="bp__ic"><Icon name="box" :size="21" /></span>
|
||||||
That's it. No dependencies, no config files to create.
|
<div>
|
||||||
</p>
|
<div class="bp__name">Rust</div>
|
||||||
</div>
|
<div class="bp__accent">Oxide Orange</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bp__role">uMod / Oxide plugin ecosystem</div>
|
||||||
<div class="flex justify-center py-1">
|
<div class="bp__list">
|
||||||
<ArrowDown class="w-5 h-5 text-neutral-700" />
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Map / BP / full wipe scheduling</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Plugin config profiles from the browser</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Wipe-day backup before every change</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Step 2 -->
|
<div class="bp" data-game="dune">
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8">
|
<div class="bp__head">
|
||||||
<div class="flex items-start gap-6">
|
<span class="bp__ic"><Icon name="sun" :size="21" /></span>
|
||||||
<div class="w-12 h-12 bg-oxide-500/10 border border-oxide-500/20 rounded-xl flex items-center justify-center shrink-0">
|
<div>
|
||||||
<Globe class="w-6 h-6 text-oxide-500" />
|
<div class="bp__name">Dune: Awakening</div>
|
||||||
</div>
|
<div class="bp__accent">Spice Amber</div>
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 2</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Register Online</h3>
|
|
||||||
<p class="text-neutral-400">
|
|
||||||
Activate your license and configure your server. Set your hostname, game port, and admin preferences.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bp__role">Battlegroup orchestration</div>
|
||||||
<div class="flex justify-center py-1">
|
<div class="bp__list">
|
||||||
<ArrowDown class="w-5 h-5 text-neutral-700" />
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Deep Desert wipe scheduling</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Procedural map regeneration</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Battlegroup lifecycle controls</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Step 3 -->
|
<div class="bp" data-game="soulmask">
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8">
|
<div class="bp__head">
|
||||||
<div class="flex items-start gap-6">
|
<span class="bp__ic"><Icon name="drama" :size="21" /></span>
|
||||||
<div class="w-12 h-12 bg-oxide-500/10 border border-oxide-500/20 rounded-xl flex items-center justify-center shrink-0">
|
<div>
|
||||||
<Wifi class="w-6 h-6 text-oxide-500" />
|
<div class="bp__name">Soulmask</div>
|
||||||
</div>
|
<div class="bp__accent">Ritual Jade</div>
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-3 mb-2">
|
|
||||||
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 3</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Secure Outbound Connection</h3>
|
|
||||||
<p class="text-neutral-400">
|
|
||||||
Your server connects securely to Corrosion's NATS cluster. No inbound firewall rules required. Your server initiates all connections.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bp__role">Linked-world cluster deployment</div>
|
||||||
<div class="flex justify-center py-1">
|
<div class="bp__list">
|
||||||
<ArrowDown class="w-5 h-5 text-neutral-700" />
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />One-click linked-server clusters</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Port and config automation</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Cluster health monitoring</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bp" data-game="conan">
|
||||||
|
<div class="bp__head">
|
||||||
|
<span class="bp__ic"><Icon name="swords" :size="21" /></span>
|
||||||
|
<div>
|
||||||
|
<div class="bp__name">Conan Exiles</div>
|
||||||
|
<div class="bp__accent">Hyborian Bronze</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bp__role">Persistent world management</div>
|
||||||
|
<div class="bp__list">
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Mod management + world backups</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Purge, decay, and event tracking</div>
|
||||||
|
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Scheduled maintenance workflows</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Step 4 -->
|
<!-- WIPE AND MAINTENANCE AUTOMATION -->
|
||||||
<div class="bg-neutral-900 border border-oxide-500/30 rounded-xl p-8">
|
<section class="sec" id="automation">
|
||||||
<div class="flex items-start gap-6">
|
<div class="wrap">
|
||||||
<div class="w-12 h-12 bg-oxide-500/15 border border-oxide-500/30 rounded-xl flex items-center justify-center shrink-0">
|
<div class="sec__head reveal">
|
||||||
<LayoutDashboard class="w-6 h-6 text-oxide-400" />
|
<span class="eyebrow">Wipe and maintenance automation</span>
|
||||||
</div>
|
<h2 class="title">Self-service workflows,<br>not manual processes</h2>
|
||||||
<div>
|
<p class="lead">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
Every wipe, update, and maintenance window runs as a verified, logged sequence.
|
||||||
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 4</span>
|
Pre-warning announcements, pre-wipe backups, health checks after restart, and
|
||||||
</div>
|
rollback capability when things go wrong.
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Full Orchestration</h3>
|
</p>
|
||||||
<p class="text-neutral-400 mb-4">From the dashboard, you can:</p>
|
</div>
|
||||||
<ul class="space-y-2">
|
|
||||||
<li class="text-neutral-300 text-sm flex items-center gap-2">
|
<div class="pipe reveal">
|
||||||
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" />
|
<span class="pchip">Pre-warning</span>
|
||||||
Execute console commands
|
<Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
|
||||||
</li>
|
<span class="pchip">Backup</span>
|
||||||
<li class="text-neutral-300 text-sm flex items-center gap-2">
|
<Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
|
||||||
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" />
|
<span class="pchip">Stop services</span>
|
||||||
Configure plugins
|
<Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
|
||||||
</li>
|
<span class="pchip">Update / rotate</span>
|
||||||
<li class="text-neutral-300 text-sm flex items-center gap-2">
|
<Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
|
||||||
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" />
|
<span class="pchip">Restart</span>
|
||||||
Schedule wipes
|
<Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
|
||||||
</li>
|
<span class="pchip">Health check</span>
|
||||||
<li class="text-neutral-300 text-sm flex items-center gap-2">
|
<Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
|
||||||
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" />
|
<span class="pchip pchip--last">Announce complete</span>
|
||||||
Monitor performance
|
</div>
|
||||||
</li>
|
|
||||||
<li class="text-neutral-300 text-sm flex items-center gap-2">
|
<div class="stack-lines reveal">
|
||||||
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" />
|
<span>Every operation is logged. Every step is verified.</span>
|
||||||
Automate Steam updates
|
<span class="hi">Rollback is one click away when supported.</span>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
<!-- ARCHITECTURE: HOW CONNECTIVITY WORKS -->
|
||||||
|
<section class="sec" id="connectivity">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec__head reveal">
|
||||||
|
<span class="eyebrow">Connectivity model</span>
|
||||||
|
<h2 class="title">Outbound-only. No exposed panel.</h2>
|
||||||
|
<p class="lead">
|
||||||
|
The host agent establishes one secure NATS connection to Corrosion's cloud. All
|
||||||
|
commands flow through that channel. Your machine never needs to accept inbound
|
||||||
|
connections from the internet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="infra reveal">
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="server" :size="16" /></div>
|
||||||
|
<b>Your host machine</b>
|
||||||
|
<p>Windows or Linux. Bare metal, VPS, or dedicated. You own it and run it.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="cpu" :size="16" /></div>
|
||||||
|
<b>Corrosion agent</b>
|
||||||
|
<p>A single Go binary. Runs as a service. Manages game processes, files, and updates.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="zap" :size="16" /></div>
|
||||||
|
<b>Outbound NATS channel</b>
|
||||||
|
<p>One secure, outbound-only connection. No open ports. No SSH tunnels.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="shield" :size="16" /></div>
|
||||||
|
<b>Corrosion cloud</b>
|
||||||
|
<p>Hosted control plane. Multi-tenant isolation. Every command is license-scoped.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="layout-dashboard" :size="16" /></div>
|
||||||
|
<b>Your browser</b>
|
||||||
|
<p>The panel at panel.corrosionmgmt.com. Console, wipes, plugins, schedules — all here.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="techrow reveal">
|
||||||
|
<span>Go host agent</span>
|
||||||
|
<span>NATS JetStream</span>
|
||||||
|
<span>NestJS API</span>
|
||||||
|
<span>PostgreSQL</span>
|
||||||
|
<span>Outbound-only connectivity</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- HOST REQUIREMENTS -->
|
||||||
|
<section class="sec" id="requirements">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec__head reveal">
|
||||||
|
<span class="eyebrow">Host requirements</span>
|
||||||
|
<h2 class="title">What you need to get started</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caps reveal" style="max-width:760px">
|
||||||
|
<div class="caps__col">
|
||||||
|
<span class="eyebrow">Your machine</span>
|
||||||
|
<div class="feat">
|
||||||
|
<span class="feat__ic"><Icon name="server" :size="16" /></span>
|
||||||
|
<div>
|
||||||
|
<b>Windows or Linux host</b>
|
||||||
|
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
|
||||||
|
VPS, dedicated server, or bare metal. You supply it; Corrosion manages it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feat">
|
||||||
|
<span class="feat__ic"><Icon name="hard-drive" :size="16" /></span>
|
||||||
|
<div>
|
||||||
|
<b>Enough CPU and RAM for your game</b>
|
||||||
|
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
|
||||||
|
Corrosion's agent is lightweight. Your game server determines the actual
|
||||||
|
hardware requirement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feat">
|
||||||
|
<span class="feat__ic"><Icon name="wifi" :size="16" /></span>
|
||||||
|
<div>
|
||||||
|
<b>Outbound internet access</b>
|
||||||
|
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
|
||||||
|
The agent connects out; your game server's player ports stay open as they
|
||||||
|
always have been.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caps__col">
|
||||||
|
<span class="eyebrow">From Corrosion</span>
|
||||||
|
<div class="feat">
|
||||||
|
<span class="feat__ic"><Icon name="download" :size="16" /></span>
|
||||||
|
<div>
|
||||||
|
<b>Agent binary (Windows or Linux)</b>
|
||||||
|
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
|
||||||
|
Downloaded from your dashboard. No manual build. No dependency management.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feat">
|
||||||
|
<span class="feat__ic"><Icon name="key" :size="16" /></span>
|
||||||
|
<div>
|
||||||
|
<b>Your license key</b>
|
||||||
|
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
|
||||||
|
Issued when you register. The agent uses it to authenticate to the cloud.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="feat">
|
||||||
|
<span class="feat__ic"><Icon name="globe" :size="16" /></span>
|
||||||
|
<div>
|
||||||
|
<b>The panel</b>
|
||||||
|
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
|
||||||
|
Everything else — console, wipes, schedules, players — lives at
|
||||||
|
panel.corrosionmgmt.com.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Architecture Diagram -->
|
<!-- FINAL CTA -->
|
||||||
<section class="py-20 bg-neutral-900/50 border-t border-neutral-800">
|
<section class="finalcta">
|
||||||
<div class="max-w-3xl mx-auto px-6 text-center">
|
<div class="finalcta__atmo" />
|
||||||
<h2 class="text-2xl font-bold text-neutral-100 mb-10">Architecture Overview</h2>
|
<div class="wrap finalcta__in reveal">
|
||||||
<div class="space-y-3">
|
<h2>Install the agent.<br>Never SSH again.</h2>
|
||||||
<div class="inline-block px-6 py-3 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 font-medium">Rust Server</div>
|
<div class="cta-row">
|
||||||
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div>
|
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
|
||||||
<div class="inline-block px-6 py-3 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 font-medium">Corrosion Plugin</div>
|
Join early access
|
||||||
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div>
|
</RouterLink>
|
||||||
<div class="inline-block px-6 py-3 bg-oxide-500/10 border border-oxide-500/30 rounded-lg text-oxide-400 font-medium">Secure NATS Messaging</div>
|
<a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
|
||||||
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div>
|
<Icon name="play" :size="17" />View live demo
|
||||||
<div class="inline-block px-6 py-3 bg-oxide-500/10 border border-oxide-500/30 rounded-lg text-oxide-400 font-medium">Corrosion Cloud</div>
|
</a>
|
||||||
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div>
|
|
||||||
<div class="inline-block px-6 py-3 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 font-medium">Dashboard + Store + Analytics</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-neutral-500 mt-10">
|
|
||||||
Corrosion does not proxy gameplay traffic. It orchestrates operations.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ const mockActiveGame = activeGame
|
|||||||
<span class="g"><Icon name="box" :size="13" /></span>
|
<span class="g"><Icon name="box" :size="13" /></span>
|
||||||
<span class="nm">
|
<span class="nm">
|
||||||
Main · 2x Vanilla
|
Main · 2x Vanilla
|
||||||
<small>asgard-01 · rust</small>
|
<small>rust-host · rust</small>
|
||||||
</span>
|
</span>
|
||||||
<span class="st"><b />online</span>
|
<span class="st"><b />online</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +193,7 @@ const mockActiveGame = activeGame
|
|||||||
<span class="g"><Icon name="sun" :size="13" /></span>
|
<span class="g"><Icon name="sun" :size="13" /></span>
|
||||||
<span class="nm">
|
<span class="nm">
|
||||||
Arrakis · Hardcore
|
Arrakis · Hardcore
|
||||||
<small>asgard-01 · dune</small>
|
<small>dune-host · dune</small>
|
||||||
</span>
|
</span>
|
||||||
<span class="st"><b />online</span>
|
<span class="st"><b />online</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +201,7 @@ const mockActiveGame = activeGame
|
|||||||
<span class="g"><Icon name="swords" :size="13" /></span>
|
<span class="g"><Icon name="swords" :size="13" /></span>
|
||||||
<span class="nm">
|
<span class="nm">
|
||||||
Exiled Lands · PvP-C
|
Exiled Lands · PvP-C
|
||||||
<small>asgard-02 · conan</small>
|
<small>conan-host · conan</small>
|
||||||
</span>
|
</span>
|
||||||
<span class="st"><b />online</span>
|
<span class="st"><b />online</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,129 +1,429 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { Check } from 'lucide-vue-next'
|
import Icon from '@/components/ds/core/Icon.vue'
|
||||||
|
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
|
||||||
|
|
||||||
|
const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
|
||||||
|
|
||||||
|
// Scroll-reveal
|
||||||
|
let io: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
function initReveal(): void {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||||
|
io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
e.target.classList.add('in')
|
||||||
|
io?.unobserve(e.target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { initReveal() })
|
||||||
|
onUnmounted(() => { io?.disconnect() })
|
||||||
|
|
||||||
|
interface PlanFeature {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Plan {
|
||||||
|
name: string
|
||||||
|
price: string
|
||||||
|
period: string
|
||||||
|
scope: string
|
||||||
|
tag: string
|
||||||
|
featured: boolean
|
||||||
|
cta: string
|
||||||
|
ctaVariant: 'primary' | 'ghost'
|
||||||
|
features: PlanFeature[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const plans: Plan[] = [
|
||||||
|
{
|
||||||
|
name: 'Hobby',
|
||||||
|
price: '$9.99',
|
||||||
|
period: '/mo',
|
||||||
|
scope: '1–5 servers · non-commercial use',
|
||||||
|
tag: '',
|
||||||
|
featured: false,
|
||||||
|
cta: 'Join early access',
|
||||||
|
ctaVariant: 'ghost',
|
||||||
|
features: [
|
||||||
|
{ text: 'Up to 5 game server instances' },
|
||||||
|
{ text: 'Non-commercial servers only' },
|
||||||
|
{ text: 'Auto-wiper with rollback' },
|
||||||
|
{ text: 'Plugin management (Rust)' },
|
||||||
|
{ text: 'File manager + real-time console' },
|
||||||
|
{ text: 'Scheduled tasks' },
|
||||||
|
{ text: 'Public server page' },
|
||||||
|
{ text: 'Community support' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Community',
|
||||||
|
price: '$19.99',
|
||||||
|
period: '/mo',
|
||||||
|
scope: '6–10 servers · non-commercial use',
|
||||||
|
tag: '',
|
||||||
|
featured: false,
|
||||||
|
cta: 'Join early access',
|
||||||
|
ctaVariant: 'ghost',
|
||||||
|
features: [
|
||||||
|
{ text: 'Up to 10 game server instances' },
|
||||||
|
{ text: 'Non-commercial servers only' },
|
||||||
|
{ text: 'Auto-wiper with rollback' },
|
||||||
|
{ text: 'Plugin management (Rust)' },
|
||||||
|
{ text: 'File manager + real-time console' },
|
||||||
|
{ text: 'Scheduled tasks' },
|
||||||
|
{ text: 'Public server page' },
|
||||||
|
{ text: 'Community support' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Operator',
|
||||||
|
price: '$99.99',
|
||||||
|
period: '/mo',
|
||||||
|
scope: 'Commercial use · or 11–50 servers',
|
||||||
|
tag: 'Most popular',
|
||||||
|
featured: true,
|
||||||
|
cta: 'Get Operator',
|
||||||
|
ctaVariant: 'primary',
|
||||||
|
features: [
|
||||||
|
{ text: 'Up to 50 game server instances' },
|
||||||
|
{ text: 'Commercial use permitted' },
|
||||||
|
{ text: 'All games: Rust, Dune, Soulmask, Conan' },
|
||||||
|
{ text: 'Auto-wiper with rollback' },
|
||||||
|
{ text: 'Plugin + mod management' },
|
||||||
|
{ text: 'File manager + real-time console' },
|
||||||
|
{ text: 'Scheduled tasks + maintenance windows' },
|
||||||
|
{ text: 'Player management + RBAC team access' },
|
||||||
|
{ text: 'Public server page + storefront' },
|
||||||
|
{ text: 'Community support + priority bug triage' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
price: '$99.99',
|
||||||
|
period: '/mo',
|
||||||
|
scope: '50+ servers · hosting partners + fleets',
|
||||||
|
tag: '',
|
||||||
|
featured: false,
|
||||||
|
cta: 'Join early access',
|
||||||
|
ctaVariant: 'ghost',
|
||||||
|
features: [
|
||||||
|
{ text: '50 servers base included' },
|
||||||
|
{ text: 'Fleet Blocks: +$49.99/mo per 50 servers' },
|
||||||
|
{ text: 'Commercial use permitted' },
|
||||||
|
{ text: 'All games + multi-game hosts' },
|
||||||
|
{ text: 'Full Operator feature set' },
|
||||||
|
{ text: 'Fleet-level management' },
|
||||||
|
{ text: 'Priority bug triage for platform issues' },
|
||||||
|
{ text: 'Community support' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- PAGE HEADER -->
|
||||||
<!-- Header -->
|
<section class="hero" style="padding-bottom:0; border-bottom:none;">
|
||||||
<section class="pt-20 pb-12">
|
<div class="hero__atmo" />
|
||||||
<div class="max-w-4xl mx-auto px-6 text-center">
|
<div class="hero__grid" />
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">Pricing</h1>
|
<div class="hero__grain" />
|
||||||
<p class="text-lg text-neutral-400">Simple. Transparent. No hidden tiers.</p>
|
<div class="wrap hero__in" style="padding-bottom: 52px;">
|
||||||
|
<div class="hero__mark">
|
||||||
|
<CorrosionMark :size="56" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<span class="eyebrow">Pricing</span>
|
||||||
|
<h1 style="font-size:var(--text-5xl)">
|
||||||
|
Scale from one server
|
||||||
|
<span class="accent">to a fleet.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero__sub">
|
||||||
|
Simple tiers based on how many servers you run and whether you operate commercially.
|
||||||
|
No per-seat charges. No surprises.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Pricing Cards -->
|
<!-- PRICING CARDS -->
|
||||||
<section class="pb-20">
|
<section class="sec" id="plans">
|
||||||
<div class="max-w-5xl mx-auto px-6">
|
<div class="wrap">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="pricing reveal">
|
||||||
<!-- Base License -->
|
<div
|
||||||
<div class="bg-neutral-900 border-2 border-oxide-500/40 rounded-xl p-8 relative">
|
v-for="plan in plans"
|
||||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
|
:key="plan.name"
|
||||||
<span class="px-3 py-1 bg-oxide-600 text-white text-xs font-semibold rounded-full uppercase tracking-wider">Most Popular</span>
|
class="plan"
|
||||||
</div>
|
:class="plan.featured ? 'plan--feature' : ''"
|
||||||
<div class="text-center mb-6">
|
>
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Base License</h3>
|
<div class="plan__tag">{{ plan.tag }}</div>
|
||||||
<div class="flex items-baseline justify-center gap-1">
|
<div class="plan__name">{{ plan.name }}</div>
|
||||||
<span class="text-4xl font-bold text-oxide-400">$50</span>
|
<div class="plan__price">
|
||||||
</div>
|
{{ plan.price }}<small>{{ plan.period }}</small>
|
||||||
<p class="text-sm text-neutral-500 mt-1">One server. Lifetime access.</p>
|
|
||||||
<p class="text-xs text-oxide-400/70 mt-1">Launch Price</p>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-3 mb-8">
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Full control plane
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Auto-Wiper with rollback
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Plugin management
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Public server site
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Multi-admin RBAC
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<RouterLink to="/register" class="block w-full py-3 bg-oxide-600 hover:bg-oxide-700 text-white text-center font-semibold rounded-lg transition-colors">
|
|
||||||
Get Started
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="plan__scope">{{ plan.scope }}</div>
|
||||||
|
|
||||||
<!-- Webstore Add-On -->
|
<ul class="plan__feats">
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8">
|
<li v-for="feat in plan.features" :key="feat.text">
|
||||||
<div class="text-center mb-6">
|
<Icon name="check" :size="13" style="color:var(--accent-text);flex:none" />
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Webstore Add-On</h3>
|
{{ feat.text }}
|
||||||
<div class="flex items-baseline justify-center gap-1">
|
</li>
|
||||||
<span class="text-4xl font-bold text-neutral-200">$10</span>
|
</ul>
|
||||||
<span class="text-neutral-500">/mo</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-neutral-500 mt-1">Integrated monetization platform.</p>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-3 mb-8">
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Item catalog
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Stripe + PayPal
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Auto-delivery via RCON
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Transaction history
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<button class="block w-full py-3 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 text-center font-semibold rounded-lg border border-neutral-700 transition-colors">
|
|
||||||
Coming Soon
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modules -->
|
<RouterLink
|
||||||
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8">
|
class="btn"
|
||||||
<div class="text-center mb-6">
|
:class="plan.ctaVariant === 'primary' ? 'btn--primary' : 'btn--ghost'"
|
||||||
<h3 class="text-xl font-bold text-neutral-100 mb-2">Modules</h3>
|
:to="{ name: 'early-access' }"
|
||||||
<div class="flex items-baseline justify-center gap-1">
|
>
|
||||||
<span class="text-4xl font-bold text-neutral-200">$9.99</span>
|
{{ plan.cta }}
|
||||||
<span class="text-neutral-500">+</span>
|
</RouterLink>
|
||||||
</div>
|
|
||||||
<p class="text-sm text-neutral-500 mt-1">Optional feature expansions.</p>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-3 mb-8">
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Analytics & insights
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Discord bot integration
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
Cloud backups
|
|
||||||
</li>
|
|
||||||
<li class="flex items-center gap-3 text-sm text-neutral-300">
|
|
||||||
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
|
|
||||||
More on the roadmap
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<RouterLink to="/site/roadmap" class="block w-full py-3 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 text-center font-semibold rounded-lg border border-neutral-700 transition-colors">
|
|
||||||
View Roadmap
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</div>
|
<!-- Fleet Block -->
|
||||||
|
<div class="fleetblock reveal">
|
||||||
|
<b>Fleet Block</b>
|
||||||
|
<span class="p">+$49.99/mo</span>
|
||||||
|
<span>each additional 50 servers — stack as many as your network needs.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Commercial use definition -->
|
||||||
|
<p class="commercial reveal">
|
||||||
|
<b>Commercial use</b> includes monetized communities, paid access, VIP slots, donations,
|
||||||
|
sponsorship-supported servers, hosting providers, or managing servers for others.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Support model -->
|
||||||
|
<p class="support-note reveal">
|
||||||
|
Community support is included with every plan — documentation, community forum,
|
||||||
|
diagnostics, and structured bug reports.
|
||||||
|
<b>Direct 1:1 support</b> is available separately at $125/hour in prepaid 1-hour blocks.
|
||||||
|
Corrosion is a tool, not a managed service.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- COMPARISON TABLE -->
|
||||||
|
<section class="sec" id="compare">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec__head reveal">
|
||||||
|
<span class="eyebrow">Feature breakdown</span>
|
||||||
|
<h2 class="title">What is included in each tier</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="compare-table reveal">
|
||||||
|
<div class="compare-table__head">
|
||||||
|
<div class="compare-table__label">Feature</div>
|
||||||
|
<div>Hobby</div>
|
||||||
|
<div>Community</div>
|
||||||
|
<div class="compare-table__featured">Operator</div>
|
||||||
|
<div>Network</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Server instances</div>
|
||||||
|
<div>1–5</div>
|
||||||
|
<div>6–10</div>
|
||||||
|
<div class="compare-table__featured">Up to 50</div>
|
||||||
|
<div>50 + Fleet Blocks</div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Commercial use</div>
|
||||||
|
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
|
||||||
|
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Rust (Oxide/uMod)</div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">All supported games</div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Auto-wiper + rollback</div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Real-time console</div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">RBAC team access</div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Public server page</div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
<div class="compare-table__row">
|
||||||
|
<div class="compare-table__label">Priority bug triage</div>
|
||||||
|
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
|
||||||
|
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
|
||||||
|
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SUPPORT MODEL -->
|
||||||
|
<section class="sec" id="support">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec__head reveal">
|
||||||
|
<span class="eyebrow">Support model</span>
|
||||||
|
<h2 class="title">Corrosion is a tool,<br>not a managed service</h2>
|
||||||
|
<p class="lead">
|
||||||
|
Every plan includes self-service support. Hands-on time is available separately — at
|
||||||
|
an honest rate, when you actually need it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="infra reveal">
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="file-text" :size="16" /></div>
|
||||||
|
<b>Documentation</b>
|
||||||
|
<p>Setup guides, architecture reference, troubleshooting walkthroughs. Included on every plan.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="message-square" :size="16" /></div>
|
||||||
|
<b>Community forum</b>
|
||||||
|
<p>Operator-to-operator knowledge base. Questions, configs, and war stories. All plans.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="activity" :size="16" /></div>
|
||||||
|
<b>Diagnostics</b>
|
||||||
|
<p>Built-in agent health checks, log access, and structured bug reports. All plans.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="zap" :size="16" /></div>
|
||||||
|
<b>Priority bug triage</b>
|
||||||
|
<p>Platform bugs for Operator and Network customers go to the front of the queue.</p>
|
||||||
|
</div>
|
||||||
|
<div class="icard">
|
||||||
|
<div class="icard__ic"><Icon name="clock" :size="16" /></div>
|
||||||
|
<b>Direct 1:1 support</b>
|
||||||
|
<p>$125/hour, prepaid in 1-hour blocks. Available to any customer who needs it.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="closing reveal" style="font-size:var(--text-md);color:var(--text-tertiary);font-weight:500">
|
||||||
|
Direct server administration, firewall configuration, mod installation, and wipe-day
|
||||||
|
hand-holding are not included in any plan. Corrosion gives you the panel and the tools.
|
||||||
|
You run the operation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FINAL CTA -->
|
||||||
|
<section class="finalcta">
|
||||||
|
<div class="finalcta__atmo" />
|
||||||
|
<div class="wrap finalcta__in reveal">
|
||||||
|
<h2>Ready to stop babysitting<br>your servers?</h2>
|
||||||
|
<div class="cta-row">
|
||||||
|
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
|
||||||
|
Join early access
|
||||||
|
</RouterLink>
|
||||||
|
<a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
|
||||||
|
<Icon name="play" :size="17" />View live demo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.plan__feats {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 16px 0 0;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
.plan__feats li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.plan__feats li svg { margin-top: 1px; }
|
||||||
|
|
||||||
|
/* Comparison table */
|
||||||
|
.compare-table {
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
}
|
||||||
|
.compare-table__head,
|
||||||
|
.compare-table__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr 1.2fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.compare-table__head {
|
||||||
|
background: var(--surface-raised-2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--tracking-caps);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
.compare-table__head > div,
|
||||||
|
.compare-table__row > div {
|
||||||
|
padding: 2px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.compare-table__head > div:first-child,
|
||||||
|
.compare-table__row > div:first-child {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.compare-table__row {
|
||||||
|
background: var(--surface-base);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
padding: 11px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
.compare-table__row:last-child { border-bottom: none; }
|
||||||
|
.compare-table__label { color: var(--text-secondary); font-weight: 500; }
|
||||||
|
.compare-table__featured {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-text) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,146 +1,353 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Check, Circle } from 'lucide-vue-next'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import Icon from '@/components/ds/core/Icon.vue'
|
||||||
|
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
|
||||||
|
|
||||||
interface Phase {
|
const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
|
||||||
name: string
|
|
||||||
label: string
|
type Status = 'shipped' | 'in-progress' | 'planned'
|
||||||
status: 'complete' | 'current' | 'upcoming'
|
|
||||||
items: { text: string; done: boolean }[]
|
interface RoadmapItem {
|
||||||
|
text: string
|
||||||
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const phases: Phase[] = [
|
interface RoadmapGroup {
|
||||||
|
status: Status
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
items: RoadmapItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: RoadmapGroup[] = [
|
||||||
{
|
{
|
||||||
name: 'Phase 1',
|
status: 'shipped',
|
||||||
label: 'Foundation',
|
label: 'Phase 1 — Foundation',
|
||||||
status: 'complete',
|
description:
|
||||||
|
'The core control plane is live. Rust server operators can install the agent, connect their server, and manage it entirely from the panel.',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Core control plane', done: true },
|
{ text: 'Host agent (Windows + Linux)', note: 'Go binary, outbound NATS, zero inbound ports' },
|
||||||
{ text: 'Auto-Wiper with rollback', done: true },
|
{ text: 'Core control plane', note: 'NestJS API, multi-tenant PostgreSQL, JWT RBAC' },
|
||||||
{ text: 'Plugin management', done: true },
|
{ text: 'Auto-wiper with rollback', note: 'Map, BP, and full wipes as verified sequences' },
|
||||||
{ text: 'Public server site', done: true },
|
{ text: 'Plugin management', note: 'Rust uMod/Oxide — browse, install, configure from the browser' },
|
||||||
|
{ text: 'Real-time console', note: 'NATS-bridged live output' },
|
||||||
|
{ text: 'File manager', note: 'Browser-based file access via the agent' },
|
||||||
|
{ text: 'Scheduled tasks and maintenance windows' },
|
||||||
|
{ text: 'Player management and RBAC team access' },
|
||||||
|
{ text: 'Public server page', note: 'Live status, wipe countdown, player count' },
|
||||||
|
{ text: 'SteamCMD game update automation' },
|
||||||
|
{ text: 'Discord and notification webhooks' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Phase 2',
|
status: 'in-progress',
|
||||||
label: 'Analytics',
|
label: 'Multi-game expansion',
|
||||||
status: 'current',
|
description:
|
||||||
|
'The agent and control plane are being extended with per-game blueprints. Rust is the reference implementation; Dune, Conan, and Soulmask follow the same agent model with game-specific operational logic.',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Player retention tracking', done: false },
|
{ text: 'Dune: Awakening blueprint', note: 'Deep Desert wipe scheduling, battlegroup lifecycle' },
|
||||||
{ text: 'Wipe performance insights', done: false },
|
{ text: 'Conan Exiles blueprint', note: 'Persistent world management, mod support, purge tracking' },
|
||||||
{ text: 'Population heatmaps', done: false },
|
{ text: 'Soulmask blueprint', note: 'Linked-world cluster deployment, port automation' },
|
||||||
|
{ text: 'Multi-instance host runtime', note: 'One agent managing N game processes on the same machine' },
|
||||||
|
{ text: 'Per-game wipe and event scheduling' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Phase 3',
|
status: 'planned',
|
||||||
label: 'Status Platform',
|
label: 'API access and integrations',
|
||||||
status: 'upcoming',
|
description:
|
||||||
|
'Operator-grade API access so you can build your own tooling on top of the Corrosion control plane.',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Public uptime tracking', done: false },
|
{ text: 'Public REST API for server management' },
|
||||||
{ text: 'Server health dashboard', done: false },
|
{ text: 'Webhook events (wipe completed, server down, player banned)' },
|
||||||
|
{ text: 'API key management per license' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Phase 4',
|
status: 'planned',
|
||||||
label: 'Module Marketplace',
|
label: 'Integrated storefront',
|
||||||
status: 'upcoming',
|
description:
|
||||||
|
'A native store layer for server communities — item catalog, VIP packages, and automated in-game delivery. No Tebex dependency required.',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Loot manager', done: false },
|
{ text: 'Item catalog and categories' },
|
||||||
{ text: 'Event systems', done: false },
|
{ text: 'PayPal and Stripe payment processing' },
|
||||||
{ text: 'Advanced gameplay modules', done: false },
|
{ text: 'Automated in-game delivery via RCON/agent' },
|
||||||
|
{ text: 'Transaction history and revenue dashboard' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Phase 5',
|
status: 'planned',
|
||||||
label: 'Integrated Webstore',
|
label: 'Fleet management for hosting partners',
|
||||||
status: 'upcoming',
|
description:
|
||||||
|
'Tools for hosting providers and multi-server operations running 50+ instances across multiple physical hosts.',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Native item store', done: false },
|
{ text: 'Fleet-level dashboards and health monitoring' },
|
||||||
{ text: 'Automated delivery', done: false },
|
{ text: 'Multi-host agent orchestration' },
|
||||||
{ text: 'Revenue dashboard', done: false },
|
{ text: 'Bulk wipe and update scheduling across a fleet' },
|
||||||
|
{ text: 'Fleet Block capacity management' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Phase 6',
|
status: 'planned',
|
||||||
label: 'B2B Hosting Integration',
|
label: 'More games',
|
||||||
status: 'upcoming',
|
description:
|
||||||
|
'Corrosion\'s agent model is not Rust-specific. The platform is being designed to support any game that can be managed via process control, file operations, and RCON-style commands.',
|
||||||
items: [
|
items: [
|
||||||
{ text: 'White-label panel', done: false },
|
{ text: 'Additional survival and sandbox games' },
|
||||||
{ text: 'Bulk license provisioning', done: false },
|
{ text: 'Community-requested game blueprints' },
|
||||||
{ text: 'SSO integration', done: false },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function phaseStatusClass(status: string): string {
|
function statusLabel(s: Status): string {
|
||||||
switch (status) {
|
if (s === 'shipped') return 'Shipped'
|
||||||
case 'complete': return 'bg-green-500/10 text-green-400 border-green-500/20'
|
if (s === 'in-progress') return 'In progress'
|
||||||
case 'current': return 'bg-oxide-500/10 text-oxide-400 border-oxide-500/20'
|
return 'Planned'
|
||||||
default: return 'bg-neutral-800 text-neutral-500 border-neutral-700'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function phaseStatusLabel(status: string): string {
|
function statusIcon(s: Status): string {
|
||||||
switch (status) {
|
if (s === 'shipped') return 'check'
|
||||||
case 'complete': return 'Shipped'
|
if (s === 'in-progress') return 'refresh-cw'
|
||||||
case 'current': return 'In Progress'
|
return 'circle'
|
||||||
default: return 'Planned'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll-reveal
|
||||||
|
let io: IntersectionObserver | null = null
|
||||||
|
|
||||||
|
function initReveal(): void {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||||
|
io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
e.target.classList.add('in')
|
||||||
|
io?.unobserve(e.target)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => { initReveal() })
|
||||||
|
onUnmounted(() => { io?.disconnect() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- PAGE HEADER -->
|
||||||
<!-- Header -->
|
<section class="hero" style="padding-bottom:0; border-bottom:none;">
|
||||||
<section class="pt-20 pb-12">
|
<div class="hero__atmo" />
|
||||||
<div class="max-w-4xl mx-auto px-6 text-center">
|
<div class="hero__grid" />
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">Roadmap</h1>
|
<div class="hero__grain" />
|
||||||
<p class="text-lg text-neutral-400">
|
<div class="wrap hero__in" style="padding-bottom:52px;">
|
||||||
Corrosion isn't a single plugin release. It's infrastructure for the Rust ecosystem.
|
<div class="hero__mark">
|
||||||
</p>
|
<CorrosionMark :size="56" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<span class="eyebrow">Roadmap</span>
|
||||||
|
<h1 style="font-size:var(--text-5xl)">
|
||||||
|
Where Corrosion
|
||||||
|
<span class="accent">is going.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero__sub">
|
||||||
|
No specific dates. No fabricated percentages. Status labels only: Shipped, In progress,
|
||||||
|
Planned. This roadmap reflects what is actually being built.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Timeline -->
|
<!-- STATUS LEGEND -->
|
||||||
<section class="pb-20">
|
<section class="sec" style="padding:32px 0; border-bottom: 1px solid var(--border-subtle);">
|
||||||
<div class="max-w-3xl mx-auto px-6">
|
<div class="wrap">
|
||||||
<div class="space-y-6">
|
<div class="rm-legend reveal">
|
||||||
<div
|
<div class="rm-badge rm-badge--shipped">
|
||||||
v-for="phase in phases"
|
<Icon name="check" :size="13" />Shipped
|
||||||
:key="phase.name"
|
</div>
|
||||||
class="bg-neutral-900 border rounded-xl p-8 transition-colors"
|
<div class="rm-badge rm-badge--progress">
|
||||||
:class="phase.status === 'current' ? 'border-oxide-500/30' : 'border-neutral-800'"
|
<Icon name="refresh-cw" :size="13" />In progress
|
||||||
>
|
</div>
|
||||||
<div class="flex items-center justify-between mb-5">
|
<div class="rm-badge rm-badge--planned">
|
||||||
<div>
|
<Icon name="circle" :size="13" />Planned
|
||||||
<span class="text-xs font-bold text-neutral-500 uppercase tracking-wider">{{ phase.name }}</span>
|
|
||||||
<h3 class="text-xl font-bold text-neutral-100">{{ phase.label }}</h3>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="text-xs font-semibold px-3 py-1 rounded-full border"
|
|
||||||
:class="phaseStatusClass(phase.status)"
|
|
||||||
>
|
|
||||||
{{ phaseStatusLabel(phase.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-3">
|
|
||||||
<li
|
|
||||||
v-for="item in phase.items"
|
|
||||||
:key="item.text"
|
|
||||||
class="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<Check v-if="item.done" class="w-4 h-4 text-green-400 shrink-0" />
|
|
||||||
<Circle v-else class="w-4 h-4 text-neutral-600 shrink-0" />
|
|
||||||
<span
|
|
||||||
class="text-sm"
|
|
||||||
:class="item.done ? 'text-neutral-300' : 'text-neutral-500'"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
<!-- ROADMAP GROUPS -->
|
||||||
|
<section class="sec" id="roadmap">
|
||||||
|
<div class="wrap">
|
||||||
|
<div
|
||||||
|
v-for="group in groups"
|
||||||
|
:key="group.label"
|
||||||
|
class="rm-group reveal"
|
||||||
|
:data-status="group.status"
|
||||||
|
>
|
||||||
|
<div class="rm-group__head">
|
||||||
|
<div
|
||||||
|
class="rm-group__badge"
|
||||||
|
:class="`rm-badge--${group.status}`"
|
||||||
|
>
|
||||||
|
<Icon :name="statusIcon(group.status)" :size="13" />
|
||||||
|
{{ statusLabel(group.status) }}
|
||||||
|
</div>
|
||||||
|
<h3 class="rm-group__title">{{ group.label }}</h3>
|
||||||
|
</div>
|
||||||
|
<p class="rm-group__desc">{{ group.description }}</p>
|
||||||
|
<ul class="rm-group__list">
|
||||||
|
<li
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.text"
|
||||||
|
class="rm-item"
|
||||||
|
>
|
||||||
|
<span class="rm-item__dot" :class="`rm-item__dot--${group.status}`" />
|
||||||
|
<span>
|
||||||
|
{{ item.text }}
|
||||||
|
<span v-if="item.note" class="rm-item__note">— {{ item.note }}</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- HONEST NOTE -->
|
||||||
|
<section class="sec" style="padding:40px 0; border-bottom:none;">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="closing reveal">
|
||||||
|
This roadmap reflects real development priorities, not marketing promises.
|
||||||
|
Timelines are not published because they depend on real-world testing and operator
|
||||||
|
feedback. <span class="accent">Join early access to influence what gets built next.</span>
|
||||||
|
</div>
|
||||||
|
<div class="hero__cta reveal" style="margin-top:28px">
|
||||||
|
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
|
||||||
|
Join early access
|
||||||
|
</RouterLink>
|
||||||
|
<a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
|
||||||
|
<Icon name="play" :size="17" />View live demo
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Legend */
|
||||||
|
.rm-legend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.rm-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
}
|
||||||
|
.rm-badge--shipped {
|
||||||
|
background: color-mix(in srgb, var(--status-online) 14%, transparent);
|
||||||
|
color: var(--status-online);
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--status-online) 28%, transparent);
|
||||||
|
}
|
||||||
|
.rm-badge--in-progress, .rm-badge--progress {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-text);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
}
|
||||||
|
.rm-badge--planned {
|
||||||
|
background: var(--surface-raised-2);
|
||||||
|
color: var(--text-muted);
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Groups */
|
||||||
|
.rm-group {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto 40px;
|
||||||
|
padding: 28px 28px 24px;
|
||||||
|
background: var(--surface-base);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: var(--ring-default);
|
||||||
|
}
|
||||||
|
.rm-group[data-status="in-progress"] {
|
||||||
|
box-shadow: inset 0 0 0 1px var(--accent-border);
|
||||||
|
}
|
||||||
|
.rm-group[data-status="shipped"] {
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--status-online) 22%, transparent);
|
||||||
|
}
|
||||||
|
.rm-group__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.rm-group__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.rm-group__title {
|
||||||
|
font-family: var(--font-brand);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.rm-group__desc {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 18px;
|
||||||
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
.rm-group__list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.rm-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.rm-item__dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex: none;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
.rm-item__dot--shipped { background: var(--status-online); }
|
||||||
|
.rm-item__dot--in-progress { background: var(--accent); }
|
||||||
|
.rm-item__dot--planned {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: inset 0 0 0 1.5px var(--text-muted);
|
||||||
|
}
|
||||||
|
.rm-item__note {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,18 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
|||||||
Reference in New Issue
Block a user