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