# 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.