diff --git a/Bamort_detailed_project_documentation.md b/Bamort_detailed_project_documentation.md new file mode 100644 index 0000000..9217368 --- /dev/null +++ b/Bamort_detailed_project_documentation.md @@ -0,0 +1,1087 @@ +# Bamort — Detailed Project Documentation + +> Version: 0.2.4 (backend) / 0.2.3 (frontend) +> Generated: 2026-03-13 +> Purpose: Comprehensive architecture, component, and business-logic reference + +--- + +## Table of Contents + +1. [Project Purpose and Domain](#1-project-purpose-and-domain) +2. [High-Level Architecture](#2-high-level-architecture) +3. [Technology Stack](#3-technology-stack) +4. [Backend Architecture](#4-backend-architecture) + - [Entry Point and Startup Sequence](#41-entry-point-and-startup-sequence) + - [Router and Middleware](#42-router-and-middleware) + - [Authentication System](#43-authentication-system) + - [Module Breakdown (Functional View)](#44-module-breakdown-functional-view) +5. [Frontend Architecture](#5-frontend-architecture) + - [Application Bootstrap](#51-application-bootstrap) + - [Router and Navigation Guards](#52-router-and-navigation-guards) + - [State Management (Pinia Stores)](#53-state-management-pinia-stores) + - [Views and Components (Functional View)](#54-views-and-components-functional-view) + - [API Communication Layer](#55-api-communication-layer) +6. [Core Business Logic](#6-core-business-logic) + - [Character Data Model](#61-character-data-model) + - [Derived Values Calculation](#62-derived-values-calculation) + - [Skill and Spell Learning System](#63-skill-and-spell-learning-system) + - [Character Creation Wizard](#64-character-creation-wizard) + - [PDF Export Pipeline](#65-pdf-export-pipeline) + - [Audit Log](#66-audit-log) +7. [Data Access and Persistence](#7-data-access-and-persistence) +8. [Module Interaction Map](#8-module-interaction-map) +9. [Complete API Route Reference](#9-complete-api-route-reference) +10. [Infrastructure and Deployment](#10-infrastructure-and-deployment) +11. [User Roles and Access Control](#11-user-roles-and-access-control) +12. [Internationalisation (i18n)](#12-internationalisation-i18n) +13. [Known Limitations and Security Notes](#13-known-limitations-and-security-notes) + +--- + +## 1. Project Purpose and Domain + +**Bamort** (BaMoRT = *Ba*sic Mo*R*phing *T*ool) is a web-based character management system for the **Midgard** tabletop role-playing game (M5 system). It is designed as a replacement for the older MOAM desktop application. + +**Core user-facing features:** +- Create, manage, and view RPG characters (stats, skills, spells, equipment) +- Guide new users through a multi-step character creation wizard following Midgard M5 rules +- Manage skill and spell learning progression with rule-enforced costs (EP, TE, LE, gold) +- Track experience points (EP), practice points (PP), and wealth (gold/silver/copper) +- Export characters as PDF character sheets via HTML templates +- Import/export characters as JSON or CSV for interoperability with other tools +- Share characters between users (read-only or read/write) +- Maintain game-system master data (canonical skill, spell, and equipment tables) + +**Business domain vocabulary:** +| Term | Meaning | +|------|---------| +| Char / Character | A player or NPC character in the game | +| Grad | Character level/grade | +| EP | Experience points — spent to improve skills and spells | +| TE | Training entries — attempts needed to improve a skill | +| LE | Learning entries — prerequisite step counts for spells | +| PP | Practice points — earned in combat/use, reduce learning costs | +| Fertigkeit | Skill (e.g., Schwimmen, Klettern) | +| Waffenfertigkeit | Weapon skill (subtype of Fertigkeit) | +| Zauber | Spell (magical ability) | +| Eigenschaft | Attribute (St=Strength, Gs=Dexterity, Gw=Agility, Ko=Constitution, In=Intelligence, Zt=Magical Talent, Au=Appearance, Wk=Willpower, Pa=Parry) | +| Rasse | Race (Mensch, Zwerg, Elf, etc.) | +| Typ | Character class code (Kr=Knight, Ma=Mage, Hx=Witch, etc.) | +| Lp / Ap / B | Life Points / Action Points / Load capacity | +| Bennies (Gg/Gp/Sg) | Luck tokens used in gameplay | +| Vermoegen | Wealth in gold/silver/copper pieces | +| Abwehr | Derived defense value | +| GSMaster | Game-system master data (canonical tables for skills, spells, equipment) | +| M5 | Midgard 5th edition rule set (the game system code used) | + +--- + +## 2. High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User (Browser) │ +└──────────────────────────┬──────────────────────────────────────┘ + │ HTTP / HTTPS +┌──────────────────────────▼──────────────────────────────────────┐ +│ Vue 3 SPA (bamort-frontend) │ +│ Vite dev server (port 5173) / Nginx (port 80) │ +│ - Vue Router (client-side navigation) │ +│ - Pinia (state management) │ +│ - vue-i18n (DE/EN translations) │ +│ - Axios (API calls to backend) │ +└──────────────────────────┬──────────────────────────────────────┘ + │ REST API calls to VITE_API_URL + │ Bearer token in Authorization header +┌──────────────────────────▼──────────────────────────────────────┐ +│ Go / Gin REST API (bamort-backend) │ +│ Port 8180 │ +│ - Public routes: /register /login /password-reset/* │ +│ - Protected routes: /api/* (auth middleware enforced) │ +│ - Public info routes: /api/public/* │ +└──────────────────────────┬──────────────────────────────────────┘ + │ GORM +┌──────────────────────────▼──────────────────────────────────────┐ +│ MariaDB 11.4 │ +│ (bamort-mariadb, port 3306) │ +│ Production: not exposed to host network │ +│ Dev: accessible at localhost:3306 + phpMyAdmin at :8081 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +The frontend communicates **directly** with the backend API via the `VITE_API_URL` environment variable — there is no Nginx reverse-proxy layer between them in production (TLS termination is the responsibility of an external proxy like Traefik). + +--- + +## 3. Technology Stack + +| Layer | Technology | Version | Purpose | +|-------|-----------|---------|---------| +| Backend language | Go | 1.25 | API server, business logic | +| Backend framework | Gin | latest | HTTP routing, middleware | +| ORM | GORM | latest | Database access, migrations | +| Database | MariaDB | 11.4 | Primary persistent storage | +| Test DB | SQLite | (via CGO) | Isolated per-test snapshot | +| PDF rendering | chromedp + Chromium | latest | HTML→PDF via headless browser | +| PDF merging | pdfcpu | latest | Multi-page PDF assembly | +| Live reload (dev) | Air | latest | Backend hot-reload during development | +| Frontend framework | Vue 3 | ^3.5.13 | UI (Options API) | +| Frontend build | Vite | ^6.0.1 | Dev server + production bundler | +| State management | Pinia | ^2.3.0 | Reactive global stores | +| HTTP client | Axios | ^1.7.9 | API communication with auth interceptors | +| i18n | vue-i18n | ^11.0.0 | German/English UI localisation | +| Frontend server | Nginx (prod) / Vite (dev) | alpine / 6.x | Static file serving + SPA routing | +| Container runtime | Docker + Docker Compose | — | Isolated service deployment | + +--- + +## 4. Backend Architecture + +### 4.1 Entry Point and Startup Sequence + +**File:** `backend/cmd/main.go` + +The `main()` function orchestrates startup in this order: + +1. **Load configuration** — reads `.env` / `.env.local` then environment variable overrides via `config.Cfg` (auto-loaded in `config.init()`) +2. **Configure logger** — sets log level (`DEBUG`, `INFO`, `WARN`, `ERROR`) and debug mode based on config +3. **Set Gin mode** — `gin.ReleaseMode` in production, `gin.DebugMode` otherwise +4. **Connect database** — calls `database.ConnectDatabase()` which selects MySQL or SQLite based on `DATABASE_TYPE` and `ENVIRONMENT` +5. **Initialize PDF templates** — copies default templates from `/app/default_templates` to `cfg.TemplatesDir` if needed via `pdfrender.InitializeTemplates()` +6. **Set up Gin engine** — calls `router.SetupGin(r)` for CORS config +7. **Register routes** — each module calls `RegisterRoutes(protected)` on the protected `/api` group +8. **Register public routes** — pdfrender and appsystem register unauthenticated endpoints +9. **Start HTTP server** — `r.Run(cfg.GetServerAddress())` + +### 4.2 Router and Middleware + +**File:** `backend/router/routes.go` and `backend/router/setup.go` + +**Route structure:** +``` +POST /register → user.RegisterUser +POST /login → user.LoginUser +POST /password-reset/request → user.RequestPasswordReset +GET /password-reset/validate/:token → user.ValidateResetToken +POST /password-reset/reset → user.ResetPassword + +/api/* ← ALL require valid Bearer token via user.AuthMiddleware() +├── /api/public/version → appsystem (no auth check inside group, separate route) +├── /api/public/systeminfo → appsystem +├── /api/user/* → user module +├── /api/users/* → user module (admin-only subset) +├── /api/characters/* → character module +├── /api/maintenance/* → gsmaster (read) + maintenance (maintainer-only write) +├── /api/importer/* → importer module +├── /api/pdf/* → pdfrender module +├── /api/transfer/* → transfer module +└── /api/appsystem/* → appsystem module +``` + +**CORS:** Configured in `router/setup.go` with allowed origins including `localhost:5173` (dev), `localhost:8180`, and configurable `BASE_URL` (production frontend). + +### 4.3 Authentication System + +**Token generation** (`user/handlers.go` → `GenerateToken`): +- Hashes `username + createdAt` with MD5 +- Embeds the user's ID in an predictable position within the hex string +- Token is stored client-side in `localStorage` + +**Token validation** (`user/handlers.go` → `CheckToken`): +- Extracts the Base64/hex-encoded user ID from position `len("Bearer ")` onward in the `Authorization` header +- Loads the user from the database by that ID +- **No cryptographic signature verification is performed** — this is a design limitation (see Section 13) + +**AuthMiddleware:** +- Calls `CheckToken`, sets `userID`, `username`, `user` in the Gin context +- Returns `401 Unauthorized` if token is absent or user not found + +**Password hashing:** MD5 (`crypto/md5`). The bcrypt implementation is present but commented out. + +### 4.4 Module Breakdown (Functional View) + +Each module follows the same structure: `handlers.go` (business logic + HTTP responses), `routes.go` (route registration), and `*_test.go` (tests). + +--- + +#### `user/` — User Account Management + +**Business function:** Registration, login, profile management, password reset via email, and role-based access control. + +**Key responsibilities:** +- `RegisterUser` — creates a new user with MD5-hashed password and default `standard` role +- `LoginUser` — validates credentials, returns a token +- `AuthMiddleware` — protects all `/api/*` routes; injects user context +- `RequireAdmin()` / `RequireMaintainer()` — role-check middleware used on specific sub-routes +- Profile endpoints: `GET/PUT /api/user/profile`, display name, email, preferred language, password change +- Admin user management: `GET/PUT/DELETE /api/users/:id` +- Password reset flow: request → email with token → validate token → set new password (uses `mail/` module for SMTP) + +**Roles:** +| Role | Permissions | +|------|------------| +| `standard` | Own characters + shared characters | +| `maintainer` | + Write access to GSMaster data and maintenance endpoints | +| `admin` | + User management (list, change role, delete, reset password) | + +--- + +#### `character/` — Character Management (Core Module) + +**Business function:** The heart of the application. Full CRUD for characters plus all RPG game-logic operations. + +**Sub-responsibilities:** + +| Sub-area | Files | Business purpose | +|----------|-------|-----------------| +| CRUD | `handlers.go`, `database.go` | Create / read / update / delete characters and their sub-entities (attributes, skills, spells, equipment) | +| Derived values | `derived_values_calculator.go` | Calculate secondary stats (Abwehr, Zaubern, Raufen, body/mind resistance) from primary attributes using M5 formulas | +| Derived values (dice) | `derived_values_calculator.go` | Calculate dice-dependent stats during character creation (Lp_max, Ap_max, B_max, PA, Wk) requiring die-roll input from frontend | +| Learning cost engine | `lerncost_handler.go` | Compute EP/TE/LE/gold cost to learn a new skill/spell or improve an existing one, considering class, category, difficulty, practice points, and reward bonuses | +| Skill actions | `handlers.go` | Add/remove/edit skills and weapon skills inline | +| Learn skill | Separate handler | Learn a new skill (deducts resources, creates audit log entries) | +| Improve skill | `ImproveSkill` handler | Increase an existing skill value (validates costs, writes audit trail) | +| Learn spell | `LearnSpell` handler | Add a new spell to the character | +| Practice points | `practice_points_handler.go` | Track PP per skill category; PP reduce TE costs in learning | +| Experience & wealth | `handlers.go` | Update EP/gold separately from full character update | +| Character sharing | `share_handlers.go` | Grant read or write access to specific other users | +| Character creation wizard | Multiple handlers | Multi-step session-based creation: basic info → attributes → derived rolling → skills → finalize | +| Creation rules | `creation_rules.go` | M5-specific tables for special abilities (W100 roll → bonus/malus), starting attribute bonuses by race/class | +| Audit log | `audit_log.go`, `audit_log_handlers.go` | Immutable log of EP/gold/stat changes with reason codes | +| Image | `image_handler.go` | Upload and store Base64 character portrait | +| Ownership guard | `ownership_guard_test.go` | Ensures only the owner (or share-granted user) can modify | +| System info | `system_information_handlers.go` | Returns available skill categories for UI dropdowns | + +--- + +#### `gsmaster/` — Game-System Master Data + +**Business function:** Canonical reference tables for the M5 game system. Skills, weapon skills, spells, equipment, weapons, and containers as defined by the Midgard rules. Users with the `maintainer` role can edit these tables. + +**Read (all authenticated users):** +- `GET /api/maintenance/skills` — list all skills +- `GET /api/maintenance/spells` — list all spells +- `GET /api/maintenance/weapons` — list all weapons +- `GET /api/maintenance/equipment` — general equipment +- `GET /api/maintenance/*-enhanced` — enhanced views with additional metadata (learning costs, categories, etc.) + +**Write (maintainer only):** +- POST/PUT/DELETE on all the above categories + +**Learning cost integration:** `gsmaster` package provides lookup helpers used by the `character` module to calculate learning costs. Skills carry `Category`, `Difficulty`, and `InnateSkill` properties that drive cost lookup logic. + +--- + +#### `equipment/` — Character Equipment Management + +**Business function:** CRUD operations specifically for the equipment attached to a character — weapons (`EqWaffe`), containers/bags (`EqContainer`), transportation (`EqContainer` with `IsTransportation=true`), and general gear (`EqAusruestung`). + +**Why separate from `character/`?** Equipment has its own handlers/routes to keep the character module focused on core stats and skills. Equipment can also reference master data items from `gsmaster/`. + +--- + +#### `pdfrender/` — PDF Character Sheet Export + +**Business function:** Generate printable PDF character sheets by rendering HTML templates with character data using a Chromium headless browser, then merging the resulting per-page PDFs into a single file. + +**Flow:** +1. `ExportCharacterToPDF` receives a character ID and template name +2. Loads the character from the database (fully preloaded) +3. For each template page (page1_stats.html, page2_play.html, etc.): + - Injects character data into the HTML template using Go template engine + - Renders it to PDF via `chromedp` (headless Chromium) + - Checks `` comment in template to detect overflow + - Generates continuation pages (page1.2_stats.html) for overflow +4. Merges all page PDFs with `pdfcpu` into one file +5. Saves to `cfg.ExportTempDir` and returns the filename +6. Frontend opens a download URL in a new window + +**Templates:** Located in `backend/templates/Default_A4_Quer/`. The `InitializeTemplates` function copies the default template to the runtime directory on first run. + +**Routes:** +- `GET /api/pdf/export/:id` — export, returns `{"filename": "..."}` +- `GET /api/pdf/file/:filename` — download the generated PDF (public route, no auth required, filename-based security) +- `GET /api/pdf/templates` — list available templates +- `POST /api/pdf/cleanup` — clean up old generated PDF files + +--- + +#### `importer/` — VTT / CSV Import and Export + +**Business function:** Import characters from Virtual Tabletop (VTT) JSON format or CSV files. Export characters to VTT JSON or CSV. Export/import spell lists via CSV. + +**Routes:** +``` +POST /api/importer/upload — Upload VTT JSON + optional CSV +POST /api/importer/spells/csv — Import spell data from CSV +GET /api/importer/export/vtt/:id — Export character as VTT JSON (API response) +GET /api/importer/export/vtt/:id/file — Export character as VTT JSON (file download) +GET /api/importer/export/csv/:id — Export character as CSV +GET /api/importer/export/spells/csv — Export all spells as CSV +``` + +--- + +#### `transfer/` — JSON Backup and Restore + +**Business function:** Portable character backup/restore in a Bamort-native JSON format. Also supports full-database export/import (admin/migration use case). + +**Routes:** +``` +GET /api/transfer/export/:id — Export character as JSON +GET /api/transfer/download/:id — Download character JSON as file +POST /api/transfer/import — Import character from JSON +POST /api/transfer/database/export — Export entire database as JSON +POST /api/transfer/database/import — Import full database from JSON +``` + +--- + +#### `maintenance/` — Admin and Database Operations + +**Business function:** Maintainer-only admin operations: database consistency checks, re-connect/reload env, data migration (SQLite→MariaDB), and managing auxiliary master data not covered by `gsmaster/`. + +**Key operations:** +- `GET /api/maintenance/setupcheck` — validate database schema integrity +- `GET /api/maintenance/setupcheck-dev` — extended dev-mode diagnostics +- `GET /api/maintenance/reconndb` — reconnect database (useful after connection interruption) +- `GET /api/maintenance/reloadenv` — reload environment variables at runtime +- `POST /api/maintenance/transfer-sqlite-to-mariadb` — one-time data migration +- CRUD for beliefs (`gsm-believes`), game systems, literature sources, misc lookups, skill improvement cost tables + +--- + +#### `appsystem/` — System Information + +**Business function:** Exposes version and system health information. + +**Routes:** +``` +GET /api/public/version — { version, gitCommit } +GET /api/public/systeminfo — { version, gitCommit, userCount, charCount, dbVersion } +``` + +These are reachable without authentication (used by `LandingView` and `SystemInfoView` to display status without requiring login). + +--- + +#### `gamesystem/` — Game System Records + +**Business function:** Manages `GameSystem` records in the database (e.g., M5 = Midgard 5th edition). Used when creating characters and as a FK reference in master data. Initial seed data creates the M5 game system if it doesn't exist. + +--- + +#### `logger/` — Application Logging + +**Business function:** Custom structured logger with levels (DEBUG/INFO/WARN/ERROR). Used throughout all packages. Debug mode controlled by config. + +--- + +#### `mail/` — Email Delivery + +**Business function:** SMTP client used exclusively for password-reset emails. Supports TLS (port 465) and STARTTLS (port 587). Configured via `MAIL_HOST`, `MAIL_PORT`, `MAIL_USERNAME`, `MAIL_PASSWORD`, `MAIL_FROM` env vars. + +--- + +## 5. Frontend Architecture + +### 5.1 Application Bootstrap + +**`src/main.js`** registers plugins in order: + +1. **Pinia** — state store (must be first) +2. **Vue Router** — client-side routing +3. **vue-i18n** — i18n instance created and exported from `stores/languageStore.js` +4. **UtilsPlugin** — installs global utilities on all components: `$formatDate`, `$formatDateTime`, `$formatRelativeDate`, `$safeValue`, `$capitalize`, `$rollDie`, `$rollDice`, `$rollDiceWithSum`, `$rollNotation`, `$randomBetween`, `$randomChoice`, `$shuffleArray` + +**`App.vue`** — root component: +- Shows `` only when `localStorage.getItem('token')` is truthy +- Applies `.full-width` CSS class on `
` for non-logged-in pages (centers login/register forms) +- Reacts to `storage` and `auth-changed` window events to update `loggedIn` state reactively + +### 5.2 Router and Navigation Guards + +**File:** `src/router/index.js` + +Uses `createWebHistory` (HTML5 pushstate — no hash URLs). Unauthenticated landing/auth pages are statically imported; all other views are lazy-loaded (code-split) for performance. + +**Navigation guard logic (beforeEach):** +1. If route requires auth and user is not logged in → redirect to `/login` +2. If route requires admin role → lazy-load `userStore`, fetch profile if not loaded, check `isAdmin` getter → redirect to `/dashboard` if not admin +3. Otherwise → allow navigation + +`isLoggedIn()` from `utils/auth.js` simply checks for a token in `localStorage`. + +### 5.3 State Management (Pinia Stores) + +**`userStore` (`src/stores/userStore.js`)** + +Central user identity state. Used by `Menu.vue`, `CharacterDetails.vue`, `UserManagementView.vue`, and the router guard. + +| State | Purpose | +|-------|---------| +| `currentUser` | Logged-in user object `{ id, username, display_name, email, role, preferred_language }` | +| `isLoading` | Loading flag for profile fetch | + +| Getter | Returns | +|--------|---------| +| `isAuthenticated` | `true` if `currentUser` is not null | +| `userRole` | `currentUser.role` or `'standard'` | +| `isAdmin` | `role === 'admin'` | +| `isMaintainer` | `role === 'maintainer'` or `'admin'` | +| `isStandardUser` | any logged-in user | + +Action `fetchCurrentUser()` calls `GET /api/user/profile`, hydrates `currentUser`, and also sets the UI language to the user's preferred language. + +--- + +**`languageStore` (`src/stores/languageStore.js`)** + +Holds current language (`de` or `en`). Unusually, the `i18n` instance itself is **created and exported from this file**, then imported in `main.js`. This coupling means language changes can be made from anywhere that imports `i18n`. + +--- + +**`characterCreationStore` (`src/stores/characterCreation.js`)** + +In-memory wizard state for multi-step character creation. Tracks current step (1–5), and partial data for each step. The actual server-side session persistence is handled directly by `CharacterCreation.vue` via API calls; the Pinia store mirrors this state in memory. + +### 5.4 Views and Components (Functional View) + +#### Public Pages (no auth required) + +| Component | Route | Purpose | +|-----------|-------|---------| +| `LandingView` | `/` | Landing page; polls `GET /api/public/version` every 5s (up to 24 retries) to show backend status; disables login button until backend responds | +| `LoginView` → `LoginForm` | `/login` | Login form; on success stores token in `localStorage`, triggers `auth-changed` event | +| `RegisterView` → `RegisterForm` | `/register` | Registration form | +| `ForgotPasswordView` → `ForgotPasswordForm` | `/forgot-password` | Sends password-reset email request | +| `ResetPasswordView` → `ResetPasswordForm` | `/reset-password` | Accepts reset token from URL, sets new password | +| `HelpView` | `/help` | FAQ page built from i18n keys dynamically | +| `SponsorsView` | `/sponsors` | Static credits/sponsor page | +| `SystemInfoView` | `/system-info` | Shows system versions and live backend stats | + +#### Authenticated Pages + +| Component | Route | Purpose | +|-----------|-------|---------| +| `DashboardView` → `CharacterList` | `/dashboard` | Lists own and shared characters; shows active creation sessions; "Create New Character" button starts a new wizard session | +| `UserProfileView` | `/profile` | Self-service: change display name, language, email, password | +| `UserManagementView` | `/users` | Admin-only: list all users, change roles, delete users, reset passwords | +| `MaintenanceView` → `Maintenance` | `/maintenance` | Maintainer tools: database checks, master data management | +| `FileUploadPage` | `/upload` | Import VTT JSON + optional CSV | +| `AusruestungView` → `AusruestungList` | `/ausruestung/:characterId` | Standalone equipment list (legacy view) | +| `CharacterDetails` | `/character/:id` | Main character hub (see below) | +| `CharacterCreation` | `/character/create/:sessionId` | Multi-step character creation wizard | + +#### CharacterDetails Component — Central Hub + +`CharacterDetails.vue` is the most complex component. It: +1. Fetches full character data from `GET /api/characters/:id` +2. Determines `isOwner` by comparing `character.user_id` vs `userStore.currentUser.id` +3. Renders a **dynamic sub-component** (``) driven by a submenu +4. Passes `character` and `isOwner` as props to every sub-view +5. Listens for `@character-updated` events from sub-views to trigger a full data reload + +**Sub-views (tabs):** + +| Component | Business purpose | +|-----------|----------------| +| `DatasheetView` | Character biography, attributes (Au/Gs/Gw/In/Ko/St/Wk/Zt/PA), derived stats (Lp/Ap/B, Abwehr, Zaubern, Raufen, Resistenz), bennies, wealth. Inline double-click editing for owners | +| `SkillView` | Skills grouped by category. Learning mode (toggle 🎓) reveals resources (EP, Gold) and action buttons: learn new skill, improve existing, add manually | +| `WeaponView` | Weapon skills and equipped weapons | +| `SpellView` | Spell list with learn/improve actions | +| `EquipmentView` | Containers, general equipment, transported items | +| `ExperianceView` | Experience and wealth tracking; audit log access | +| `DeleteCharView` | Confirmation UI for deleting the character (owner only) | +| `AuditLogView` | Read-only history of EP/gold changes | + +**Overlay dialogs accessible from CharacterDetails:** +- `ExportDialog` — multi-format export: choose PDF template, VTT JSON, or BaMoRT JSON and trigger download +- `VisibilityDialog` — toggle character between public/private (owner only) + +#### Learning Dialogs + +`SkillLearnDialog.vue`, `SkillImproveDialog.vue`, `SpellLearnDialog.vue` are modal dialogs that: +1. Call `POST /api/characters/lerncost-new` with skill name, current level, type, and action +2. Display the computed cost breakdown (EP, TE, LE, gold, PP reduction) +3. Allow the user to confirm → backend executes the learning/improvement and deducts resources + +### 5.5 API Communication Layer + +**File:** `src/utils/api.js` + +A pre-configured Axios instance: +- `baseURL`: `import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de'` +- **Request interceptor:** Injects `Authorization: Bearer ` from `localStorage` automatically +- **Response interceptor:** On `401`, removes the stale token from `localStorage` and logs a warning (but does not redirect — the nav guard handles that on next navigation) + +All components and stores import this `API` instance. Raw `axios` (without the interceptor) is used only in `LandingView` and `SystemInfoView` for unauthenticated public API calls. + +--- + +## 6. Core Business Logic + +### 6.1 Character Data Model + +The character data model maps closely to the **Midgard M5 character sheet**: + +``` +Char (root entity, table: char_chars) +├── Eigenschaften[] — 9 primary attributes (Au, Gs, Gw, In, Ko, St, Wk, Zt, PA) +├── Lp — Life points (current + max) +├── Ap — Action points (current + max) +├── B — Load/burden capacity (current + max) +├── Merkmale — Physical description (eye/hair color, height, etc.) +├── Erfahrungsschatz — EP (experience points) + ES (experience treasure/milestone) +├── Bennies — Luck tokens: Gg (lucky coin), Gp (lucky penny), Sg (lucky stroke) +├── Vermoegen — Wealth: Goldstuecke, Silberstuecke, Kupferstuecke +├── Fertigkeiten[] — Skills (Fertigkeit), each with value, base, bonus, PP, category +├── Waffenfertigkeiten[] — Weapon skills (subtype with additional combat fields) +├── Zauber[] — Spells with description, bonus, source reference +├── Waffen[] — Equipped weapons (EqWaffe) with attack/defense bonuses +├── Behaeltnisse[] — Bags/containers (EqContainer) +├── Transportmittel[] — Vehicles (EqContainer with IsTransportation=true) +├── Ausruestung[] — General equipment items +└── Spezialisierung — JSON array of specialization strings +``` + +**`FeChar`** (Frontend Character) is the API response type: it embeds `Char` plus computed fields: +- `Git` (poison tolerance = 30 + Ko/2) +- `CategorizedSkills` — map grouping skills by category for `SkillView` rendering +- `InnateSkills` — innate/racial skills separated from learned ones + +### 6.2 Derived Values Calculation + +**File:** `backend/character/derived_values_calculator.go` + +The backend implements the M5 formula set. Key derivations: + +| Derived value | Formula | +|--------------|---------| +| AusdauerBonus | Ko/10 + St/20 | +| SchadensBonus | St/20 + Gs/30 − 3 | +| AngriffsBonus | Gs/20 + Gw/30 − 3 | +| AbwehrBonus | Gw/20 + Gs/30 − 3 | +| ZauberBonus | In/10 + Zt/20 − 3 | +| ResistenzBonusKoerper | Ko/10 + Wk/20 − 3 | +| ResistenzBonusGeist | In/10 + Wk/15 − 3 | +| Abwehr | (grade-based base) + AbwehrBonus + PA (parry) | +| Zaubern | (class-based base) + ZauberBonus | +| Raufen | (class-based base at Grad 1) | + +Dice-dependent values (rolled during character creation) include: `lp_max`, `ap_max`, `b_max`, `pa` (parry), `wk` (willpower). The frontend rolls dice and sends the results to `POST /api/characters/create-session/:id/derived` where the server validates and stores them. + +The frontend component in `CharacterCreation.vue` exposes the dice-roll UI and calls `GET /api/characters/calculate-static-fields` and `POST /api/characters/calculate-rolled-field` to get the values computed by the backend rather than computing them in JavaScript. + +### 6.3 Skill and Spell Learning System + +**File:** `backend/character/lerncost_handler.go` + +The learning cost engine computes what it costs (EP + TE, or EP + LE for spells) to learn a new skill/spell or improve an existing one. This is the most complex piece of business logic in the application. + +**Input:** +```json +{ + "name": "Klettern", + "type": "skill", + "action": "improve", + "current_level": 10, + "target_level": 12, + "use_pp": 2, + "reward": { "type": "half_ep_improvement" } +} +``` + +**Processing steps:** +1. Look up the character's class abbreviation (`Typ`) +2. Normalize the skill/spell name (trim, lowercase for lookup) +3. Find the skill in `gsmaster` master data to get `Category` and `Difficulty` +4. Query learning cost tables: + - For **skills**: `ClassCategoryEPCost` (EP per TE by class+category) + `SkillImprovementCost` (TE per level step) + `SkillCategoryDifficulty` (base LE) + - For **spells**: `ClassSpellSchoolEPCost` (EP per LE by class+spell school) + `SpellLevelLECost` (LE by spell level) + - For **weapon skills**: separate weapon skill cost tables +5. Apply **practice points (PP)** reduction: each PP reduces required TE by 1 (capped at max PP available for category) +6. Apply **reward bonuses** if specified: + - `free_learning` — zero cost + - `free_spell_learning` — zero cost for spells + - `half_ep_improvement` — halve EP cost when improving + - `gold_for_ep` — substitute up to half the EP with 10 GS each +7. Return full cost breakdown plus `CanAfford` flag comparing costs to character's current EP/gold + +For **improve** action, costs are computed **per level step** from `current_level+1` to `target_level` and aggregated: `MultiLevelCostResponse` contains both per-step and total costs. + +The `ImproveSkill` and `LearnSkill` handlers execute the actual resource deduction and write audit log entries. + +### 6.4 Character Creation Wizard + +**Backend:** `backend/character/` (multiple handlers) +**Frontend:** `frontend/src/components/CharacterCreation.vue` and `CharacterCreation/` sub-components +**Session storage:** `models.CharacterCreationSession` (table: `char_creation_sessions`) + +The wizard is a **server-side session** model. A session stores partial character data as JSON blobs per step. Sessions expire (stored `ExpiresAt`). + +**Steps:** + +| Step | Name | Backend endpoint | Data stored | +|------|------|-----------------|-------------| +| 1 | Basic Info | `PUT .../basic` | Name, race, class, origin, social class, faith, gender, handedness | +| 2 | Attributes | `PUT .../attributes` | Primary attributes (rolled client-side, validated server-side) | +| 3 | Derived Values | `PUT .../derived` | Dice-rolled derived stats (LP/AP/B/PA/Wk) | +| 4 | Skills | `PUT .../skills` | Selected starting skills with initial values | +| 5 | Finalize | `POST .../finalize` | Creates the final `Char` record from all session data | + +**Available-skills-for-creation endpoint** (`POST /api/characters/available-skills-creation`): +Returns all learnable skills for a specific class, grouped by category, with starting LP (Learning Points) that determine how many skills the character can learn at creation. LP are class+category specific (`ClassCategoryLearningPoints` table). + +**Special abilities** (`creation_rules.go`): +M5 allows rolling a W100 to gain a random special ability at character creation. The `GetSpecialAbilityByRoll(roll)` function maps dice outcomes to named bonuses (e.g., roll 56–60 → "Gute Reflexe+9"). This is pure backend logic; the frontend passes the rolled value as part of the creation session data, and the server calls this function when computing derived values. + +**Dashboard integration:** `CharacterList.vue` shows in-progress sessions via `CharacterCreationSessions.vue`. Users can resume or delete draft sessions. + +### 6.5 PDF Export Pipeline + +**Flow (backend):** +``` +ExportCharacterToPDF (pdfrender/handlers.go) + ↓ +LoadCharacterFromDB (full preload) + ↓ +For each template page (page1_stats.html, page2_play.html, ...): + → Execute Go text/template with character data + → chromedp: launch Chromium, navigate to data URL, print to PDF + → Check overflow marker + → Generate continuation pages if needed (page1.2_stats.html) + ↓ +pdfcpu.MergeFiles → single merged PDF + ↓ +Save to cfg.ExportTempDir + ↓ +Return { filename: "CharName_20231225_143045.pdf" } +``` + +**Flow (frontend):** +``` +ExportDialog.vue + ↓ +GET /api/pdf/templates → populate template selector + ↓ +User selects format (PDF / VTT / BaMoRT) + template + ↓ +For PDF format: + GET /api/pdf/export/:id?template=Default_A4_Quer + → Response: { filename: "..." } + → window.open(baseURL + /api/pdf/file/, '_blank') + +For VTT format: + GET /api/importer/export/vtt/:id/file (blob) → browser download link + +For BaMoRT format: + GET /api/transfer/download/:id (blob) → browser download link +``` + +**Note:** For PDF, `window.open` is called after the async API response, making it subject to popup blockers. + +### 6.6 Audit Log + +**Files:** `backend/character/audit_log.go`, `audit_log_handlers.go` + +Every change to EP, gold, or significant character values writes an immutable `AuditLogEntry` record: + +| Field | Purpose | +|-------|---------| +| `CharacterID` | Which character | +| `FieldName` | What changed (e.g., `"experience_points"`, `"goldstücke"`) | +| `OldValue` / `NewValue` | Before and after | +| `Difference` | `NewValue - OldValue` | +| `Reason` | Typed constant: `manual`, `skill_learning`, `skill_improvement`, `spell_learning`, `equipment`, `reward`, `correction`, `import` | +| `UserID` | Who made the change | +| `Notes` | Free-text context | +| `Timestamp` | Auto-set by GORM | + +API endpoints: +- `GET /api/characters/:id/audit-log` — all entries or filtered by `?field=experience_points` +- `GET /api/characters/:id/audit-log/stats` — aggregated statistics + +--- + +## 7. Data Access and Persistence + +### Database Connection + +- **Production/development:** MariaDB 11.4 via GORM MySQL driver. DSN from `DATABASE_URL` env var. +- **Tests:** SQLite (via CGO). `testutils.SetupTestDB()` copies `testdata/prepared_test_data.db` to a temp dir per test run, ensuring isolation. +- **Global variable:** `database.DB *gorm.DB` — shared across all packages by import. + +### Schema Migrations + +`models.MigrateStructure(db)` is called at startup and runs GORM `AutoMigrate` for all entity types in dependency order: +1. `game_systems` +2. GSMaster tables (skills, spells, equipment, weapons, containers, believes, misc lookups) +3. Character tables (chars, attributes, skills, spells, equipment sub-tables) +4. Equipment tables +5. Skill learning cost tables +6. Learning/cost relation tables + +Migration history is tracked in `schema_version` and `migration_history` tables. + +### Test Data + +`testdata/prepared_test_data.db` is a pre-populated SQLite snapshot containing real test characters, including **character ID 18 ("Fanjo Vetrani")** which is used as the primary test fixture throughout all backend tests. + +### Custom Types + +`StringArray` is a custom GORM type that serializes `[]string` as JSON into a TEXT column: +```go +type StringArray []string +// Stored as: ["specialization1","specialization2"] +``` +Used by `Char.Spezialisierung`. + +--- + +## 8. Module Interaction Map + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Frontend (Vue 3) │ +│ │ +│ CharacterDetails ──────→ SkillView ──────→ SkillLearnDialog │ +│ │ │ │ +│ │ SpellView ──────→ SpellLearnDialog │ +│ │ │ │ +│ └──────→ ExportDialog │ │ +│ │ │ │ +└─────────────────────┼──────────────────────────────┼────────────────┘ + │ REST API │ REST API + ┌─────────────▼──────────────┐ ┌───────────▼───────────────┐ + │ pdfrender module │ │ character module │ + │ ExportCharacterToPDF │ │ GetLernCostNewSystem │ + │ chromedp → Chromium │ │ LearnSkill / ImproveSkill │ + │ pdfcpu merge │ │ Audit log writes │ + └─────────────────────────── ┘ └───────────────────────────┘ + │ + ┌────────────▼──────────────┐ + │ gsmaster module │ + │ Skill/Spell lookup │ + │ Cost table queries │ + └───────────────────────────┘ + │ + ┌────────────▼──────────────┐ + │ models package │ + │ GORM entity definitions │ + │ LearnCost, Char, etc. │ + └───────────────────────────┘ + │ + ┌────────────▼──────────────┐ + │ database.DB │ + │ MariaDB (prod/dev) │ + │ SQLite (test) │ + └───────────────────────────┘ +``` + +**Cross-module dependencies (backend):** +- `character` depends on `gsmaster` (skill/spell lookup for cost calculations) +- `character` depends on `models` (all entity types) +- `character` depends on `database` (direct DB access) +- `gsmaster` depends on `models` and `database` +- `pdfrender` depends on `models` and `database` (loads full character) +- `maintenance` depends on `gsmaster`, `models`, `database` +- `user` is depended on by all modules via `AuthMiddleware` import into `router` + +--- + +## 9. Complete API Route Reference + +### Public Routes (no authentication) +``` +POST /register +POST /login +POST /password-reset/request +GET /password-reset/validate/:token +POST /password-reset/reset +GET /api/public/version +GET /api/public/systeminfo +GET /api/pdf/file/:filename +``` + +### User Routes (authenticated) +``` +GET /api/user/profile +PUT /api/user/display-name +PUT /api/user/language +PUT /api/user/email +PUT /api/user/password +``` + +### App System Routes +``` +GET /api/version — version info (authenticated) +GET /api/systeminfo — system info (authenticated) +GET /api/public/version — version info (public, no auth) +GET /api/public/systeminfo — system info (public, no auth) +``` + +### Admin User Management (admin role required) +``` +GET /api/users +GET /api/users/:id +PUT /api/users/:id/role +DELETE /api/users/:id +PUT /api/users/:id/password +``` + +### Character Routes +``` +GET /api/characters — list own + shared characters +POST /api/characters — create character +GET /api/characters/:id — get full character (FeChar) +PUT /api/characters/:id — update character +PATCH /api/characters/:id — partial update +DELETE /api/characters/:id — delete (owner only) +PUT /api/characters/:id/image — update portrait +GET /api/characters/:id/datasheet-options — dropdowns for editing + +GET /api/characters/:id/shares — list share grants +PUT /api/characters/:id/shares — update share grants +GET /api/characters/:id/available-users — users available for sharing + +GET /api/characters/:id/experience-wealth — EP + wealth snapshot +PUT /api/characters/:id/experience — update EP +PUT /api/characters/:id/wealth — update wealth + +GET /api/characters/:id/audit-log — change history +GET /api/characters/:id/audit-log/stats — audit statistics + +POST /api/characters/lerncost-new — compute learning costs +POST /api/characters/lerncost — alias for lerncost-new +POST /api/characters/improve-skill-new — execute skill improvement +POST /api/characters/improve-skill — alias +POST /api/characters/:id/learn-skill-new — execute skill learning +POST /api/characters/:id/learn-skill — alias +POST /api/characters/:id/learn-spell-new — execute spell learning +POST /api/characters/:id/learn-spell — alias + +POST /api/characters/available-skills-new — available skills with costs +POST /api/characters/available-skills — alias +POST /api/characters/available-skills-creation — skills for creation wizard +POST /api/characters/available-spells-creation — spells for creation wizard +POST /api/characters/available-spells-new — available spells with costs +POST /api/characters/available-spells — alias +GET /api/characters/spell-details — spell detail lookup +GET /api/characters/:id/reward-types — available reward types + +GET /api/characters/:id/practice-points — PP by category +PUT /api/characters/:id/practice-points — update PP +POST /api/characters/:id/practice-points/add — add PP +POST /api/characters/:id/practice-points/use — consume PP + +GET /api/characters/skill-categories — static skill category list + +GET /api/characters/create-sessions — list wizard sessions +POST /api/characters/create-session — start new session +GET /api/characters/create-session/:id — get session data +PUT /api/characters/create-session/:id/basic — save step 1 +PUT /api/characters/create-session/:id/attributes — save step 2 +PUT /api/characters/create-session/:id/derived — save step 3 +PUT /api/characters/create-session/:id/skills — save step 4 +POST /api/characters/create-session/:id/finalize — create character from session +DELETE /api/characters/create-session/:id — discard session + +GET /api/characters/races — available races list +GET /api/characters/classes — available character classes +GET /api/characters/classes/learning-points — LP per class+category for creation +GET /api/characters/origins — available origins +GET /api/characters/beliefs — belief/faith search + +POST /api/characters/calculate-static-fields — derived values (no dice) +POST /api/characters/calculate-rolled-field — derived value requiring dice roll +``` + +### GSMaster Routes (read: all authenticated; write: maintainer+) +``` +GET /api/maintenance — all master data +GET /api/maintenance/skills +GET /api/maintenance/skills-enhanced +GET /api/maintenance/skills/:id +GET /api/maintenance/skills-enhanced/:id +GET /api/maintenance/weaponskills[/*] +GET /api/maintenance/spells[/*] +GET /api/maintenance/equipment[/*] +GET /api/maintenance/weapons[/*] +POST/PUT/DELETE above (maintainer only) +``` + +### Maintenance Routes (maintainer role required) +``` +GET /api/maintenance/gsm-believes +PUT /api/maintenance/gsm-believes/:id +GET /api/maintenance/game-systems +PUT /api/maintenance/game-systems/:id +GET /api/maintenance/gsm-lit-sources +PUT /api/maintenance/gsm-lit-sources/:id +GET /api/maintenance/gsm-misc +PUT /api/maintenance/gsm-misc/:id +GET /api/maintenance/skill-improvement-cost2 +PUT /api/maintenance/skill-improvement-cost2/:id +GET /api/maintenance/setupcheck +GET /api/maintenance/setupcheck-dev +GET /api/maintenance/mktestdata +GET /api/maintenance/reconndb +GET /api/maintenance/reloadenv +POST /api/maintenance/transfer-sqlite-to-mariadb +``` + +### PDF Export Routes +``` +GET /api/pdf/templates — list available templates +GET /api/pdf/export/:id — export character to PDF +POST /api/pdf/cleanup — clean up old generated PDF files +GET /api/pdf/file/:filename — download generated PDF (public, no auth) +``` + +### Importer Routes +``` +POST /api/importer/upload — import VTT JSON + CSV +POST /api/importer/spells/csv — import spell CSV +GET /api/importer/export/vtt/:id — export as VTT JSON +GET /api/importer/export/vtt/:id/file — download VTT JSON file +GET /api/importer/export/csv/:id — export as CSV +GET /api/importer/export/spells/csv — export all spells as CSV +``` + +### Transfer Routes +``` +GET /api/transfer/export/:id — export character JSON +GET /api/transfer/download/:id — download character JSON file +POST /api/transfer/import — import character JSON +POST /api/transfer/database/export — export full database +POST /api/transfer/database/import — import full database +``` + +--- + +## 10. Infrastructure and Deployment + +### Development Environment + +``` +docker/docker-compose.dev.yml + bamort-backend-dev (port 8180) — Go source mounted + Air live-reload + bamort-frontend-dev (port 5173) — Vue source mounted + Vite HMR + bamort-mariadb-dev (port 3306) — MariaDB with health-check + bamort-phpmyadmin-dev (port 8081) — Database GUI +``` + +- Full source code is volume-mounted into each container. +- Backend uses `air -c .air.toml` for hot-reload on `.go` file changes. +- Frontend uses Vite's native HMR for instant module replacement. +- Database data persists in `docker/bamort-db-dev/` on the host. +- Database is initialized from `docker/init-db/*.sql` on first container creation only. + +### Production Environment + +``` +docker/docker-compose.yml + bamort-backend (port 8182→8180) — compiled Go binary, no source mount + bamort-frontend (port 8181→80) — Nginx serving pre-built Vue bundle + bamort-mariadb (not exposed) — MariaDB accessible only within Docker network +``` + +- Backend image: multi-stage build. Stage 1 (golang:1.25-alpine) compiles the binary. Stage 2 (alpine:3.23) installs Chromium and copies only the binary + templates. +- Frontend image: multi-stage. Stage 1 (node:22-alpine) runs `npm run build` with `VITE_*` build args baked in. Stage 2 (nginx:alpine) serves the static bundle. +- **`VITE_API_URL`** is a **compile-time** build argument — runtime env vars cannot change it after image build. +- Database port is intentionally not exposed in production. +- TLS termination is expected at an external reverse proxy (e.g., Traefik). +- Templates are volume-mounted: `./templates:/app/templates`. + +### Starting/Stopping + +```bash +# Development +cd /data/dev/bamort +./docker/start-dev.sh # builds + starts all dev containers +./docker/stop-dev.sh # stops and removes dev containers + +# Production +./docker/start-prd.sh # builds new images while old containers run, then switches +./docker/stop-prd.sh # stops production containers +``` + +--- + +## 11. User Roles and Access Control + +| Capability | standard | maintainer | admin | +|-----------|----------|-----------|-------| +| Register / login | ✓ | ✓ | ✓ | +| View own characters | ✓ | ✓ | ✓ | +| View public/shared characters | ✓ | ✓ | ✓ | +| Create/edit/delete own characters | ✓ | ✓ | ✓ | +| Learn/improve skills and spells | ✓ | ✓ | ✓ | +| Export characters to PDF | ✓ | ✓ | ✓ | +| Import/export characters (JSON/CSV) | ✓ | ✓ | ✓ | +| Read GSMaster data | ✓ | ✓ | ✓ | +| Write GSMaster data | ✗ | ✓ | ✓ | +| Maintenance operations | ✗ | ✓ | ✓ | +| List all users | ✗ | ✗ | ✓ | +| Change user roles | ✗ | ✗ | ✓ | +| Delete users | ✗ | ✗ | ✓ | +| Reset any user's password | ✗ | ✗ | ✓ | + +**Enforcement mechanisms:** +- Backend: `user.AuthMiddleware()` on all `/api/*` routes +- Backend: `user.RequireMaintainer()` middleware on maintenance write routes +- Backend: `user.RequireAdmin()` middleware on `/api/users/*` routes +- Backend: `checkCharacterOwnership()` inside character handlers (returns 403 if user doesn't own the character) +- Frontend: router navigation guard redirects non-admins away from `/users` +- Frontend: `isOwner` prop controls visibility of edit/delete buttons in `CharacterDetails` + +--- + +## 12. Internationalisation (i18n) + +**Implementation:** `vue-i18n` (v11, composition-mode `legacy: false`) + +**Locales:** `src/locales/de/` and `src/locales/en/` — JS module files (not JSON) exporting translation objects. Both must be updated when adding new UI strings. + +**Default language:** `de` (German) + +**Language selection precedence:** +1. User's stored preference in `localStorage.language` +2. User's `preferred_language` field from their profile (set on `fetchCurrentUser()`) +3. Fallback to `de` + +**Language change flow:** +- User changes language in `UserProfileView` → `PUT /api/user/language` +- Frontend immediately sets `this.$i18n.locale` and updates `localStorage.language` +- On next login, `userStore.fetchCurrentUser()` sets locale from profile + +**Usage in components:** +```vue +{{ $t('skill.name') }} +this.$t('error.notFound') +this.$te('help.faq3.question') +``` + +--- + +## 13. Known Limitations and Security Notes + +### Authentication Security + +> **Important:** The current token scheme has cryptographic weaknesses. + +1. **MD5 token generation** — MD5 is not a cryptographic authentication scheme. The token is derived predictably from `username + createdAt`, without a secret key. An attacker with knowledge of when a user registered could forge tokens. +2. **No token signature verification** — `CheckToken` extracts the user ID from the token string and loads the user from the database; it does not verify that the token was actually issued by the server. +3. **MD5 password hashing** — Passwords are hashed with MD5 (not bcrypt/argon2). MD5 is unsuitable for password storage; it is fast enough for brute-force attacks and not salted. +4. **Token scope** — Tokens do not expire. Once issued, a token is valid indefinitely. + +The bcrypt implementation is commented out in `user/handlers.go` and `user/model.go` — migration to bcrypt for future versions is anticipated but not yet active. + +### Other Notes + +- `backend/startserver.sh` contains hardcoded development credentials — this file should not be committed to public repositories. +- The PDF download endpoint (`/api/pdf/file/:filename`) is unauthenticated by design (to allow direct browser downloads) but relies on filename unpredictability for security. +- `FileUploadPage.vue` manually adds an `Authorization` header that is already added by the Axios interceptor — this is a redundancy, not a security issue. +- The production `DATABASE_URL` in `docker-compose.yml` is constructed inline without fallback defaults — missing env vars will produce an empty credentials DSN. diff --git a/backend_findings.md b/backend_findings.md new file mode 100644 index 0000000..c253157 --- /dev/null +++ b/backend_findings.md @@ -0,0 +1,1362 @@ +# BaMoRT Backend — Comprehensive Architecture & Code Findings + +> Generated: 2026-03-13 +> Source: `/home/de31a2/bamort/backend` +> Module: `bamort` (Go 1.24, toolchain 1.24.4) + +--- + +## Table of Contents + +1. [Module Structure Overview](#1-module-structure-overview) +2. [Entry Point — cmd/main.go](#2-entry-point--cmdmaingo) +3. [Router & Middleware Setup](#3-router--middleware-setup) +4. [Authentication & Authorization](#4-authentication--authorization) +5. [Configuration](#5-configuration) +6. [Database Layer](#6-database-layer) +7. [Models (GORM Entities)](#7-models-gorm-entities) +8. [Module-by-Module Analysis](#8-module-by-module-analysis) + - [user/](#81-user) + - [character/](#82-character) + - [equipment/](#83-equipment) + - [gsmaster/](#84-gsmaster) + - [gamesystem/](#85-gamesystem) + - [maintenance/](#86-maintenance) + - [pdfrender/](#87-pdfrender) + - [importer/](#88-importer) + - [transfer/](#89-transfer) + - [appsystem/](#810-appsystem) + - [logger/](#811-logger) + - [mail/](#812-mail) + - [router/](#813-router) + - [config/](#814-config) + - [database/](#815-database) + - [testutils/](#816-testutils) + - [api/](#817-api) +9. [Complete API Route Listing](#9-complete-api-route-listing) +10. [Testing Approach](#10-testing-approach) +11. [Key Dependencies](#11-key-dependencies) +12. [Security Observations](#12-security-observations) +13. [Cross-Module Relationships Diagram](#13-cross-module-relationships-diagram) + +--- + +## 1. Module Structure Overview + +``` +backend/ +├── cmd/ # Entry point (main.go) +├── config/ # App configuration loading from env/.env +├── database/ # DB connection, migrations, test helpers +├── models/ # All GORM entities and migration orchestration +├── router/ # Gin engine setup, CORS, base route group + JWT guard +├── user/ # User accounts, auth, roles, password reset +├── character/ # Core character CRUD, skills, spells, learning system +├── equipment/ # Character equipment (weapons, containers, gear) +├── gsmaster/ # Game-system master data (skills, spells, weapons, learning costs) +├── gamesystem/ # Game system record management and initial seed data +├── maintenance/ # Admin/maintainer ops – DB checks, migrations, scratchpad endpoints +├── pdfrender/ # HTML→PDF export using chromedp + pdfcpu merge +├── importer/ # VTT-JSON / CSV import and export of characters/spells +├── transfer/ # JSON-based character and full-database import/export +├── appsystem/ # Version and system-info endpoints +├── logger/ # Custom leveled logger (DEBUG/INFO/WARN/ERROR) +├── mail/ # SMTP email client (password reset emails) +├── api/ # Integration tests spanning multiple modules +├── testutils/ # Shared test environment setup helpers +├── testdata/ # Prepared SQLite snapshot used by all tests +├── templates/ # PDF HTML templates (Default_A4_Quer/) +└── scripts/ # Shell scripts (e.g., SQLite→MariaDB transfer) +``` + +Each domain module follows the pattern: + +``` +module/ + handlers.go HTTP handler functions (Gin controllers) + routes.go RegisterRoutes(r *gin.RouterGroup) + *_test.go Tests using setupTestEnvironment(t) +``` + +--- + +## 2. Entry Point — cmd/main.go + +**File:** `cmd/main.go` + +### Startup Sequence + +```go +func main() { + cfg := config.Cfg // 1. Load config (auto-loaded in init()) + + logger.SetDebugMode(cfg.DebugMode) // 2. Configure logger + logger.SetMinLogLevel(...) + + if cfg.IsProduction() { + gin.SetMode(gin.ReleaseMode) // 3. Set Gin mode + } + + database.ConnectDatabase() // 4. Connect to database + + pdfrender.InitializeTemplates(...) // 5. Sync PDF templates + + r := gin.Default() + router.SetupGin(r) // 6. CORS middleware + + protected := router.BaseRouterGrp(r) // 7. Public auth routes + JWT-protected group + + // 8. Register module routes + user.RegisterRoutes(protected) + gsmaster.RegisterRoutes(protected) + character.RegisterRoutes(protected) + equipment.RegisterRoutes(protected) + maintenance.RegisterRoutes(protected) + importer.RegisterRoutes(protected) + pdfrender.RegisterRoutes(protected) + transfer.RegisterRoutes(protected) + appsystem.RegisterRoutes(protected) + + // 9. Public routes (no auth) + pdfrender.RegisterPublicRoutes(r) + appsystem.RegisterPublicRoutes(r) + + r.Run(cfg.GetServerAddress()) // 10. Start HTTP server +} +``` + +### Swagger Annotations +The file contains `@title BaMoRT API`, `@version 1`, `@host localhost:8180` – indicates Swagger/OpenAPI doc generation is intended. + +--- + +## 3. Router & Middleware Setup + +### CORS (`router/setup.go`) + +```go +func SetupGin(r *gin.Engine) { + allowedOrigins := []string{ + config.Cfg.FrontendURL, + "http://localhost:5173", + "http://192.168.0.48:5173", + "http://192.168.0.36:5173", + "https://bamort.trokan.de", + } + r.Use(cors.New(cors.Config{ + AllowOrigins: allowedOrigins, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * 3600, + })) +} +``` + +### Route Groups (`router/routes.go`) + +**Public (unauthenticated) routes:** + +| Method | Path | Handler | +|--------|------|---------| +| POST | `/register` | `user.RegisterUser` | +| POST | `/login` | `user.LoginUser` | +| POST | `/password-reset/request` | `user.RequestPasswordReset` | +| GET | `/password-reset/validate/:token` | `user.ValidateResetToken` | +| POST | `/password-reset/reset` | `user.ResetPassword` | + +**Protected group:** `/api` — guarded by `user.AuthMiddleware()`. + +--- + +## 4. Authentication & Authorization + +### Token Scheme + +The project uses a **custom MD5-based token**, not JWT. + +**Token Generation (`user/handlers.go`):** +```go +func GenerateToken(u *User) string { + tx := md5.Sum([]byte(u.Username + u.CreatedAt.String())) + hashString := hex.EncodeToString(tx[:]) + // Insert ".{userID}:" at position 7 + pos := 7 + idm := "." + fmt.Sprintf("%d", u.UserID) + ":" + token := hashString[:pos] + idm + hashString[pos:] + return token +} +``` + +The token embeds the user ID in a predictable location. Clients send it as the `Authorization` header value (prefixed `Bearer `). + +**Token Validation (`CheckToken`):** +1. Extracts user ID from position `7 + len("Bearer ")` in the `Authorization` header. +2. Loads the user from the database by that ID. +3. Returns the user or `nil`. + +**No cryptographic signature is verified.** Any token with a valid user ID at the expected position is accepted. + +### `AuthMiddleware()` (`user/handlers.go`) + +Sets `userID`, `username`, and `user` in the Gin context for downstream handlers. + +### Password Hashing + +Passwords are hashed with **MD5** (not bcrypt): +```go +hashedPassword := md5.Sum([]byte(user.PasswordHash)) +user.PasswordHash = hex.EncodeToString(hashedPassword[:]) +``` +The bcrypt implementation is commented out. + +### Roles (`user/model.go`) + +| Constant | Value | Description | +|----------|-------|-------------| +| `RoleStandardUser` | `"standard"` | Default for new registrations | +| `RoleMaintainer` | `"maintainer"` | Can manage master data | +| `RoleAdmin` | `"admin"` | Can manage users | + +Role-checking middleware: +- `RequireAdmin()` → used on `/api/users/*` admin endpoints +- `RequireMaintainer()` → used on maintenance/gsmaster write endpoints + +--- + +## 5. Configuration + +**File:** `config/config.go` + +### `Config` Struct Fields + +| Field | Env Var(s) | Default | Description | +|-------|-----------|---------|-------------| +| `ServerPort` | `API_PORT`, `SERVER_PORT` | `"8180"` | HTTP listen port | +| `DatabaseURL` | `DATABASE_URL` | `""` | DSN string for DB driver | +| `DatabaseType` | `DATABASE_TYPE` | `"mysql"` | `mysql` or `sqlite` | +| `DebugMode` | `DEBUG` | `false` | Enables verbose logging | +| `LogLevel` | `LOG_LEVEL` | `"INFO"` | `DEBUG`, `INFO`, `WARN`, `ERROR` | +| `Environment` | `ENVIRONMENT`, `GO_ENV` | `"production"` | `production`, `development`, `test` | +| `DevTesting` | `DEVTESTING` | `"no"` | `"yes"` redirects to SQLite test DB | +| `FrontendURL` | `BASE_URL` | `"http://localhost:5173"` | CORS allowed origin | +| `TemplatesDir` | `TEMPLATES_DIR` | `"./templates"` | PDF template directory | +| `ExportTempDir` | `EXPORT_TEMP_DIR` | `"./xporttemp"` | Temporary PDF output directory | +| `MailHost` | `MAIL_HOST` | `""` | SMTP server hostname | +| `MailPort` | `MAIL_PORT` | `465` | SMTP port (465=TLS, 587=STARTTLS) | +| `MailUsername` | `MAIL_USERNAME` | `""` | SMTP credentials | +| `MailPassword` | `MAIL_PASSWORD` | `""` | SMTP credentials | +| `MailFrom` | `MAIL_FROM` | (falls back to username) | Sender email address | + +### Loading Logic + +1. Reads `.env` and `.env.local` files from the working directory. +2. Overrides with env vars. +3. `ENVIRONMENT=development` automatically enables `DebugMode=true` and `LogLevel=DEBUG`. +4. `ENVIRONMENT=test` or `DEVTESTING=yes` causes `ConnectDatabase()` to use the SQLite test snapshot. + +--- + +## 6. Database Layer + +**Package:** `database/` + +### Connection (`database/config.go`) + +```go +func ConnectDatabase() *gorm.DB { + if envIsTest || devTestingIsYes { + SetupTestDB() // Use prepared SQLite snapshot + } else { + ConnectDatabaseOrig() // MySQL or SQLite via DATABASE_URL + } + return DB +} +``` + +Supported drivers: `mysql` (default), `sqlite`. + +### Global Variable + +`database.DB *gorm.DB` — all packages import this directly. + +### Test Database + +- **Source:** `testdata/prepared_test_data.db` (SQLite snapshot with real test data including character ID 18 "Fanjo Vetrani") +- **Mechanism:** `SetupTestDB()` copies the snapshot to a `os.MkdirTemp` directory, opens it, and assigns it to `database.DB`. +- Each test run gets a fresh isolated copy. + +### Migration Tables (`database/model.go`) + +| Table | Purpose | +|-------|---------| +| `schema_version` | Current schema version tracking | +| `migration_history` | Log of applied migrations | + +### Migration Orchestration (`models/database.go`) + +`models.MigrateStructure(db)` calls in order: +1. `gameSystemMigrateStructure` — `game_systems` +2. `gsMasterMigrateStructure` — skills, spells, equipment, weapons, etc. +3. `characterMigrateStructure` — characters, skills, spells, equipment +4. `equipmentMigrateStructure` +5. `skillsMigrateStructure` — learning cost tables +6. `importerMigrateStructure` — (currently no-op) +7. `learningMigrateStructure` — all learning/cost relation tables + +### `StringArray` Custom Type + +```go +type StringArray []string +// Stored as JSON in TEXT column (Value+Scan implementations) +``` + +Used for `Char.Spezialisierung` (character specializations). + +--- + +## 7. Models (GORM Entities) + +### Base Types + +```go +type BamortBase struct { + ID uint `gorm:"primaryKey"` + Name string +} + +type BamortCharTrait struct { + BamortBase + CharacterID uint `gorm:"index"` + UserID uint `gorm:"index"` +} + +type Magisch struct { + IstMagisch bool + Abw int + Ausgebrannt bool +} +``` + +--- + +### User Model (`user/model.go` → table: `users`) + +| Field | Type | Notes | +|-------|------|-------| +| `UserID` | uint PK | | +| `Username` | string unique | | +| `DisplayName` | string | Falls back to Username if empty | +| `PasswordHash` | string | MD5 hex string | +| `Email` | string unique | | +| `Role` | string | `standard`, `maintainer`, `admin` | +| `PreferredLanguage` | string | default `de` | +| `ResetPwHash` | *string | Hidden from JSON serialization | +| `ResetPwHashExpires` | *time.Time | 14 days validity | +| `CreatedAt`, `UpdatedAt` | time.Time | | + +--- + +### Character Model (`models/model_character.go` → table: `char_chars`) + +**Main struct `Char`:** + +| Field | Type | Notes | +|-------|------|-------| +| `ID` | uint PK | (via BamortBase) | +| `Name` | string | Character name | +| `GameSystem` | string | e.g. `"midgard"` | +| `GameSystemId` | uint | FK to `game_systems` | +| `UserID` | uint index | FK to `users` | +| `User` | User | FK `UserID→users.user_id` CASCADE | +| `Rasse` | string | Race (e.g. "Mensch", "Zwerg") | +| `Typ` | string | Class code (e.g. "Kr", "Ma") | +| `Alter` | int | Age | +| `Anrede` | string | Title/salutation | +| `Grad` | int | Character grade/level | +| `Gender` | string | | +| `SocialClass` | string | | +| `Groesse` | int | Height (cm) | +| `Gewicht` | int | Weight (kg) | +| `Herkunft` | string | Origin/homeland | +| `Glaube` | string | Faith | +| `Hand` | string | Dominant hand | +| `Public` | bool | Publicly visible | +| `ResistenzKoerper` | int | Body resistance (derived) | +| `ResistenzGeist` | int | Mental resistance (derived) | +| `Abwehr` | int | Defense value (derived) | +| `Zaubern` | int | Spell casting value (derived) | +| `Raufen` | int | Brawling value (derived) | +| `Lp` | Lp | Life points (has-one, CASCADE) | +| `Ap` | Ap | Action points (has-one) | +| `B` | B | Load/burden (has-one) | +| `Merkmale` | Merkmale | Physical traits (has-one) | +| `Eigenschaften` | []Eigenschaft | Attributes Au/Gs/Gw/In/Ko/St/Wk/Zt/PA | +| `Fertigkeiten` | []SkFertigkeit | Skills (has-many) | +| `Waffenfertigkeiten` | []SkWaffenfertigkeit | Weapon skills | +| `Zauber` | []SkZauber | Spells | +| `Spezialisierung` | StringArray | TEXT/JSON stored | +| `Bennies` | Bennies | Gg/Gp/Sg | +| `Vermoegen` | Vermoegen | GS/SS/KS wealth | +| `Erfahrungsschatz` | Erfahrungsschatz | EP/ES experience | +| `Waffen` | []EqWaffe | Weapons (has-many) | +| `Behaeltnisse` | []EqContainer | Containers | +| `Transportmittel` | []EqContainer | Transportation | +| `Ausruestung` | []EqAusruestung | General equipment | +| `Image` | string | Base64 or URL | + +**Helper types:** + +| Type | Table | Fields | +|------|-------|--------| +| `Eigenschaft` | (embedded in preload) | ID, CharacterID, UserID, Name, Value | +| `Lp` | — | ID, CharacterID, Max, Value | +| `Ap` | — | ID, CharacterID, Max, Value | +| `B` | — | ID, CharacterID, Max, Value | +| `Merkmale` | — | BamortCharTrait + Augenfarbe/Haarfarbe/Sonstige/Breite/Groesse | +| `Erfahrungsschatz` | — | BamortCharTrait + ES, EP | +| `Bennies` | — | BamortCharTrait + Gg, Gp, Sg | +| `Vermoegen` | — | BamortCharTrait + Goldstuecke, Silberstuecke, Kupferstuecke | + +**`FeChar`** (frontend-facing): +Embeds `Char` + `Git` (gift tolerance = 30+Ko/2) + `CategorizedSkills` map + `InnateSkills` slice. + +--- + +### Skill Models (`models/model_char_skills.go`) + +| Type | Table | Key Fields | +|------|-------|------------| +| `SkFertigkeit` | `char_skills` | BamortCharTrait + Fertigkeitswert, BasisWert, Bonus, Pp, Category, Improvable, LearningCost | +| `SkWaffenfertigkeit` | `char_weaponskills` | Embeds SkFertigkeit | +| `SkAngeboreneFertigkeit` | (no own table) | Embeds SkFertigkeit | +| `SkZauber` | `char_spells` | BamortCharTrait + Beschreibung, Bonus, Quelle | + +--- + +### Equipment Models (`models/model_char_equipment.go`) + +| Type | Table | Key Fields | +|------|-------|------------| +| `EqAusruestung` | `equi_equipments` | BamortCharTrait + Magisch + Anzahl, ContainedIn, Bonus, Gewicht, Wert | +| `EqWaffe` | `equi_weapons` | BamortCharTrait + Magisch + Abwb, Anb, Anzahl, Schb, NameFuerSpezialisierung | +| `EqContainer` | `equi_containers` | BamortCharTrait + Magisch + IsTransportation, Tragkraft, Volumen, ExtID | + +--- + +### GSMaster Models (`models/model_gsmaster.go`) + +| Type | Table | Key Fields | +|------|-------|------------| +| `Skill` | `gsm_skills` | ID, GameSystem/Id, Name, Initialwert, BasisWert, Bonuseigenschaft, Improvable, InnateSkill, Category, Difficulty | +| `WeaponSkill` | (inherits Skill) | Embeds Skill | +| `Spell` | (via gsmaster) | ID, Name, Stufe, AP, Art, Zauberdauer, Reichweite, Ursprung, Category, LearningCategory | +| `Equipment` | (via gsmaster) | ID, Name, Gewicht, Wert, PersonalItem | +| `Weapon` | (extends Equipment) | SkillRequired, Damage, RangeNear/Middle/Far | +| `Container` | (extends Equipment) | Tragkraft, Volumen | +| `Transportation` | (extends Container) | — | +| `Believe` | (via gsmaster) | ID, GameSystem/Id, Name, Beschreibung, SourceID | +| `MiscLookup` | `gsm_misc_lookups` | ID, GameSystem/Id, Key, Value | +| `LookupList` | `gsm_lookup_lists` | Generic lookup | +| `LearnCost` | (computed, no own table) | GameSystem, Stufe, LE, TE, Ep, Money, PP | + +--- + +### Game System (`models/model_game_system.go` → table: `game_systems`) + +| Field | Notes | +|-------|-------| +| `Code` | Unique, e.g. `"M5"` | +| `Name` | e.g. `"M-System"` | +| `IsActive` | bool | + +Default game system: Code=`M5`, Name=`M-System`. + +--- + +### Learning-Cost Models (`models/model_learning_costs.go`) + +| Type | Table | Purpose | +|------|-------|---------| +| `Source` | `gsm_lit_sources` | Rulebook/sourcebook references | +| `CharacterClass` | `gsm_character_classes` | Class codes (Kr, Ma, Hx …) + source link | +| `SkillCategory` | `learning_skill_categories` | Alltag, Freiland, Halbwelt, Kampf, Körper, Sozial, Unterwelt, Waffen, Wissen | +| `SkillDifficulty` | `learning_skill_difficulties` | leicht, normal, schwer, sehr schwer | +| `SpellSchool` | `learning_spell_schools` | Beherrschen, Bewegen, Erkennen, … | +| `ClassCategoryEPCost` | `learning_class_category_ep_costs` | EP per TE by class+category | +| `ClassSpellSchoolEPCost` | `learning_class_spell_school_ep_costs` | EP per LE by class+school | +| `SpellLevelLECost` | `learning_spell_level_le_costs` | LE required by spell level | +| `SkillCategoryDifficulty` | `learning_skill_category_difficulties` | LE cost for skill in category+difficulty | +| `WeaponSkillCategoryDifficulty` | (separate table) | LE cost for weapon skills | +| `SkillImprovementCost` | `learning_skill_improvement_costs` | TE to improve by current level | +| `ClassCategoryLearningPoints` | — | Starting LP per class+category for creation | +| `AuditLogEntry` | `audit_log_entries`(?) | EP/gold change audit trail | + +--- + +### Character Creation Session (`models/model_character_creation.go`) + +| Type | Table | Purpose | +|------|-------|---------| +| `CharacterCreationSession` | `char_creation_sessions` | Multi-step wizard persistence | + +Fields: UserID, Name, Rasse, Typ, Herkunft, Stand, Glaube, Attributes (JSON), DerivedValues (JSON), Skills (JSON), Spells (JSON), SkillPoints (JSON), CurrentStep, ExpiresAt. + +--- + +### Character Share (`models/model_character_share.go` → table: `char_shares`) + +| Field | Notes | +|-------|-------| +| `CharacterID` | FK to char_chars | +| `UserID` | FK to users (recipient) | +| `Permission` | `"read"` or `"write"` | + +--- + +### Database Schema/Migration Models (`database/model.go`) + +| Type | Table | +|------|-------| +| `SchemaVersion` | `schema_version` | +| `MigrationHistory` | `migration_history` | + +--- + +## 8. Module-by-Module Analysis + +--- + +### 8.1 `user/` + +**Purpose:** User account management, authentication, authorization, password reset. + +**Key Files:** +- `model.go` — User struct, CRUD methods, reset-hash helpers +- `handlers.go` — RegisterUser, LoginUser, CheckToken, AuthMiddleware, password reset +- `admin_handlers.go` — ListUsers, GetUser, UpdateUserRole, ChangeUserPassword, DeleteUser +- `middleware.go` — RequireRole, RequireAdmin, RequireMaintainer +- `database.go` — `MigrateStructure` for `users` table +- `routes.go` — Route registration + +**Business Logic:** +- Registration creates users with MD5-hashed passwords and a default `standard` role. +- Login returns a custom MD5 token containing the encoded user ID. +- Password reset uses a 32-byte random hex hash (secure) with a 14-day expiry, sent via SMTP. +- `AuthMiddleware` validates the token structure, loads the user from DB, injects `userID`/`username`/`user` into context. + +**HTTP Endpoints (protected unless noted):** + +| Method | Path | Handler | Auth level | +|--------|------|---------|-----------| +| POST | `/register` | `RegisterUser` | Public | +| POST | `/login` | `LoginUser` | Public | +| POST | `/password-reset/request` | `RequestPasswordReset` | Public | +| GET | `/password-reset/validate/:token` | `ValidateResetToken` | Public | +| POST | `/password-reset/reset` | `ResetPassword` | Public | +| GET | `/api/user/profile` | `GetUserProfile` | Auth | +| PUT | `/api/user/display-name` | `UpdateDisplayName` | Auth | +| PUT | `/api/user/email` | `UpdateEmail` | Auth | +| PUT | `/api/user/password` | `UpdatePassword` | Auth | +| PUT | `/api/user/language` | `UpdateLanguage` | Auth | +| GET | `/api/users` | `ListUsers` | Admin | +| GET | `/api/users/:id` | `GetUser` | Admin (own profile also allowed) | +| PUT | `/api/users/:id/role` | `UpdateUserRole` | Admin | +| PUT | `/api/users/:id/password` | `ChangeUserPassword` | Admin | +| DELETE | `/api/users/:id` | `DeleteUser` | Admin | + +--- + +### 8.2 `character/` + +**Purpose:** Core character management — CRUD, skill/spell learning system, character creation wizard, audit logging, sharing, practice points, PDF-relevant data prep. + +**Key Files:** +- `handlers.go` — ListCharacters, CreateCharacter, GetCharacter, UpdateCharacter, DeleteCharacter, ToFeChar, UpdateCharacterImage, GetDatasheetOptions +- `lerncost_handler.go` — `GetLernCostNewSystem`, `ImproveSkill`, `LearnSkill`, `LearnSpell` — full learning economy +- `derived_values_calculator.go` — `CalculateStaticFields`, `CalculateRolledField` — character creation math +- `creation_rules.go` — `GetSpecialAbilityByRoll` — special ability table +- `audit_log.go` — `CreateAuditLogEntry`, `GetAuditLogForCharacter/Field` +- `share_handlers.go` — `GetCharacterShares`, `UpdateCharacterShares`, `GetAvailableUsersForSharing` +- `practice_points_handler.go` — `GetPracticePoints`, `UpdatePracticePoints`, `AddPracticePoint`, `UsePracticePoint` +- `skill_type_helper.go` — skill categorization helpers +- `system_information_handlers.go` — `GetSkillCategoriesHandlerStatic` +- `spell_utils.go` — spell availability and creation utilities +- `database.go` — `SaveCharacterToDB` +- `image_handler.go` — character image upload/handling + +**Business Logic:** +- `ListCharacters`: Returns `{ self_owned: [], others: [] }` where `others` = public + shared. +- `GetCharacter`: Returns `FeChar` (augmented view with categorized skills). +- `toFeChar`: Adds bonus field from attributes, splits skills by category. +- `checkCharacterOwnership`: Guards update/delete/share with `character.UserID == c.GetUint("userID")`. +- **Learning System** (`GetLernCostNewSystem`): Calculates LE/TE/EP/Money cost for learning or improving a skill/spell/weapon. Considers: character class, skill category, difficulty, current level, practice points (PP) reductions, gold-for-EP conversion, reward types (free learning, half EP). +- **Derived Values**: `CalculateStaticFieldsLogic` implements Midgard 5th edition formulae for Ausdauer-, Schadens-, Angriffs-, Abwehr-, Zauber-Bonus, Resistenz, Abwehr, Zaubern, Raufen. + +**HTTP Endpoints (`/api/characters/...`):** + +| Method | Path | Handler | +|--------|------|---------| +| GET | `/api/characters` | `ListCharacters` | +| POST | `/api/characters` | `CreateCharacter` | +| GET | `/api/characters/:id` | `GetCharacter` | +| PUT/PATCH | `/api/characters/:id` | `UpdateCharacter` | +| DELETE | `/api/characters/:id` | `DeleteCharacter` | +| PUT | `/api/characters/:id/image` | `UpdateCharacterImage` | +| GET | `/api/characters/:id/datasheet-options` | `GetDatasheetOptions` | +| GET | `/api/characters/:id/shares` | `GetCharacterShares` | +| PUT | `/api/characters/:id/shares` | `UpdateCharacterShares` | +| GET | `/api/characters/:id/available-users` | `GetAvailableUsersForSharing` | +| GET | `/api/characters/:id/experience-wealth` | `GetCharacterExperienceAndWealth` | +| PUT | `/api/characters/:id/experience` | `UpdateCharacterExperience` | +| PUT | `/api/characters/:id/wealth` | `UpdateCharacterWealth` | +| GET | `/api/characters/:id/audit-log` | `GetCharacterAuditLog` | +| GET | `/api/characters/:id/audit-log/stats` | `GetAuditLogStats` | +| POST | `/api/characters/lerncost-new` | `GetLernCostNewSystem` | +| POST | `/api/characters/lerncost` | `GetLernCostNewSystem` | +| POST | `/api/characters/improve-skill-new` | `ImproveSkill` | +| POST | `/api/characters/improve-skill` | `ImproveSkill` | +| POST | `/api/characters/:id/learn-skill-new` | `LearnSkill` | +| POST | `/api/characters/:id/learn-skill` | `LearnSkill` | +| POST | `/api/characters/:id/learn-spell-new` | `LearnSpell` | +| POST | `/api/characters/:id/learn-spell` | `LearnSpell` | +| POST | `/api/characters/available-skills-new` | `GetAvailableSkillsNewSystem` | +| POST | `/api/characters/available-skills` | `GetAvailableSkillsNewSystem` | +| POST | `/api/characters/available-skills-creation` | `GetAvailableSkillsForCreation` | +| POST | `/api/characters/available-spells-creation` | `GetAvailableSpellsForCreation` | +| POST | `/api/characters/available-spells-new` | `GetAvailableSpellsNewSystem` | +| POST | `/api/characters/available-spells` | `GetAvailableSpellsNewSystem` | +| GET | `/api/characters/spell-details` | `GetSpellDetails` | +| GET | `/api/characters/:id/reward-types` | `GetRewardTypesStatic` | +| GET | `/api/characters/:id/practice-points` | `GetPracticePoints` | +| PUT | `/api/characters/:id/practice-points` | `UpdatePracticePoints` | +| POST | `/api/characters/:id/practice-points/add` | `AddPracticePoint` | +| POST | `/api/characters/:id/practice-points/use` | `UsePracticePoint` | +| GET | `/api/characters/skill-categories` | `GetSkillCategoriesHandlerStatic` | +| GET | `/api/characters/create-sessions` | `ListCharacterSessions` | +| POST | `/api/characters/create-session` | `CreateCharacterSession` | +| GET | `/api/characters/create-session/:sessionId` | `GetCharacterSession` | +| PUT | `/api/characters/create-session/:sessionId/basic` | `UpdateCharacterBasicInfo` | +| PUT | `/api/characters/create-session/:sessionId/attributes` | `UpdateCharacterAttributes` | +| PUT | `/api/characters/create-session/:sessionId/derived` | `UpdateCharacterDerivedValues` | +| PUT | `/api/characters/create-session/:sessionId/skills` | `UpdateCharacterSkills` | +| POST | `/api/characters/create-session/:sessionId/finalize` | `FinalizeCharacterCreation` | +| DELETE | `/api/characters/create-session/:sessionId` | `DeleteCharacterSession` | +| GET | `/api/characters/races` | `GetRaces` | +| GET | `/api/characters/classes` | `GetCharacterClasses` | +| GET | `/api/characters/classes/learning-points` | `GetCharacterClassLearningPoints` | +| GET | `/api/characters/origins` | `GetOrigins` | +| GET | `/api/characters/beliefs` | `SearchBeliefs` | +| POST | `/api/characters/calculate-static-fields` | `CalculateStaticFields` | +| POST | `/api/characters/calculate-rolled-field` | `CalculateRolledField` | + +--- + +### 8.3 `equipment/` + +**Purpose:** CRUD for character-owned equipment items and weapons. + +**Key Files:** `handlers.go`, `routes.go` + +**Business Logic:** +- `checkEquipmentOwnership`: Loads character by ID, compares `character.UserID` to logged-in user. +- All operations (CRUD) filter/verify ownership before executing DB writes. + +**HTTP Endpoints:** + +| Method | Path | Handler | +|--------|------|---------| +| POST | `/api/equipment` | `CreateAusruestung` | +| GET | `/api/equipment/character/:character_id` | `ListAusruestung` | +| PUT | `/api/equipment/:ausruestung_id` | `UpdateAusruestung` | +| DELETE | `/api/equipment/:ausruestung_id` | `DeleteAusruestung` | +| POST | `/api/weapons` | `CreateWaffe` | +| GET | `/api/weapons/character/:character_id` | `ListWaffen` | +| PUT | `/api/weapons/:waffe_id` | `UpdateWaffe` | +| DELETE | `/api/weapons/:waffe_id` | `DeleteWaffe` | + +--- + +### 8.4 `gsmaster/` + +**Purpose:** Game-system master data management — the reference library for skills, weapon skills, spells, equipment, weapons, and learning costs. + +**Key Files:** +- `handlers.go` — Generic get/add/update/delete using Go generics (`getMDItem[T]`, `getMDItems[T]`, etc.) +- `routes.go` — Public read endpoints + maintainer-guarded write endpoints +- `learning_costs.go` — `LearningCostsTable` with full EP/TE cost tables for all 15 character classes +- `learning_costs_init.go` — `ValidateLearningCostsData`, `GetLearningCostsSummary` +- `levelup.go` — `CalculateImprovementCost`, `CalculateSkillImprovementCost` +- `game_system.go` — Game system lookup helper +- `misclookup.go` — `GetMiscLookupByKey`, `GetMiscLookupByKeyForSystem` (races, faiths, origins, etc.) +- `skill_enhanced_handlers.go`, `spell_enhanced_handlers.go`, `weapon_enhanced_handlers.go`, `weapon_skill_enhanced_handlers.go`, `equipment_enhanced_handlers.go` — Enhanced CRUD with source/difficulty enrichment + +**Business Logic:** +- Learning costs are stored both as hardcoded Go maps (in `learning_costs.go`, 15 classes × 8-9 categories) and in the database (`ClassCategoryEPCost` tables). +- `GetMiscLookupByKey("faiths")` merges entries from both `gsm_misc_lookups` and `gsm_believes` tables. +- Generic handlers use interface constraints (`Creator`, `Saver`, `FirstIdGetter`, `Deleter`) via Go generics for DRY CRUD. + +**HTTP Endpoints (`/api/maintenance/...`):** + +| Method | Path | Handler | Auth Level | +|--------|------|---------|-----------| +| GET | `/api/maintenance` | `GetMasterData` | Auth | +| GET | `/api/maintenance/skills` | `GetMDSkills` | Auth | +| GET | `/api/maintenance/skills-enhanced` | `GetEnhancedMDSkills` | Auth | +| GET | `/api/maintenance/skills/:id` | `GetMDSkill` | Auth | +| GET | `/api/maintenance/skills-enhanced/:id` | `GetEnhancedMDSkill` | Auth | +| GET | `/api/maintenance/weaponskills` | `GetMDWeaponSkills` | Auth | +| GET | `/api/maintenance/weaponskills-enhanced` | `GetEnhancedMDWeaponSkills` | Auth | +| GET | `/api/maintenance/weaponskills/:id` | `GetMDWeaponSkill` | Auth | +| GET | `/api/maintenance/weaponskills-enhanced/:id` | `GetEnhancedMDWeaponSkill` | Auth | +| GET | `/api/maintenance/spells` | `GetMDSpells` | Auth | +| GET | `/api/maintenance/spells-enhanced` | `GetEnhancedMDSpells` | Auth | +| GET | `/api/maintenance/spells/:id` | `GetMDSpell` | Auth | +| GET | `/api/maintenance/spells-enhanced/:id` | `GetEnhancedMDSpell` | Auth | +| GET | `/api/maintenance/equipment` | `GetMDEquipments` | Auth | +| GET | `/api/maintenance/equipment-enhanced` | `GetEnhancedMDEquipment` | Auth | +| GET | `/api/maintenance/equipment/:id` | `GetMDEquipment` | Auth | +| GET | `/api/maintenance/equipment-enhanced/:id` | `GetEnhancedMDEquipmentItem` | Auth | +| GET | `/api/maintenance/weapons` | `GetMDWeapons` | Auth | +| GET | `/api/maintenance/weapons-enhanced` | `GetEnhancedMDWeapons` | Auth | +| GET | `/api/maintenance/weapons/:id` | `GetMDWeapon` | Auth | +| GET | `/api/maintenance/weapons-enhanced/:id` | `GetEnhancedMDWeapon` | Auth | +| POST | `/api/maintenance/skills-enhanced` | `CreateEnhancedMDSkill` | Maintainer | +| PUT | `/api/maintenance/skills/:id` | `UpdateMDSkill` | Maintainer | +| PUT | `/api/maintenance/skills-enhanced/:id` | `UpdateEnhancedMDSkill` | Maintainer | +| POST | `/api/maintenance/skills` | `AddSkill` | Maintainer | +| DELETE | `/api/maintenance/skills/:id` | `DeleteMDSkill` | Maintainer | +| PUT | `/api/maintenance/weaponskills/:id` | `UpdateMDWeaponSkill` | Maintainer | +| PUT | `/api/maintenance/weaponskills-enhanced/:id` | `UpdateEnhancedMDWeaponSkill` | Maintainer | +| POST | `/api/maintenance/weaponskills` | `AddWeaponSkill` | Maintainer | +| DELETE | `/api/maintenance/weaponskills/:id` | `DeleteMDWeaponSkill` | Maintainer | +| PUT | `/api/maintenance/spells/:id` | `UpdateMDSpell` | Maintainer | +| PUT | `/api/maintenance/spells-enhanced/:id` | `UpdateEnhancedMDSpell` | Maintainer | +| POST | `/api/maintenance/spells` | `AddSpell` | Maintainer | +| DELETE | `/api/maintenance/spells/:id` | `DeleteMDSpell` | Maintainer | +| PUT | `/api/maintenance/equipment/:id` | `UpdateMDEquipment` | Maintainer | +| PUT | `/api/maintenance/equipment-enhanced/:id` | `UpdateEnhancedMDEquipmentItem` | Maintainer | +| POST | `/api/maintenance/equipment` | `AddEquipment` | Maintainer | +| DELETE | `/api/maintenance/equipment/:id` | `DeleteMDEquipment` | Maintainer | +| PUT | `/api/maintenance/weapons/:id` | `UpdateMDWeapon` | Maintainer | +| PUT | `/api/maintenance/weapons-enhanced/:id` | `UpdateEnhancedMDWeapon` | Maintainer | +| POST | `/api/maintenance/weapons` | `AddWeapon` | Maintainer | +| DELETE | `/api/maintenance/weapons/:id` | `DeleteMDWeapon` | Maintainer | + +--- + +### 8.5 `gamesystem/` + +**Purpose:** Minimal package that handles migration of the `game_systems` table and seeds the initial Midgard 5 game system. + +**Key File:** `database.go` + +- `MigrateStructure(db)` — AutoMigrates `GameSystem`. +- `MigrateDataIfNeeded(db)` — Creates `{Code:"M5", Name:"M-System"}` if no row with ID=1 exists. + +No HTTP endpoints registered. + +--- + +### 8.6 `maintenance/` + +**Purpose:** Admin/maintainer tooling — DB health checks, data migrations, test data generation, SQLite→MariaDB transfer. + +**Key Files:** +- `handlers.go` — SetupCheck, MakeTestdataFromLive, ReconnectDataBase, ReloadENV, TransferSQLiteToMariaDB, migrateAllStructures +- `masterdata_handlers.go` — GetGameSystems, UpdateGameSystem, GetLitSources, UpdateLitSource, GetMisc, UpdateMisc, GetSkillImprovementCost2, UpdateSkillImprovementCost2 +- `believe_handlers.go` — GetBelieves, UpdateBelieve +- `skill_migration.go` — Skill-related migration helpers + +**Business Logic:** +- `migrateAllStructures` calls all module `MigrateStructure()` functions in correct dependency order. +- `MakeTestdataFromLive` exports current live DB to a SQLite file for test snapshots. +- `TransferSQLiteToMariaDB` copies records table-by-table from SQLite to MariaDB. +- All endpoints require `RequireMaintainer()` or `RequireAdmin()` — no public access. + +**HTTP Endpoints (`/api/maintenance/...` — Maintainer required):** + +| Method | Path | Handler | +|--------|------|---------| +| GET | `/api/maintenance/gsm-believes` | `GetBelieves` | +| PUT | `/api/maintenance/gsm-believes/:id` | `UpdateBelieve` | +| GET | `/api/maintenance/game-systems` | `GetGameSystems` | +| PUT | `/api/maintenance/game-systems/:id` | `UpdateGameSystem` | +| GET | `/api/maintenance/gsm-lit-sources` | `GetLitSources` | +| PUT | `/api/maintenance/gsm-lit-sources/:id` | `UpdateLitSource` | +| GET | `/api/maintenance/gsm-misc` | `GetMisc` | +| PUT | `/api/maintenance/gsm-misc/:id` | `UpdateMisc` | +| GET | `/api/maintenance/skill-improvement-cost2` | `GetSkillImprovementCost2` | +| PUT | `/api/maintenance/skill-improvement-cost2/:id` | `UpdateSkillImprovementCost2` | +| GET | `/api/maintenance/setupcheck` | `SetupCheck` | +| GET | `/api/maintenance/setupcheck-dev` | `SetupCheckDev` | +| GET | `/api/maintenance/mktestdata` | `MakeTestdataFromLive` | +| GET | `/api/maintenance/reconndb` | `ReconnectDataBase` | +| GET | `/api/maintenance/reloadenv` | `ReloadENV` | +| POST | `/api/maintenance/transfer-sqlite-to-mariadb` | `TransferSQLiteToMariaDB` | + +*(These overlap with the `gsmaster` maintenance prefix — both modules register under `/api/maintenance` with `gsmaster` first, then `maintenance` module adds its own routes. Both use `RequireMaintainer()` for writes.)* + +--- + +### 8.7 `pdfrender/` + +**Purpose:** Generate multi-page PDF character sheets from HTML templates using headless Chromium + pdfcpu merge. + +**Key Files:** +- `handlers.go` — `ListTemplates`, `ExportCharacterToPDF`, `CleanupExportTemp`, `GetPDFFile` +- `init.go` — `InitializeTemplates` — copies bundled templates to `TemplatesDir` on startup +- `chromedp.go` — `NewPDFRenderer`, `RenderHTMLToPDF` using chromedp +- `mapper.go` — `MapCharacterToViewModel` — maps `Char` to `CharacterSheetViewModel` +- `viewmodel.go` — All view model types (`CharacterSheetViewModel`, `CharacterInfo`, `AttributeValues`, `DerivedValueSet`, `SkillViewModel`, `WeaponViewModel`, `SpellViewModel`, `EquipmentViewModel`, `MagicItemViewModel`, `PageMeta`) +- `render_with_continuation.go` — `RenderPageWithContinuations` — auto-generates overflow pages +- `pagination.go` / `pagination_helper.go` — item distribution across pages +- `fill_capacity.go` — reads `` comments from HTML templates +- `template_parser.go` / `template_metadata.go` — template loading and Go `html/template` execution +- `inline_resources.go` — inlines CSS/images into HTML before rendering +- `file_management.go` — manages `xporttemp` directory + +**Business Logic:** +1. On startup: sync `./default_templates` → `cfg.TemplatesDir`. +2. `ExportCharacterToPDF`: + - Loads character, maps to view model. + - Renders pages 1–4 (`page_1.html` through `page_4.html`) with continuation logic. + - Each page checks `` to determine when to create a continuation page. + - All rendered PDFs are merged via `pdfcpu`. + - Saves to `xporttemp/` directory, returns `{"filename": "..."}`. +3. `GetPDFFile`: public endpoint, serves files from `xporttemp/` by filename. + +**HTTP Endpoints:** + +| Method | Path | Handler | Auth | +|--------|------|---------|------| +| GET | `/api/pdf/templates` | `ListTemplates` | Auth | +| GET | `/api/pdf/export/:id` | `ExportCharacterToPDF` | Auth | +| POST | `/api/pdf/cleanup` | `CleanupExportTemp` | Auth | +| GET | `/api/pdf/file/:filename` | `GetPDFFile` | Public | + +--- + +### 8.8 `importer/` + +**Purpose:** Import characters from VTT JSON format or CSV; export characters as VTT JSON, CSV, or spell CSV. + +**Key Files:** +- `handler.go` — `UploadFiles`, `ImportSpellCSVHandler` +- `importer.go` — `ImportChar`, `CheckSkill`, VTT import logic +- `importVTTJson.go` — `ImportVTTJSON` — main VTT import driver +- `exporter.go` — `ExportCharToVTT`, `ExportCharacterToCSV` +- `model.go` — `CharacterImport` struct (VTT format), sub-structs for Fertigkeiten/Spells/Equipment +- `routes.go` — Route registration + +**Business Logic:** +- `UploadFiles`: Accepts `file_vtt` (required) + `file_csv` (optional), validates extensions (.csv/.json), calls `ImportVTTJSON`. +- `CheckSkill`: Looks up skill in `gsm_skills`, auto-creates if `autocreate=true`. +- Source IDs are cached (`sourceIDCache`) to reduce DB queries during bulk import. +- `ExportCharacterVTTFileHandler`: Returns character data as downloadable `.json` file. + +**HTTP Endpoints:** + +| Method | Path | Handler | +|--------|------|---------| +| POST | `/api/importer/upload` | `UploadFiles` | +| POST | `/api/importer/spells/csv` | `ImportSpellCSVHandler` | +| GET | `/api/importer/export/vtt/:id` | `ExportCharacterVTTHandler` | +| GET | `/api/importer/export/vtt/:id/file` | `ExportCharacterVTTFileHandler` | +| GET | `/api/importer/export/csv/:id` | `ExportCharacterCSVHandler` | +| GET | `/api/importer/export/spells/csv` | `ExportSpellsCSVHandler` | + +--- + +### 8.9 `transfer/` + +**Purpose:** JSON-serialized character import/export and full database backup/restore. + +**Key Files:** +- `handlers.go` — HTTP handlers for all transfer operations +- `exporter.go` — `ExportCharacter` — serializes a full `Char` + metadata to `CharacterExport` +- `importer.go` — `ImportCharacter` — deserializes and recreates character in DB +- `database.go` — `ExportDatabase`, `ImportDatabase` — full table serialization +- `routes.go` — Route registration + +**Business Logic:** +- `ExportCharacter` / `ImportCharacter`: Full round-trip JSON serialization of characters with all associations. +- `ExportDatabase` / `ImportDatabase`: Table-level JSON snapshot used for backup/restore. +- `DownloadCharacterHandler` sets `Content-Disposition: attachment` for browser download. + +**HTTP Endpoints:** + +| Method | Path | Handler | +|--------|------|---------| +| GET | `/api/transfer/export/:id` | `ExportCharacterHandler` | +| GET | `/api/transfer/download/:id` | `DownloadCharacterHandler` | +| POST | `/api/transfer/import` | `ImportCharacterHandler` | +| POST | `/api/transfer/database/export` | `ExportDatabaseHandler` | +| POST | `/api/transfer/database/import` | `ImportDatabaseHandler` | + +--- + +### 8.10 `appsystem/` + +**Purpose:** Exposes application version and system info (user/character counts, DB version). + +**Key Files:** +- `version.go` — `Version = "0.2.4"`, `GetInfo()`, `GetInfo2()` +- `handlers.go` — `Versionsinfo`, `SystemInfo` +- `routes.go` — Route registration (both protected and public) + +**HTTP Endpoints:** + +| Method | Path | Handler | Auth | +|--------|------|---------|------| +| GET | `/api/version` | `Versionsinfo` | Auth | +| GET | `/api/systeminfo` | `SystemInfo` | Auth | +| GET | `/api/public/version` | `Versionsinfo` | Public | +| GET | `/api/public/systeminfo` | `SystemInfo` | Public | + +--- + +### 8.11 `logger/` + +**Purpose:** Custom leveled logger with timestamp prefixing. + +**Key API:** + +```go +logger.Debug(format, args...) +logger.Info(format, args...) +logger.Warn(format, args...) +logger.Error(format, args...) +logger.SetDebugMode(bool) +logger.SetMinLogLevel(logger.DEBUG | INFO | WARN | ERROR) +``` + +- Outputs to `os.Stdout` with format: `[YYYY-MM-DD HH:MM:SS] LEVEL: message` +- Debug messages are suppressed unless both `debugEnabled=true` AND `minLevel<=DEBUG`. +- Configured once in `main()` from `config.Cfg`. + +--- + +### 8.12 `mail/` + +**Purpose:** SMTP email client used exclusively for password-reset emails. + +**Key File:** `smtp.go` + +```go +type Client struct { + host, username, password, from string + port int +} + +func NewClient() *Client // reads from config.Cfg +func (c *Client) Send(msg Message) error +``` + +- Port 465: Direct TLS (`tls.Dial`) +- Port 587: STARTTLS (`smtp.Dial` + `TLSConfig`) +- If `host == ""`: logs the email content instead of sending (fallback for unconfigured environments). + +--- + +### 8.13 `router/` + +**Purpose:** Central Gin setup and base route group creation. + +**Files:** +- `setup.go` — `SetupGin(r)` — CORS middleware +- `routes.go` — `BaseRouterGrp(r)` — public auth routes + protected `/api` group +- `routes_test.go` — router tests + +--- + +### 8.14 `config/` + +**Purpose:** Application configuration loading and validation. + +**Key Functions:** +- `init()` — auto-loads config on package import +- `LoadConfig()` — reads `.env`/`.env.local`, then env vars +- `loadEnvFile()` / `loadEnvFileContent()` — parse `KEY=VALUE` lines +- `cfg.IsProduction() bool` +- `cfg.GetServerAddress() string` — returns `":PORT"` format + +--- + +### 8.15 `database/` + +**Purpose:** Database connection management, test helpers, and migration execution. + +**Key Functions:** +- `ConnectDatabase()` — routes to `SetupTestDB()` or `ConnectDatabaseOrig()` based on env +- `ConnectDatabaseOrig()` — opens MySQL or SQLite with GORM +- `SetupTestDB(opts ...bool)` — copies snapshot `testdata/prepared_test_data.db` → temp dir, opens it +- `MigrateStructure(db)` — migrates `schema_version` + `migration_history` +- `MigrateDataIfNeeded(db)` — ensures schema version record exists +- `GetDB()` — lazy init getter + +**Path Constants:** +```go +PreparedTestDB = filepath.Join(backendDir, "testdata", "prepared_test_data.db") +TestDataDir = filepath.Join(backendDir, "maintenance", "testdata") +``` + +--- + +### 8.16 `testutils/` + +**Purpose:** Shared test environment setup utility. + +```go +func SetupTestEnvironment(t *testing.T) +func SetupTestEnvironmentWithConfig(t *testing.T, envVars map[string]string) +func EnsureTestEnvironment(t *testing.T) +``` + +- Sets `ENVIRONMENT=test` to redirect DB connection to SQLite test snapshot. +- Registers `t.Cleanup()` to restore original env vars after each test. + +--- + +### 8.17 `api/` + +**Purpose:** Integration tests that test multi-module interactions via full HTTP stack. + +Key test: `TestListCharacters` — spins up Gin with all essential routes, uses `database.SetupTestDB(true)`. + +--- + +## 9. Complete API Route Listing + +### Public (No Authentication) + +| Method | Path | Module | Purpose | +|--------|------|--------|---------| +| POST | `/register` | user | Register a new user | +| POST | `/login` | user | Login, get token | +| POST | `/password-reset/request` | user | Request password reset email | +| GET | `/password-reset/validate/:token` | user | Validate reset token | +| POST | `/password-reset/reset` | user | Set new password | +| GET | `/api/pdf/file/:filename` | pdfrender | Download generated PDF | +| GET | `/api/public/version` | appsystem | App version (no auth) | +| GET | `/api/public/systeminfo` | appsystem | System info (no auth) | + +### Protected — All Authenticated Users (`/api/...`) + +**User Management:** + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/api/user/profile` | Own profile | +| PUT | `/api/user/display-name` | Update display name | +| PUT | `/api/user/email` | Update email | +| PUT | `/api/user/password` | Change password | +| PUT | `/api/user/language` | Set preferred language | + +**Characters:** + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/api/characters` | List own + shared + public characters | +| POST | `/api/characters` | Create character | +| GET | `/api/characters/:id` | Get full character (FeChar) | +| PUT/PATCH | `/api/characters/:id` | Update character | +| DELETE | `/api/characters/:id` | Delete character | +| PUT | `/api/characters/:id/image` | Upload character image | +| GET | `/api/characters/:id/datasheet-options` | PDF datasheet options | +| GET | `/api/characters/:id/shares` | List share recipients | +| PUT | `/api/characters/:id/shares` | Update share recipients | +| GET | `/api/characters/:id/available-users` | Users available to share with | +| GET | `/api/characters/:id/experience-wealth` | Current EP + wealth | +| PUT | `/api/characters/:id/experience` | Update experience points | +| PUT | `/api/characters/:id/wealth` | Update wealth | +| GET | `/api/characters/:id/audit-log` | Change history | +| GET | `/api/characters/:id/audit-log/stats` | Change statistics | +| POST | `/api/characters/lerncost` | Calculate learn/improve cost | +| POST | `/api/characters/lerncost-new` | (Alias) Calculate learn/improve cost | +| POST | `/api/characters/improve-skill` | Apply skill improvement | +| POST | `/api/characters/improve-skill-new` | (Alias) Apply skill improvement | +| POST | `/api/characters/:id/learn-skill` | Learn new skill | +| POST | `/api/characters/:id/learn-skill-new` | (Alias) Learn new skill | +| POST | `/api/characters/:id/learn-spell` | Learn new spell | +| POST | `/api/characters/:id/learn-spell-new` | (Alias) Learn new spell | +| POST | `/api/characters/available-skills` | Available skills to learn | +| POST | `/api/characters/available-skills-new` | (Alias) | +| POST | `/api/characters/available-skills-creation` | Skills for creation wizard | +| POST | `/api/characters/available-spells` | Available spells to learn | +| POST | `/api/characters/available-spells-new` | (Alias) | +| POST | `/api/characters/available-spells-creation` | Spells for creation wizard | +| GET | `/api/characters/spell-details` | Spell detail lookup | +| GET | `/api/characters/:id/reward-types` | Reward options for this context | +| GET | `/api/characters/:id/practice-points` | Current practice points | +| PUT | `/api/characters/:id/practice-points` | Update practice points | +| POST | `/api/characters/:id/practice-points/add` | Add a practice point | +| POST | `/api/characters/:id/practice-points/use` | Use a practice point | +| GET | `/api/characters/skill-categories` | Static skill category listing | +| GET | `/api/characters/create-sessions` | List creation wizard sessions | +| POST | `/api/characters/create-session` | Start creation wizard session | +| GET | `/api/characters/create-session/:sessionId` | Get session data | +| PUT | `/api/characters/create-session/:sessionId/basic` | Save basic info | +| PUT | `/api/characters/create-session/:sessionId/attributes` | Save attributes | +| PUT | `/api/characters/create-session/:sessionId/derived` | Save derived values | +| PUT | `/api/characters/create-session/:sessionId/skills` | Save skills selection | +| POST | `/api/characters/create-session/:sessionId/finalize` | Finalize creation | +| DELETE | `/api/characters/create-session/:sessionId` | Discard session | +| GET | `/api/characters/races` | Available races | +| GET | `/api/characters/classes` | Available character classes | +| GET | `/api/characters/classes/learning-points` | LP by class | +| GET | `/api/characters/origins` | Available origins | +| GET | `/api/characters/beliefs` | Faith search | +| POST | `/api/characters/calculate-static-fields` | Calc derived values (no dice) | +| POST | `/api/characters/calculate-rolled-field` | Calc derived value (with dice) | + +**Equipment:** + +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/api/equipment` | Add equipment item | +| GET | `/api/equipment/character/:character_id` | List equipment for character | +| PUT | `/api/equipment/:ausruestung_id` | Update equipment | +| DELETE | `/api/equipment/:ausruestung_id` | Delete equipment | +| POST | `/api/weapons` | Add weapon | +| GET | `/api/weapons/character/:character_id` | List weapons for character | +| PUT | `/api/weapons/:waffe_id` | Update weapon | +| DELETE | `/api/weapons/:waffe_id` | Delete weapon | + +**Master Data (read — all authenticated, write — maintainers only):** + +| Method | Path | Auth | Purpose | +|--------|------|------|---------| +| GET | `/api/maintenance` | Auth | All master data overview | +| GET | `/api/maintenance/skills` | Auth | List skills | +| GET | `/api/maintenance/skills/:id` | Auth | Get skill | +| GET | `/api/maintenance/skills-enhanced` | Auth | List skills (rich) | +| GET | `/api/maintenance/skills-enhanced/:id` | Auth | Get skill (rich) | +| POST | `/api/maintenance/skills` | Maintainer | Create skill | +| POST | `/api/maintenance/skills-enhanced` | Maintainer | Create skill (rich) | +| PUT | `/api/maintenance/skills/:id` | Maintainer | Update skill | +| PUT | `/api/maintenance/skills-enhanced/:id` | Maintainer | Update skill (rich) | +| DELETE | `/api/maintenance/skills/:id` | Maintainer | Delete skill | +| GET | `/api/maintenance/weaponskills[/:id]` | Auth | Weapon skill(s) | +| GET | `/api/maintenance/weaponskills-enhanced[/:id]` | Auth | Weapon skill(s) (rich) | +| POST/PUT/DELETE | `/api/maintenance/weaponskills[/:id]` | Maintainer | Weapon skill CRUD | +| GET | `/api/maintenance/spells[/:id]` | Auth | Spell(s) | +| GET | `/api/maintenance/spells-enhanced[/:id]` | Auth | Spell(s) (rich) | +| POST/PUT/DELETE | `/api/maintenance/spells[/:id]` | Maintainer | Spell CRUD | +| GET | `/api/maintenance/equipment[/:id]` | Auth | Equipment item(s) | +| GET | `/api/maintenance/equipment-enhanced[/:id]` | Auth | Equipment (rich) | +| POST/PUT/DELETE | `/api/maintenance/equipment[/:id]` | Maintainer | Equipment CRUD | +| GET | `/api/maintenance/weapons[/:id]` | Auth | Master weapon(s) | +| GET | `/api/maintenance/weapons-enhanced[/:id]` | Auth | Master weapon(s) (rich) | +| POST/PUT/DELETE | `/api/maintenance/weapons[/:id]` | Maintainer | Weapon CRUD | + +**Maintenance Admin (Maintainer required):** + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/api/maintenance/gsm-believes` | List faith entries | +| PUT | `/api/maintenance/gsm-believes/:id` | Update faith entry | +| GET | `/api/maintenance/game-systems` | List game systems | +| PUT | `/api/maintenance/game-systems/:id` | Update game system | +| GET | `/api/maintenance/gsm-lit-sources` | List source books | +| PUT | `/api/maintenance/gsm-lit-sources/:id` | Update source book | +| GET | `/api/maintenance/gsm-misc` | MiscLookup entries | +| PUT | `/api/maintenance/gsm-misc/:id` | Update misc entry | +| GET/PUT | `/api/maintenance/skill-improvement-cost2[/:id]` | Improvement cost table | +| GET | `/api/maintenance/setupcheck` | DB structure validation | +| GET | `/api/maintenance/setupcheck-dev` | Extended setup check | +| GET | `/api/maintenance/mktestdata` | Generate test snapshot from live DB | +| GET | `/api/maintenance/reconndb` | Reconnect database | +| GET | `/api/maintenance/reloadenv` | Reload .env config | +| POST | `/api/maintenance/transfer-sqlite-to-mariadb` | Migrate data | + +**PDF:** + +| Method | Path | Auth | Purpose | +|--------|------|------|---------| +| GET | `/api/pdf/templates` | Auth | List available PDF templates | +| GET | `/api/pdf/export/:id` | Auth | Export character to PDF | +| POST | `/api/pdf/cleanup` | Auth | Clean up old PDFs | +| GET | `/api/pdf/file/:filename` | Public | Download PDF file | + +**Import/Export:** + +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/api/importer/upload` | Import character from VTT JSON | +| POST | `/api/importer/spells/csv` | Import spells from CSV | +| GET | `/api/importer/export/vtt/:id` | Export character as VTT JSON (response body) | +| GET | `/api/importer/export/vtt/:id/file` | Export character as downloadable VTT JSON | +| GET | `/api/importer/export/csv/:id` | Export character as CSV | +| GET | `/api/importer/export/spells/csv` | Export all spells as CSV | +| GET | `/api/transfer/export/:id` | Export character as BaMoRT JSON | +| GET | `/api/transfer/download/:id` | Download character as JSON file | +| POST | `/api/transfer/import` | Import character from BaMoRT JSON | +| POST | `/api/transfer/database/export` | Full DB export to JSON | +| POST | `/api/transfer/database/import` | Full DB import from JSON | + +**System:** + +| Method | Path | Auth | Purpose | +|--------|------|------|---------| +| GET | `/api/version` | Auth | App version + git commit | +| GET | `/api/systeminfo` | Auth | Version + user/char counts + DB version | + +**Admin (Admin role required):** + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/api/users` | List all users | +| GET | `/api/users/:id` | Get user by ID | +| PUT | `/api/users/:id/role` | Change user role | +| PUT | `/api/users/:id/password` | Change user password | +| DELETE | `/api/users/:id` | Delete user | + +--- + +## 10. Testing Approach + +### Test Environment Setup + +Every test file that touches the database must call: + +```go +func setupTestEnvironment(t *testing.T) { + original := os.Getenv("ENVIRONMENT") + os.Setenv("ENVIRONMENT", "test") + t.Cleanup(func() { os.Setenv("ENVIRONMENT", original) }) +} +``` + +Or the shared utility: +```go +testutils.SetupTestEnvironment(t) +``` + +This causes `database.ConnectDatabase()` to open the SQLite test snapshot instead of MariaDB. + +### Test Database Mechanism + +1. `testdata/prepared_test_data.db` — a SQLite snapshot with real-world test data. +2. `database.SetupTestDB(true)` copies this to a `os.MkdirTemp` location and opens it. +3. Tests run against this isolated copy — no pollution of live data. +4. Character ID 18 ("Fanjo Vetrani") is guaranteed to exist in the test DB. + +### Test Structure Conventions + +- All test files use `_test.go` suffix. +- Test functions call `database.SetupTestDB(true)` or `testutils.SetupTestEnvironment(t)` first. +- Integration tests use `testify/assert`. +- Many module tests (`character_test.go`, `handlers_test.go`, etc.) use helper constructors from `test_data_helper.go`. +- The `api/api_test.go` package runs HTTP tests using `httptest.NewRecorder()`. +- `maintenance/copy_db_test.go` tests the DB snapshot refresh mechanism. +- Test data helpers (`character/test_data_helper.go`) provide `CreateTestCharacter()`, etc. + +### Key Test Files by Module + +| Module | Test Files | +|--------|-----------| +| `api/` | `api_test.go` — Integration | +| `character/` | `character_test.go`, `handlers_test.go`, `lerncost_handler_test.go`, `skill_update_test.go`, `learn_spell_test.go`, etc. | +| `database/` | `database_test.go`, `testhelper_test.go` | +| `gsmaster/` | `gsmaster_test.go`, `learning_costs_test.go`, `export_import_test.go` | +| `importer/` | `importer_test.go`, `charimport_test.go`, `upload_test.go` | +| `maintenance/` | `handlers_test.go`, `masterdata_handlers_test.go`, `copy_db_test.go` | +| `models/` | `model_character_test.go`, `model_char_skills_test.go`, `model_learning_costs_test.go` | +| `pdfrender/` | `chromedp_test.go`, `mapper_test.go`, `pagination_test.go`, `integration_test.go` | +| `transfer/` | `database_test.go`, `exporter_test.go`, `importer_test.go` | +| `user/` | `handlers_test.go`, `role_test.go` | + +--- + +## 11. Key Dependencies + +| Dependency | Version | Purpose | +|-----------|---------|---------| +| `github.com/gin-gonic/gin` | v1.10.0 | HTTP framework | +| `github.com/gin-contrib/cors` | v1.7.3 | CORS middleware | +| `gorm.io/gorm` | v1.25.12 | ORM | +| `gorm.io/driver/mysql` | v1.5.7 | MySQL/MariaDB driver | +| `gorm.io/driver/sqlite` | v1.5.7 | SQLite driver (tests + dev) | +| `github.com/chromedp/chromedp` | v0.14.2 | Headless Chrome for PDF rendering | +| `github.com/pdfcpu/pdfcpu` | v0.11.1 | PDF merging | +| `github.com/stretchr/testify` | v1.10.0 | Test assertions | + +--- + +## 12. Security Observations + +### Critical Issues + +1. **Weak Token Authentication**: The custom token embeds the user ID in a predictable location. Any token with any valid user ID at position `7 + len("Bearer ")` is accepted. There is **no HMAC or digital signature**. An attacker can forge tokens. + +2. **MD5 Password Hashing**: Passwords are stored as MD5 hashes. MD5 is cryptographically broken for password storage; brute-force and rainbow-table attacks are trivial. Should be replaced with bcrypt, argon2id, or scrypt. + +3. **Token Leaks User ID**: The token format encodes the user ID in plaintext, revealing enumerable user IDs to clients. + +### Moderate Concerns + +4. **Debug Logging of Passwords**: `handlers.go` has `fmt.Printf("pwdh: %s", ...)` that prints password hashes to stdout on every login — a security logging issue. + +5. **Hardcoded Origins in CORS**: Local network IPs (`192.168.0.48`, `192.168.0.36`) are hardcoded — appropriate for development but should be env-driven for production. + +6. **Importer File Upload**: `UploadFiles` saves files to `./uploads/` relative path without sanitizing filenames for path traversal. The extension validation (`isValidFileType`) only checks the suffix. + +7. **SQL Injection via `fieldName`**: `GetMiscLookupByKeyForSystem` constructs `query.Order(orderBy)` from a user-supplied `order` parameter — however the value is mapped to a safe set of constants before use, so actual injection risk is mitigated. + +8. **No rate limiting**: Login and registration endpoints have no rate limiting, making them susceptible to brute-force attacks. + +### Notes + +- The bcrypt implementation is commented out in `handlers.go` with the MD5 hash in use — this is intentional but documents the known deficit. +- Password reset tokens use `crypto/rand` (secure, 32 bytes), which is correct. +- GORM parameterized queries protect most DB operations from SQL injection. + +--- + +## 13. Cross-Module Relationships Diagram + +``` +cmd/main.go + ├── config ──────────────────────────────────── read by all modules + ├── logger ──────────────────────────────────── used by all modules + ├── database ─────────────────────────────────── .DB used by all models + │ └── testutils ────────────────────────── test env setup + ├── router + │ └── user (AuthMiddleware, role guards) + ├── user ─────────────────── handlers, model, auth + │ └── mail (SMTP for password reset) + ├── models ────────────────── GORM entities, MigrateStructure + │ ├── user.User (FK in Char) + │ └── database.DB (queries) + ├── character ─────────────── CRUD + learning + creation wizard + │ ├── models (Char, SkFertigkeit, SkZauber, etc.) + │ ├── gsmaster (LerncostRequest, skill lookups) + │ └── database.DB + ├── equipment ─────────────── Equipment/Weapon CRUD + │ ├── models (EqAusruestung, EqWaffe) + │ └── database.DB + ├── gsmaster ──────────────── Master data (skills/spells/weapons) + │ ├── models (Skill, Spell, Weapon, etc.) + │ ├── user (RequireMaintainer guard) + │ └── database.DB + ├── gamesystem ────────────── GameSystem seed/migration + │ └── models.GameSystem + ├── maintenance ───────────── Admin tooling, migration orchestration + │ ├── database, gamesystem, user, models (all MigrateStructure) + │ └── user (RequireMaintainer guard) + ├── pdfrender ─────────────── HTML→PDF pipeline + │ ├── models.Char (character data) + │ └── config (TemplatesDir, ExportTempDir) + ├── importer ──────────────── VTT/CSV import/export + │ └── models (Char, Skill, Spell) + ├── transfer ──────────────── JSON character + DB backup + │ └── models.Char + └── appsystem ─────────────── Version info + └── database.DB (count queries) +``` + +--- + +*End of findings — /home/de31a2/bamort/backend_findings.md* diff --git a/frontend_findings.md b/frontend_findings.md new file mode 100644 index 0000000..9f68629 --- /dev/null +++ b/frontend_findings.md @@ -0,0 +1,1563 @@ +# BaMoRT Frontend — Comprehensive Code Analysis + +_Generated: 2026-03-13_ + +--- + +## Table of Contents + +1. [Project Setup](#1-project-setup) +2. [Application Entry](#2-application-entry) +3. [Router](#3-router) +4. [Pinia Stores](#4-pinia-stores) +5. [Views (src/views/)](#5-views-srcviews) +6. [Components (src/components/)](#6-components-srccomponents) +7. [API Layer](#7-api-layer) +8. [i18n](#8-i18n) +9. [Styling](#9-styling) +10. [Business Logic in Frontend](#10-business-logic-in-frontend) +11. [User Flows](#11-user-flows) +12. [Component Hierarchy](#12-component-hierarchy) + +--- + +## 1. Project Setup + +### `package.json` + +``` +name: bamort-frontend +version: 0.2.3 +type: module +``` + +**Runtime dependencies:** +| Package | Version | Purpose | +|---|---|---| +| `vue` | ^3.5.13 | Core framework (Options API) | +| `vue-router` | ^4.5.0 | Client-side routing | +| `pinia` | ^2.3.0 | State management | +| `vue-i18n` | ^11.0.0 | Internationalisation (DE + EN) | +| `axios` | ^1.7.9 | HTTP client | + +**Dev dependencies:** +| Package | Version | Purpose | +|---|---|---| +| `vite` | ^6.0.1 | Build tool / dev server | +| `@vitejs/plugin-vue` | ^5.2.1 | Vite Vue 3 plugin | +| `vite-plugin-vue-devtools` | ^7.6.5 | Browser devtools integration | + +No testing framework, no TypeScript compiler (`.ts` files exist only as type declaration stubs). + +### `vite.config.js` + +```js +export default defineConfig({ + plugins: [vue(), vueDevTools()], + resolve: { + alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } + }, + server: { + host: ['bamort.trokan.de', '192.168.0.48', 'localhost', 'terra.local'], + } +}) +``` + +Key notes: +- `@` alias maps to `src/` +- Dev server explicitly bound to multiple hostnames (network-accessible) +- Minification and source maps are commented out (disabled) +- No Vite proxy configuration — all API calls go directly to the configured `VITE_API_URL` + +### `index.html` + +Single-page app root. Mounts `#app` and loads `src/main.js` as an ES module. Uses `favicon.png`. + +### `src/version.js` + +```js +export const VERSION = '0.2.3' +export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown' +``` + +Exposes `getVersion()` and `getGitCommit()` consumed by `LandingView` and `SystemInfoView`. + +--- + +## 2. Application Entry + +**`src/main.js`** + +```js +import './assets/main.css' +import { createApp } from "vue" +import { createPinia } from 'pinia' +import App from "./App.vue" +import router from "./router" +import { i18n } from './stores/languageStore' +import UtilsPlugin from './utils/utilsPlugin' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.use(i18n) +app.use(UtilsPlugin) +app.mount("#app") +``` + +Order of registration: +1. Pinia (state store) +2. Vue Router +3. vue-i18n instance (imported from `languageStore.js`, not created here) +4. `UtilsPlugin` (global properties: date helpers + dice rollers) + +**`src/App.vue`** + +Root component. Shows `` only when the user is logged-in (detected via `localStorage.getItem('token')`). The main `` is wrapped in a `
` that gets the CSS class `full-width` when the user is not logged in (centering forms on the page). Listens to `storage` and custom `auth-changed` window events to reactively update `loggedIn`. + +### `UtilsPlugin` (`src/utils/utilsPlugin.js`) + +Installs these global properties on the Vue app instance: + +| Property | Source | Description | +|---|---|---| +| `$formatDate` | `dateUtils.js` | Format ISO date to locale string | +| `$formatDateTime` | `dateUtils.js` | Format ISO datetime | +| `$formatRelativeDate` | `dateUtils.js` | Relative time (e.g. "2 hours ago") | +| `$safeValue` | `dateUtils.js` | Return value or fallback | +| `$capitalize` | `dateUtils.js` | Capitalize string | +| `$rollDie` | `randomUtils.js` | Roll single die (1–N) | +| `$rollDice` | `randomUtils.js` | Roll multiple dice → array | +| `$rollDiceWithSum` | `randomUtils.js` | Roll + return sum | +| `$rollNotation` | `randomUtils.js` | Parse RPG notation e.g. `"3d6+2"` | +| `$randomBetween` | `randomUtils.js` | Random int in range | +| `$randomChoice` | `randomUtils.js` | Pick random element from array | +| `$shuffleArray` | `randomUtils.js` | Fisher-Yates shuffle | + +--- + +## 3. Router + +**`src/router/index.js`** — `createWebHistory` (HTML5 mode, no hash). + +### Route Table + +| Path | Name | Component | Auth? | Admin? | +|---|---|---|---|---| +| `/` | `Landing` | `LandingView` | — | — | +| `/login` | `Login` | `LoginView` | — | — | +| `/register` | `Register` | `RegisterView` | — | — | +| `/forgot-password` | `ForgotPassword` | `ForgotPasswordView` | — | — | +| `/reset-password` | `ResetPassword` | `ResetPasswordView` | — | — | +| `/dashboard` | `Dashboard` | `DashboardView` | ✓ | — | +| `/profile` | `UserProfile` | `UserProfileView` | ✓ | — | +| `/users` | `UserManagement` | `UserManagementView` | ✓ | ✓ | +| `/ausruestung/:characterId` | `Ausruestung` | `AusruestungView` | ✓ | — | +| `/maintenance` | `Maintenance` | `MaintenanceView` | ✓ | — | +| `/upload` | `FileUpload` | `FileUploadPage` | ✓ | — | +| `/sponsors` | `Sponsors` | `SponsorsView` | — | — | +| `/help` | `Help` | `HelpView` | — | — | +| `/system-info` | `SystemInfo` | `SystemInfoView` | — | — | +| `/character/:id` | `CharacterDetails` | `CharacterDetails` | ✓ | — | +| `/character/create/:sessionId` | `CharacterCreation` | `CharacterCreation` | ✓ | — | + +**Static imports** (loaded immediately — needed for unauthenticated pages): `LandingView`, `LoginView`, `RegisterView`, `ForgotPasswordView`, `ResetPasswordView`. + +**Lazy-loaded** (code-split into separate chunks on first access): all other views and both character components. + +Route params are passed as props for `CharacterDetails` (`id`) and `CharacterCreation` (`sessionId`). + +### Navigation Guard + +```js +router.beforeEach(async (to, from, next) => { + if (to.meta.requiresAuth && !isLoggedIn()) { + next({ name: "Login" }) + } else if (to.meta.requiresAdmin) { + const userStore = useUserStore() + if (!userStore.currentUser) await userStore.fetchCurrentUser() + if (!userStore.isAdmin) next({ name: "Dashboard" }) + else next() + } else { + next() + } +}) +``` + +`isLoggedIn()` simply checks `localStorage.getItem("token")`. The admin check lazy-loads the user profile if it hasn't been fetched yet. + +--- + +## 4. Pinia Stores + +### `userStore` (`src/stores/userStore.js`) + +```js +defineStore('user', { + state: () => ({ currentUser: null, isLoading: false }), + getters: { + isAuthenticated: state => !!state.currentUser, + userRole: state => state.currentUser?.role || 'standard', + isAdmin: state => state.currentUser?.role === 'admin', + isMaintainer: state => state.currentUser?.role === 'maintainer' + || state.currentUser?.role === 'admin', + isStandardUser: state => !!state.currentUser + }, + actions: { + async fetchCurrentUser() { /* GET /api/user/profile */ }, + clearUser() { this.currentUser = null } + } +}) +``` + +- Fetches profile from `GET /api/user/profile`. +- On successful fetch, also updates the i18n locale from `profile.preferred_language` and persists it to `localStorage`. +- `currentUser` shape: `{ id, username, display_name, email, role, preferred_language }`. +- Used by: `Menu.vue`, `CharacterDetails.vue`, `UserManagementView.vue`, router guard. + +### `languageStore` (`src/stores/languageStore.js`) + +```js +export const i18n = createI18n({ + legacy: false, + locale: localStorage.getItem('language') || 'de', + fallbackLocale: 'en', + messages: { de, en } +}) + +defineStore('language', { + state: () => ({ currentLanguage: localStorage.getItem('language') || 'de' }), + actions: { + setLanguage(lang) { + this.currentLanguage = lang + i18n.global.locale.value = lang + localStorage.setItem('language', lang) + } + } +}) +``` + +Unusually, the `i18n` instance is **created and exported from this file**, then imported in `main.js`. Default language: `de`. + +### `characterCreationStore` (`src/stores/characterCreation.js`) + +Manages the multi-step character wizard session state in-memory (backed by server-side session). + +**State:** + +```js +{ + sessionData: { + basic_info: null, + attributes: null, + derived_values: null, + skills: [], + skills_meta: { totalUsedPoints: 0, selectedCategory: null }, + spells: [], + equipment: null + }, + currentStep: 1, // 1–5 + sessionId: null, + isLoading: false, + error: null +} +``` + +**Getters:** `characterClass`, `characterStand`, `characterRace`, `hasSelectedSkills`, `totalSkillPoints`, `isValid` (checks basic_info + attributes + derived_values are all non-null). + +**Actions:** `initializeSession(sessionId?)`, `createNewSession()`, `loadSession(sessionId)` (TODO), `saveSession()` (TODO), `updateStepData(data)`, `goToStep(n)`, `nextStep()`, `previousStep()`, `clearSession()`. + +Note: `saveSession()` and `loadSession()` are marked TODO — the actual session persistence is handled directly by `CharacterCreation.vue` via the API, bypassing this store. + +### `counter` (`src/stores/counter.ts`) + +A boilerplate Pinia counter store from Vue scaffolding. Not used in the application. + +--- + +## 5. Views (`src/views/`) + +Most views are thin wrappers that delegate to a single component. + +### `LandingView.vue` + +**Purpose:** Public landing page. Displays app logo, description, version info, and action links. + +**Data fetched:** `GET ${VITE_API_URL}/api/public/version` (unauthenticated, uses raw `axios` not the `API` utility). Retries every 5 s up to 24 times if the backend is unreachable. + +**Logic:** +- `isBackendAvailable` computed: disables the login button until the backend responds. +- Clears retry interval on `beforeUnmount`. +- Shows frontend version from `version.js` (`0.2.3`) and backend version from API. + +**Components used:** None (self-contained). + +--- + +### `LoginView.vue` + +Thin wrapper: renders ``. + +--- + +### `RegisterView.vue` + +Thin wrapper: renders ``. + +--- + +### `ForgotPasswordView.vue` + +Thin wrapper: renders ``. + +--- + +### `ResetPasswordView.vue` + +Thin wrapper: renders ``. + +--- + +### `DashboardView.vue` + +Thin wrapper: renders ``. + +--- + +### `CharacterView.vue` + +Thin wrapper (legacy): passes `$route.params.characterId` to `` as `:characterId`. Route `/character/:id` uses `CharacterDetails` directly via lazy import; this view appears unused by the router. + +--- + +### `AusruestungView.vue` + +Passes `$route.params.characterId` to ``. Legacy/simple standalone equipment list for a character — separate from the main character detail equipment tab. + +--- + +### `MaintenanceView.vue` + +Thin wrapper: renders ``. + +--- + +### `UserProfileView.vue` + +**Purpose:** Self-service profile management for the logged-in user. + +**API calls:** +- `GET /api/user/profile` (on created) +- `PUT /api/user/display-name` — change display name (max 30 chars) +- `PUT /api/user/language` — change preferred language (updates i18n locale + localStorage) +- `PUT /api/user/email` — change email +- `PUT /api/user/password` — change password (requires current + new + confirm) + +**Validation:** +- Display name: max 30 chars +- Password: min 6 chars, must match confirmation +- Email: must differ from current, type="email" +- Duplicate email check via backend error message parsing + +**Sections rendered:** User info display, Change Display Name, Change Language, Change Email, Change Password. + +**After language update:** directly sets `this.$i18n.locale` and `localStorage.language`. Also updates `userStore.currentUser.display_name` after display name change. + +--- + +### `UserManagementView.vue` + +**Purpose:** Admin panel for managing all users. + +**Guard:** Route has `requiresAdmin: true`. + +**API calls:** +- `GET /api/users` — list all users +- `PUT /api/users/:id/role` — change role (`standard` | `maintainer` | `admin`) +- `DELETE /api/users/:id` — delete user +- `PUT /api/users/:id/password` — admin password reset (no current password required) + +**Dialogs (modal overlays):** +1. Role change dialog — select from standard/maintainer/admin +2. Delete confirmation dialog +3. Password change dialog (min 6 chars, must confirm) + +**Protection:** Buttons disabled for the current logged-in user's own row (cannot self-demote or self-delete). Detected via `currentUser.id === user.id` from `userStore`. + +--- + +### `FileUploadPage.vue` + +**Purpose:** Import character data from VTT (Virtual Tabletop) JSON or CSV files. + +**API call:** `POST /api/importer/upload` with `multipart/form-data`. + +**Validation:** Checks MIME types (`application/json`, `text/csv`). VTT file is required; CSV is optional. + +**Note:** Manually adds `Authorization` header even though the API interceptor already does this — a redundancy. + +--- + +### `HelpView.vue` + +Static content page. Dynamically loads FAQ items from i18n keys `help.faq1.question`/`.answer`, `help.faq2...`, etc., iterating until a key is not found using `$te()`. + +--- + +### `SponsorsView.vue` + +Static content page listing contributors and special thanks. Links to GitHub and Ko-fi. + +--- + +### `SystemInfoView.vue` + +**Purpose:** Displays system/technology information and live backend status. + +**API call:** `GET ${VITE_API_URL}/api/public/systeminfo` (uses raw `axios`). Returns `{ version, gitCommit, userCount, charCount, dbVersion }`. + +**Technology stack display:** Hardcoded lists of Vue 3, Vite, Go 1.25, Gin, GORM, MariaDB, JWT, chromedp, Docker. + +--- + +## 6. Components (`src/components/`) + +### `Menu.vue` + +**Purpose:** Fixed top navigation bar (60px high, z-index 1000). + +**Authentication-dependent rendering:** +- Always visible: Home, Info dropdown (Help, Sponsors, System Info) +- Logged-in only: Characters dropdown (Dashboard, Import Data) +- Maintainer/Admin only: Admin dropdown (Maintenance, User Management) +- Not logged-in only: Register link +- Right side (logged-in): User icon dropdown (Profile, Logout) + +**Hover dropdowns:** Implemented with `@mouseenter`/`@mouseleave` + 200 ms `setTimeout` delay to prevent premature closing. + +**Logout flow:** calls `logout()` from `auth.js` (removes token), clears `userStore`, dispatches `auth-changed` window event, navigates to `/`. + +**Auth change listener:** `window.addEventListener('auth-changed', ...)` — re-fetches user profile on login or clears on logout. + +**Props/emits:** None. + +--- + +### `LoginForm.vue` + +**Purpose:** Login form. + +**API call:** `POST /login` with `{ username, password }`. On success: stores JWT in `localStorage`, calls `userStore.fetchCurrentUser()`, dispatches `auth-changed`, navigates to `/dashboard`. + +**Error handling:** Displays "Invalid credentials" on any error. + +**Note:** Hardcoded English labels — does not use `$t()` for the form labels (unlike most other components). + +--- + +### `RegisterForm.vue` + +**Purpose:** Registration form. + +**Validation:** Client-side password confirmation match check before API call. + +**API call:** `POST /register` with `{ username, password, email }`. + +**i18n:** Fully translated via `auth.*` keys. + +--- + +### `ForgotPasswordForm.vue` + +**Purpose:** Request a password reset email. + +**API call:** `POST /password-reset/request` with `{ email, redirect_url: window.location.origin }`. + +**UX:** Shows success state (hides form, shows info text) on success without redirecting. Shows error message on failure. + +--- + +### `ResetPasswordForm.vue` + +Handles the password-reset token link (not examined in detail — delegates to ``). + +--- + +### `CharacterList.vue` + +**Purpose:** Dashboard character listing with creation session management. + +**API calls:** +- `GET /api/characters` → `{ self_owned: [...], others: [...] }` +- `GET /api/characters/create-sessions` → `{ sessions: [...] }` +- `POST /api/characters/create-session` → `{ session_id }` — starts new character wizard +- `DELETE /api/characters/create-session/:sessionId` + +**Layout:** +- "Create New Character" button (green dashed border) +- `` sub-component (shows in-progress wizards) +- Two column layout: owned characters | shared characters +- Each character shows: name, race, class, grade, owner, public/private badge +- Character names are `` to `/character/:id` + +**Props/emits:** None (standalone). + +**Sub-components:** `CharacterCreationSessions`. + +--- + +### `CharacterCreationSessions.vue` + +**Purpose:** Shows active character creation sessions (drafts) on the dashboard. + +**Props:** `sessions: Array` + +**Emits:** `continue-session(sessionId)`, `delete-session(sessionId)` + +**Renders:** Grid of cards, one per session. Shows name, step progress (e.g. "Step 2/5"), race/class, last updated, expires. Clicking a card card navigates via `continue-session` emit. + +--- + +### `CharacterDetails.vue` + +**Purpose:** Main character management hub. Fetches character data and dynamically renders sub-views through a tab-like submenu. + +**Props:** `id` (string, from route) + +**API calls:** +- `GET /api/characters/:id` (on created + after any edit via `refreshCharacter`) + +**Computed:** `isOwner` — compares `character.user_id` with `userStore.currentUser.id`. + +**Sub-views rendered via `` with `character` and `isOwner` props:** +1. `DatasheetView` (default) +2. `SkillView` +3. `WeaponView` +4. `SpellView` +5. `EquipmentView` +6. `ExperianceView` +7. `DeleteCharView` + +**Additional dialogs:** +- 📄 Export button → `ExportDialog` +- 🌐/🔒 Visibility button (owner only) → `VisibilityDialog` + +**Event handling:** `@character-updated` from sub-views triggers `refreshCharacter()` which reloads from API. + +--- + +### `DatasheetView.vue` + +**Purpose:** Character stats and biographical information display with inline editing. + +**Props:** `character: Object` (required) + +**Emits:** `character-updated` + +**Sub-components:** `ImageUploadCropper` + +**Inline editing mechanism:** Double-click (`@dblclick`) on any displayed value activates an input/select in its place. On blur or Enter, `saveEdit(path)` / `saveProp(prop)` PATCHes the character via `PUT /api/characters/:id`. On Escape, `cancelEdit()` restores display. + +**Stat fields (left column stat box):** + +| Label key | Character path | +|---|---| +| `stats.beauty` | `eigenschaften.0.value` | +| `stats.dexterity` | `eigenschaften.1.value` | +| `stats.agility` | `eigenschaften.2.value` | +| `stats.intelligence` | `eigenschaften.3.value` | +| `stats.constitution` | `eigenschaften.4.value` | +| `stats.charisma` | `eigenschaften.5.value` | +| `stats.strength` | `eigenschaften.6.value` | +| `stats.willpower` | `eigenschaften.7.value` | +| `stats.spelltalent` | `eigenschaften.8.value` | +| `stats.poisontolerance` | `git` | +| `stats.movement` | `b.max` | +| `stats.lifepoints` | `lp.max` | +| `stats.staminapoints` | `ap.max` | +| `stats.divinegrace` | `bennies.gg` | +| `stats.fatesfavor` | `bennies.sg` | + +**Character info fields (editable):** name, typ (class), gender (select), grad, rasse (select), origin (select), social_class (select), spezialisierung (select), alter, hand (select: rechts/links/beidhändig), groesse, gewicht, merkmale.groesse, merkmale.breite, merkmale.augenfarbe, merkmale.haarfarbe, glaube (multi-select), merkmale.sonstige. + +**Select options:** loaded lazily via `GET /api/characters/:id/datasheet-options` (only when a select-type field is first double-clicked). + +**`glaube` (beliefs) field:** Multi-select stored as comma-separated string; displayed and edited as array. + +**Path traversal for saving:** `path.split('.')` reduces into nested character object, then `PUT /api/characters/:id` with full character payload. + +--- + +### `SkillView.vue` + +**Purpose:** Display and manage character skills (Fertigkeiten + Waffenfertigkeiten) and innate skills. + +**Props:** `character: Object` (required), `isOwner: Boolean` + +**Emits:** `character-updated` + +**Sub-components:** `SkillImproveDialog`, `SkillLearnDialog` + +**Learning mode toggle:** 🎓 button visible to owner. When active: +- Resources bar (EP + Gold) shown +- Additional action buttons: 📚 Learn New, ➕ Add Skill +- Each skill row shows ⬆️ Improve button + +**Table structure:** +- Regular skills: grouped by category (`character.categorizedskills`) — name, EW (Fertigkeitswert), bonus, PP (Praxispunkte), note +- Weapon skills: from `character.waffenfertigkeiten` +- Innate skills: from `character.InnateSkills` (right column, name + value only) + +**PP (Praxispunkte) adjustment:** +/− buttons directly call `POST /api/characters/practice-points`. + +**Inline skill editing:** Double-click on name, fertigkeitswert, or bemerkung → inline input; saved via `PUT /api/characters/:skill_id/skill`. + +**Dialogs:** +1. `SkillLearnDialog` — learn new skill (complex, see below) +2. Improve Selection Dialog — simple modal; selects skill + PP count → `POST /api/characters/improve-skill` +3. Add Skill Dialog — manually add skill by name/value → `POST /api/characters/:id/add-skill` +4. `SkillImproveDialog` — detailed multi-level improvement dialog + +--- + +### `SkillImproveDialog.vue` + +**Purpose:** Detailed skill improvement dialog supporting multiple reward types and level sequences. + +**Props:** `character: Object`, `skill: Object`, `isVisible: Boolean`, `learningType: 'improve'|'learn'|'spell'` + +**Emits:** `close`, `skill-updated` + +**Features:** +- Loads available improvement levels + costs from `GET /api/characters/lerncost-newsystem` or similar +- Loads reward types from API +- Shows live resource tracking (EP remaining, Gold remaining, PP remaining) +- Multi-level selection (click/deselect in sequence — must be consecutive) +- Reward types: `default` (EP + Gold), `noGold` (EP only), `halveepnoGold` (half EP), `pp` (PP only), `mixed` (EP + PP) +- Submits via `POST /api/characters/improve-skill-newsystem` or similar + +--- + +### `SkillLearnDialog.vue` + +**Purpose:** Learn a brand-new skill (not yet on the character). + +**Features:** +- Drag & drop (HTML5 native) or click-to-select skills from available list to "to learn" list +- Category filter buttons (all categories shown as chips) +- Search filter input +- Sort by name or EP cost (ascending/descending toggle) +- Reward type selector: Standard (EP + Gold) or Nur EP (no gold) +- Live EP/Gold remaining display +- Multi-skill batch learning (multiple skills can be queued) +- Submits batch via `POST /api/characters/learn-skills` (or similar) + +--- + +### `SpellView.vue` + +**Purpose:** Display character spells. + +**Props:** `character: Object`, `isOwner: Boolean` + +**Emits:** `character-updated` + +**Sub-components:** `SpellLearnDialog` + +**Table:** name, description, bonus, source. + +**Learning:** Owner sees 🎓 button → opens `SpellLearnDialog`. + +--- + +### `SpellLearnDialog.vue` + +**Purpose:** Learn new spells with filtering, sorting, and multi-spell queuing. + +**Features:** +- School (Schule) filter buttons +- Search term filter +- Sort by name, EP cost, Gold cost +- Reward type selector (loads from API) +- Available/selected two-column layout +- Per-spell affordability check +- Batch spell learning queue with total cost summary +- Submits via `POST /api/characters/learn-spells` (or similar) + +--- + +### `WeaponView.vue` + +**Purpose:** Display and manage character weapons. + +**Props:** `character: Object`, `isOwner: Boolean` + +**Emits:** `character-updated` + +**Table:** name (`*` suffix if magical), description, weight, value, amount, contained_in, bonus (anb/abwb). + +**Add weapon dialog:** +- Loads master weapon list from `GET /api/maintenance/weapons` (or similar) +- Text search filter +- Select + set amount +- Submits via `POST /api/equipment` (or similar weapon endpoint) + +**Edit weapon dialog:** +- Inline form for: description, is_magical checkbox, amount, value, attack bonus (anb), defense bonus (abwb), damage bonus (schb) +- Saves via `PUT /api/equipment/:id` (or similar) + +**Delete weapon:** `DELETE /api/equipment/:id` with confirm dialog using `$t('weapon.confirmDelete')` with `{name}` interpolation. + +--- + +### `EquipmentView.vue` + +**Purpose:** Display and manage character non-weapon equipment. + +**Props:** `character: Object`, `isOwner: Boolean` + +**Emits:** `character-updated` + +**Table:** name, description, weight, value, amount, contained_in, bonus. + +**Add equipment dialog:** +- Loads master equipment from `GET /api/maintenance/equipment` +- Text search filter +- Select + set amount +- Submits:`POST /api/equipment` with `{ character_id, name, beschreibung, gewicht, wert, anzahl, bonus, beinhaltet_in, contained_in, container_type }` + +**Delete:** `DELETE /api/equipment/:id` with i18n confirm using `{name}` interpolation. + +--- + +### `ExperianceView.vue` + +**Purpose:** Manage EP (experience points) and gold/wealth, with audit log. + +**Props:** `character: Object`, `isOwner: Boolean` + +**Sub-components:** `AuditLogView` + +**Computed:** `totalWealthInGS` — currency conversion: +```js +goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10) +``` +(1 GS = 10 SS = 10 KS — Midgard RPG currency system) + +**EP management:** Add/remove EP inputs → `PUT /api/characters/:id/experience` or similar. + +**Gold management:** Add/remove gold inputs → `PUT /api/characters/:id/wealth` or similar. + +After both operations: calls `this.$refs.auditLog.loadAuditLog()` to refresh the audit log. + +--- + +### `AuditLogView.vue` + +**Purpose:** Display change history (EP/gold transactions) for a character. + +**Props:** `character: Object` + +**API:** `GET /api/characters/:id/audit-log` with query params for field filter and date range. + +**Filters:** +- Field: all / experience_points / gold / silver / copper +- Date range: all time / today / this week / this month / custom (from–to date inputs) +- Group by date toggle + +**Statistics section:** total_changes, ep_spent, ep_gained, gold_spent, gold_gained. + +**Entry display:** Shows old → new value with difference (+/−), reason, optional notes. Color-coded: green for positive, red for negative changes. + +--- + +### `ExportDialog.vue` + +**Purpose:** Multi-format character export modal. + +**Props:** `characterId`, `showDialog: Boolean` + +**Emits:** `update:showDialog`, `export-success` + +**Computed:** `canExport` — false if no format selected, or PDF format but no template selected. + +**On created:** loads templates from `GET /api/pdf/templates`. + +**Export formats:** + +| Format | API Call | Delivery | +|---|---|---| +| PDF | `GET /api/pdf/export/:id?template=X&showUserName=true` → returns `{ filename }`, then opens `GET /api/pdf/file/:filename` in new tab | Browser window popup | +| VTT | `GET /api/importer/export/vtt/:id/file` (blob) | Programmatic download link | +| BaMoRT (JSON) | `GET /api/transfer/download/:id` (blob) | Programmatic download link | + +**PDF export note:** Gets filename from API first, then constructs full URL from `API.defaults.baseURL + /api/pdf/file/:filename`. Uses `window.open(url, '_blank')` synchronously (not before an async call) — avoids popup blockers per project convention. + +--- + +### `VisibilityDialog.vue` + +**Purpose:** Change character visibility (private/public) and manage per-user sharing. + +**Props:** `characterId`, `currentVisibility: Boolean`, `showDialog: Boolean` + +**Emits:** `update:showDialog`, `visibility-updated(isPublic)` + +**Watches:** `showDialog` — on open, loads available users and current shares. + +**API calls:** +- `GET /api/characters/:id/available-users` — users eligible to share with +- `GET /api/characters/:id/shares` (implied) — currently shared users +- `PUT /api/characters/:id/visibility` + share update + +**Features:** Radio buttons for private/public. Searchable user list (filter by display name, username, email). Two-panel: "Add Users" | "Currently Shared With" with remove buttons. + +--- + +### `DeleteCharView.vue` + +**Purpose:** Delete character confirmation panel (inside character detail submenu). + +**Props:** `character: Object`, `isOwner: Boolean` + +**Emits:** `deleted`, `cancel` + +**Behavior:** Shows "not authorized" message for non-owners. For owners: confirmation text + "Yes, Delete" button → `DELETE /api/characters/:id` → navigates to `/dashboard`. + +--- + +### `ImageUploadCropper.vue` + +**Purpose:** Upload and crop a character portrait image. + +**Props:** `characterId` + +**Emits:** `image-updated` + +**Features:** +- File picker restricted to `image/*` +- HTML5 Canvas cropping with mouse drag selection +- Two crop shapes: rectangular or circular +- Live preview canvas (400×400) +- `POST /api/characters/:id/image` with FormData containing crop coordinates + +**Implementation:** Uses `FileReader` → `Image` → two `` elements (source + preview). Mouse events track drag selection rectangle. On save, crops the selection to a new canvas and uploads as FormData. + +--- + +### `Maintenance.vue` + +**Purpose:** Master data management hub for game system admins/maintainers. + +**API call:** `GET /api/maintenance` — loads all master data at once into `mdata` object. + +**`mdata` shape:** `{ skills, skillcategories, weaponskills, spells, spellcategories, equipment, weapons }` + +**Sub-views** (selected via bottom submenu): +1. `maintenance/SkillView` (default) +2. `maintenance/SpellView` +3. `maintenance/EquipmentView` +4. `maintenance/WeaponView` +5. `maintenance/WeaponSkillView` +6. `maintenance/BelieveView` +7. `maintenance/GameSystemView` +8. `maintenance/LitSourceView` +9. `maintenance/MiscLookupView` +10. `maintenance/SkillImprovementCostView` + +All sub-views receive `mdata` as prop. + +--- + +### `maintenance/SkillView.vue` + +**Purpose:** CRUD for skill master data. + +**Features:** +- Full text search + multi-field filters (category, difficulty, improvable, innate, bonus property) +- Sortable table (name, category columns) +- Inline row editing (click row → edit form appears in table) +- Create new skill inline +- API: `POST /api/maintenance/skills`, `PUT /api/maintenance/skills/:id`, `DELETE /api/maintenance/skills/:id` + +(Other maintenance views follow the same pattern for their respective domains.) + +--- + +### `CharacterCreation.vue` + +**Purpose:** Multi-step character creation wizard. + +**Props:** `sessionId: String` (required, from route) + +**5-step wizard:** +1. `CharacterBasicInfo` — name, origin, belief, gender, race, class, social class +2. `CharacterAttributes` — 7 core attributes (1–100 range, roll support) +3. `CharacterDerivedValues` — calculated + adjustable secondary stats +4. `CharacterSkills` — point-buy skill selection with category budgets +5. `CharacterSpells` — spell selection → finalize character + +**API calls:** +- `GET /api/characters/create-session/:sessionId` — load session state +- `GET /api/characters/skill-categories` — available skill categories with point budgets +- Step-specific save: `PUT /api/characters/create-session/:id/basic-info` etc. +- `POST /api/characters/create` — finalize (step 5) +- `DELETE /api/characters/create-session/:id` — delete draft + +**Session persistence:** Each `handleNext(data)` saves the updated step data to the backend before incrementing `currentStep`. Session reloaded after each save to ensure consistent state. + +**Expiry info:** Session expiry date shown at bottom. "Delete Draft" button available throughout. + +--- + +### `CharacterCreation/CharacterBasicInfo.vue` + +**Purpose:** Step 1 of the wizard. Collects fundamental character identity. + +**Fields:** Name (2–50 chars, required), Origin (select from API list), Belief/Glaube (typeahead search from API, minimum 2 chars to activate), Gender (male/female), Race (select), Class/Typ (select), Social Class (select or dice roll). + +**Dice roll for social class:** 🎲 button (requires class to be selected first). Rolls `1d100` with class-specific modifier and maps to social class tier. Shows result in overlay that dismisses on click. + +**Races:** loaded from backend (API call for character creation options). + +**Emits:** `next(formData)`, `save(formData)` + +**Validation:** `isValid` computed checks all required fields are non-empty. + +--- + +### `CharacterCreation/CharacterAttributes.vue` + +**Purpose:** Step 2 — set the 7 base attributes (St, Gs, Gw, Ko, In, Zt, Au) each 1–100. + +**Features:** +- Individual roll buttons using `max(2d100)` formula per attribute (except Au) +- 🎲 "Roll All" button +- Race-specific Beauty (Au) restrictions: Elves ≥ 81, Gnomes/Dwarves ≤ 80 +- Shows total points sum and average +- Disabled state display (shows "ENABLED"/"DISABLED" status indicator) + +--- + +### `CharacterCreation/CharacterDerivedValues.vue` + +**Purpose:** Step 3 — calculate and optionally override secondary stats. + +**Fields:** PA (personal charisma), WK (willpower), LP max (life points), AP max (adventure points), B max (movement), resistances, defense, bonuses, SG (fate's favor), GG (divine grace), GP (luck points). + +**Dice formulas displayed in i18n keys:** +- PA = 1d100 + 4×(In/10) − 20 +- WK = 1d100 + 2×(Ko/10 + In/10) − 20 +- LP = 1d3 + 7 + (Ko/10) +- AP = 3d6 + class modifier +- B = 1d6 + race modifier + +**Recalculate button:** Calls backend to recalculate from attributes. + +**Individual roll buttons** per derived value with tooltip showing formula. + +--- + +### `CharacterCreation/CharacterSkills.vue` + +**Purpose:** Step 4 — point-buy skill selection with category budgets. + +**Props:** `sessionData`, `skillCategories` + +Each category has a point budget (`max_points`). Skills have costs. Emits `next`, `previous`, `save`. + +--- + +### `CharacterCreation/CharacterSpells.vue` + +**Purpose:** Step 5 — optional spell selection before finalization. + +**Props:** `sessionData`, `skillCategories` + +**Emits:** `previous`, `finalize(finalData)`, `save` + +Finalize calls `POST /api/characters/create` with complete session data. On success navigates to the new character's detail view. + +--- + +### `AusruestungList.vue` (legacy) + +**Purpose:** Simple standalone equipment list (old design). Loaded via `/ausruestung/:characterId` route. + +**API:** `GET /ausruestung/:characterId` (note: no `/api` prefix — likely a legacy endpoint). + +**Sub-component:** `AusruestungForm` + +--- + +### `AusruestungForm.vue` (legacy) + +Simple form to add equipment name/quantity/weight. Posts to `/ausruestung` (no `/api` prefix). + +--- + +### `LanguageSwitcher.vue` + +Marked as **"not used anymore"** in a comment in the template. A simple `