diff --git a/CHANGELOG.md b/CHANGELOG.md
index 805c014..4b5c3fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+### Fixed (Site Audit — Fake Data, Resilience, Fonts — 2026-06-11)
+
+**Frontend:**
+- `SetupWizardView.vue` — Replaced fake install instructions (`get.corrosionmgmt.com | sh` install script and `corrosion-agent` binary, neither of which exists) with the real host-agent download + run commands matching ServerView; multi-game copy on the completion step
+- Marketing views (Landing, Pricing, HowItWorks, Roadmap, EarlyAccess) — Replaced "View live demo" CTA (no demo exists; it linked to the panel login) with an honest "Sign in" link
+- `ErrorBoundary.vue` — Error state now resets on route change (previously one failed view bricked the entire SPA, including marketing pages, until manual reload); added `content` variant
+- `DashboardLayout.vue` — Routed views are now wrapped in a content-scoped ErrorBoundary so the sidebar/topbar survive a view failure instead of the whole panel unmounting
+- `index.html` / `styles/tokens/fonts.css` — Google Fonts moved from CSS `@import` to `` tags. The bundler silently dropped the mid-bundle `@import`, so production shipped system fallback fonts (Geist/JetBrains Mono/Oxanium never loaded)
+- `StatusPageView.vue` — Platform KPIs show "—" until the first successful fetch instead of fake zeros
+- `LoginView.vue` — Added missing "Forgot password?" link (route + backend endpoint already existed)
+
+**Backend (NestJS):**
+- `AdminSeedService` (new, auth module) — Bootstraps a super-admin user + active license from `ADMIN_EMAIL`/`ADMIN_PASSWORD`/`ADMIN_USERNAME`/`ADMIN_LICENSE_KEY` when the users table is empty. A fresh deploy previously had a schema but no possible login. Compose already passes the env vars
+
+**Purpose:** Findings from the full-site fake-data audit. Show real data or honest empty states — never invented values, dead URLs, or fabricated zeros.
+
### Fixed (Safe Formatting Utilities — 2026-02-15)
**Frontend:**
diff --git a/backend-nest/src/modules/auth/admin-seed.service.ts b/backend-nest/src/modules/auth/admin-seed.service.ts
new file mode 100644
index 0000000..391d4a4
--- /dev/null
+++ b/backend-nest/src/modules/auth/admin-seed.service.ts
@@ -0,0 +1,82 @@
+import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import * as argon2 from 'argon2';
+import { randomBytes } from 'crypto';
+import { User } from '../../entities/user.entity';
+import { License } from '../../entities/license.entity';
+
+/**
+ * Bootstraps the first admin account on a fresh database.
+ *
+ * A fresh deploy builds the schema via docker-entrypoint-initdb.d but contains
+ * zero users, so the panel has no possible login. If ADMIN_EMAIL and
+ * ADMIN_PASSWORD are set and the users table is empty, this creates a
+ * super-admin user plus an active license — the same rows the register flow
+ * would create. It never runs against a database that already has users.
+ */
+@Injectable()
+export class AdminSeedService implements OnApplicationBootstrap {
+ private readonly logger = new Logger(AdminSeedService.name);
+
+ constructor(
+ private readonly config: ConfigService,
+ @InjectRepository(User) private readonly userRepository: Repository,
+ @InjectRepository(License) private readonly licenseRepository: Repository,
+ ) {}
+
+ async onApplicationBootstrap(): Promise {
+ try {
+ await this.seedAdminIfEmpty();
+ } catch (err) {
+ // A failed seed must not take the API down — surface it loudly and move on
+ this.logger.error(`Admin bootstrap failed: ${(err as Error).message}`, (err as Error).stack);
+ }
+ }
+
+ private async seedAdminIfEmpty(): Promise {
+ const email = this.config.get('admin.email');
+ const password = this.config.get('admin.password');
+ const username = this.config.get('admin.username') || 'Commander';
+
+ if (!email || !password) {
+ this.logger.log('Admin bootstrap skipped: ADMIN_EMAIL / ADMIN_PASSWORD not set');
+ return;
+ }
+
+ const userCount = await this.userRepository.count();
+ if (userCount > 0) {
+ return;
+ }
+
+ const password_hash = await argon2.hash(password);
+ const user = this.userRepository.create({
+ email: email.toLowerCase(),
+ username,
+ password_hash,
+ email_verified: true,
+ is_super_admin: true,
+ });
+ await this.userRepository.save(user);
+
+ const licenseKey = this.config.get('admin.licenseKey') || this.generateLicenseKey();
+ const license = this.licenseRepository.create({
+ license_key: licenseKey,
+ owner_user_id: user.id,
+ status: 'active',
+ modules_enabled: [],
+ webstore_active: false,
+ });
+ await this.licenseRepository.save(license);
+
+ this.logger.log(`Bootstrap admin created: ${user.email} (license ${license.license_key})`);
+ }
+
+ private generateLicenseKey(): string {
+ const part1 = randomBytes(2).toString('hex').toUpperCase();
+ const part2 = randomBytes(2).toString('hex').toUpperCase();
+ const part3 = randomBytes(2).toString('hex').toUpperCase();
+ return `CORR-${part1}-${part2}-${part3}`;
+ }
+}
diff --git a/backend-nest/src/modules/auth/auth.module.ts b/backend-nest/src/modules/auth/auth.module.ts
index 62b8807..881648f 100644
--- a/backend-nest/src/modules/auth/auth.module.ts
+++ b/backend-nest/src/modules/auth/auth.module.ts
@@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
+import { AdminSeedService } from './admin-seed.service';
import { JwtStrategy } from './jwt.strategy';
import { User } from '../../entities/user.entity';
import { License } from '../../entities/license.entity';
@@ -27,7 +28,7 @@ import { TeamMember } from '../../entities/team-member.entity';
TypeOrmModule.forFeature([User, License, Role, TeamMember]),
],
controllers: [AuthController],
- providers: [AuthService, JwtStrategy],
+ providers: [AuthService, AdminSeedService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
diff --git a/frontend/index.html b/frontend/index.html
index 35a0aca..919a143 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -9,6 +9,14 @@
Corrosion Management
+
+
+
+
-
+
@@ -44,6 +57,11 @@ function retry() {
padding: var(--space-6);
}
+.eb-screen--content {
+ min-height: 60vh;
+ background: transparent;
+}
+
.eb-card {
background: var(--surface-base);
box-shadow: var(--ring-default), var(--shadow-md);
diff --git a/frontend/src/components/layout/DashboardLayout.vue b/frontend/src/components/layout/DashboardLayout.vue
index 23ec642..67bd669 100644
--- a/frontend/src/components/layout/DashboardLayout.vue
+++ b/frontend/src/components/layout/DashboardLayout.vue
@@ -14,6 +14,7 @@ import { useThemeGame } from '@/composables/useThemeGame'
import { useGameProfile } from '@/config/gameProfiles'
import type { NavSection, NavItemDef } from '@/config/gameProfiles'
import { safeDate } from '@/utils/formatters'
+import ErrorBoundary from '@/components/ErrorBoundary.vue'
import Logo from '@/components/ds/brand/Logo.vue'
import Badge from '@/components/ds/core/Badge.vue'
import StatusDot from '@/components/ds/core/StatusDot.vue'
@@ -284,9 +285,11 @@ const themeIcon = computed(() => theme.value === 'dark' ? 'sun' : 'moon')
-
+
-
+
+
+
diff --git a/frontend/src/styles/tokens/fonts.css b/frontend/src/styles/tokens/fonts.css
index 5fb6020..4d17a71 100644
--- a/frontend/src/styles/tokens/fonts.css
+++ b/frontend/src/styles/tokens/fonts.css
@@ -4,13 +4,14 @@
JetBrains Mono — console, data, IDs, telemetry
Oxanium — brand wordmark + marketing display (game-ops flavor)
------------------------------------------------------------
- NOTE: Loaded from Google Fonts CDN. If you want these self-
- hosted (offline), send the woff2 files and these @imports
- become @font-face rules.
+ NOTE: The Google Fonts stylesheet is loaded via tags in
+ index.html — NOT @import here. A CSS @import that ends up
+ mid-bundle after concatenation is silently dropped by the
+ optimizer (fonts never load in production). If you want these
+ self-hosted (offline), send the woff2 files and they become
+ @font-face rules here.
============================================================ */
-@import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800&family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Oxanium:wght@500;600;700;800&display=swap');
-
:root {
--font-sans: 'Geist', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, monospace;
diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue
index 5963c04..899a6fe 100644
--- a/frontend/src/views/auth/LoginView.vue
+++ b/frontend/src/views/auth/LoginView.vue
@@ -191,6 +191,8 @@ function handleBackToLogin() {
No account?
Create one
+ ·
+ Forgot password?
diff --git a/frontend/src/views/auth/SetupWizardView.vue b/frontend/src/views/auth/SetupWizardView.vue
index b23136b..490d31b 100644
--- a/frontend/src/views/auth/SetupWizardView.vue
+++ b/frontend/src/views/auth/SetupWizardView.vue
@@ -196,14 +196,17 @@ async function completeSetup() {
- The agent auto-registers with your panel. You can also use the uMod plugin for lightweight integration.
+ On Windows, download the agent from the Server page after setup. The agent connects outbound and auto-registers with your panel.
@@ -235,7 +238,7 @@ async function completeSetup() {
You're all set
-
Your server is configured. Head to the dashboard to start managing your Rust server.
+
Your server is configured. Head to the dashboard to start managing your game server.