Files
bamort/Bamort_detailed_project_documentation.md
T
2026-04-01 15:16:12 +02:00

55 KiB
Raw Blame History

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
  2. High-Level Architecture
  3. Technology Stack
  4. Backend Architecture
  5. Frontend Architecture
  6. Core Business Logic
  7. Data Access and Persistence
  8. Module Interaction Map
  9. Complete API Route Reference
  10. Infrastructure and Deployment
  11. User Roles and Access Control
  12. Internationalisation (i18n)
  13. Known Limitations and Security Notes

1. Project Purpose and Domain

Bamort (BaMoRT = Basic MoRphing Tool) 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 modegin.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 serverr.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.goGenerateToken):

  • 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.goCheckToken):

  • 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 <!-- MaxItems: N --> 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 <Menu /> only when localStorage.getItem('token') is truthy
  • Applies .full-width CSS class on <main> 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 (15), 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
LoginViewLoginForm /login Login form; on success stores token in localStorage, triggers auth-changed event
RegisterViewRegisterForm /register Registration form
ForgotPasswordViewForgotPasswordForm /forgot-password Sends password-reset email request
ResetPasswordViewResetPasswordForm /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
DashboardViewCharacterList /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
MaintenanceViewMaintenance /maintenance Maintainer tools: database checks, master data management
FileUploadPage /upload Import VTT JSON + optional CSV
AusruestungViewAusruestungList /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 (<component :is="currentView">) 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 <token> 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:

{
  "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 5660 → "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 <!-- MaxItems: N --> 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/<filename>, '_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:

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

# 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 UserProfileViewPUT /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:

{{ $t('skill.name') }}           <!-- in template -->
this.$t('error.notFound')        <!-- in script -->
this.$te('help.faq3.question')   <!-- existence check for dynamic FAQ -->

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