* introduced central package registry by package init function * dynamic registration of routes, model, migrations and initializers. * setting a docker compose project name to prevent shutdown of other containers with the same (composer)name * ai documentation * app template * Create tests for ALL API entpoints in ALL packages Based on current data. Ensure that all API endpoints used in frontend are tested. These tests are crucial for the next refactoring tasks. * adopting agent instructions for a more consistent coding style * added desired module layout and debugging information * Fix All Failing tests All failing tests are fixed now that makes the refactoring more easy since all tests must pass * restored routes for maintenance * added common translations * added new tests for API Endpoint * Merge branch 'separate_business_logic' * added lern and skill improvement cost editing * Set Docker image tag when building to prevent rebuild when nothing has changed * add and remove PP for Weaponskill fixed * add and remove PP for same named skills fixed * add new task
55 KiB
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
- Project Purpose and Domain
- High-Level Architecture
- Technology Stack
- Backend Architecture
- Frontend Architecture
- Core Business Logic
- Data Access and Persistence
- Module Interaction Map
- Complete API Route Reference
- Infrastructure and Deployment
- User Roles and Access Control
- Internationalisation (i18n)
- 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:
- Load configuration — reads
.env/.env.localthen environment variable overrides viaconfig.Cfg(auto-loaded inconfig.init()) - Configure logger — sets log level (
DEBUG,INFO,WARN,ERROR) and debug mode based on config - Set Gin mode —
gin.ReleaseModein production,gin.DebugModeotherwise - Connect database — calls
database.ConnectDatabase()which selects MySQL or SQLite based onDATABASE_TYPEandENVIRONMENT - Initialize PDF templates — copies default templates from
/app/default_templatestocfg.TemplatesDirif needed viapdfrender.InitializeTemplates() - Set up Gin engine — calls
router.SetupGin(r)for CORS config - Register routes — each module calls
RegisterRoutes(protected)on the protected/apigroup - Register public routes — pdfrender and appsystem register unauthenticated endpoints
- 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 + createdAtwith 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 theAuthorizationheader - 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, setsuserID,username,userin the Gin context - Returns
401 Unauthorizedif 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 defaultstandardroleLoginUser— validates credentials, returns a tokenAuthMiddleware— protects all/api/*routes; injects user contextRequireAdmin()/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 skillsGET /api/maintenance/spells— list all spellsGET /api/maintenance/weapons— list all weaponsGET /api/maintenance/equipment— general equipmentGET /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:
ExportCharacterToPDFreceives a character ID and template name- Loads the character from the database (fully preloaded)
- 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
- Merges all page PDFs with
pdfcpuinto one file - Saves to
cfg.ExportTempDirand returns the filename - 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 templatesPOST /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 integrityGET /api/maintenance/setupcheck-dev— extended dev-mode diagnosticsGET /api/maintenance/reconndb— reconnect database (useful after connection interruption)GET /api/maintenance/reloadenv— reload environment variables at runtimePOST /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:
- Pinia — state store (must be first)
- Vue Router — client-side routing
- vue-i18n — i18n instance created and exported from
stores/languageStore.js - 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 whenlocalStorage.getItem('token')is truthy - Applies
.full-widthCSS class on<main>for non-logged-in pages (centers login/register forms) - Reacts to
storageandauth-changedwindow events to updateloggedInstate 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):
- If route requires auth and user is not logged in → redirect to
/login - If route requires admin role → lazy-load
userStore, fetch profile if not loaded, checkisAdmingetter → redirect to/dashboardif not admin - 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:
- Fetches full character data from
GET /api/characters/:id - Determines
isOwnerby comparingcharacter.user_idvsuserStore.currentUser.id - Renders a dynamic sub-component (
<component :is="currentView">) driven by a submenu - Passes
characterandisOwneras props to every sub-view - Listens for
@character-updatedevents 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 downloadVisibilityDialog— toggle character between public/private (owner only)
Learning Dialogs
SkillLearnDialog.vue, SkillImproveDialog.vue, SpellLearnDialog.vue are modal dialogs that:
- Call
POST /api/characters/lerncost-newwith skill name, current level, type, and action - Display the computed cost breakdown (EP, TE, LE, gold, PP reduction)
- 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>fromlocalStorageautomatically - Response interceptor: On
401, removes the stale token fromlocalStorageand 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 forSkillViewrenderingInnateSkills— 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:
- Look up the character's class abbreviation (
Typ) - Normalize the skill/spell name (trim, lowercase for lookup)
- Find the skill in
gsmastermaster data to getCategoryandDifficulty - 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
- For skills:
- Apply practice points (PP) reduction: each PP reduces required TE by 1 (capped at max PP available for category)
- Apply reward bonuses if specified:
free_learning— zero costfree_spell_learning— zero cost for spellshalf_ep_improvement— halve EP cost when improvinggold_for_ep— substitute up to half the EP with 10 GS each
- Return full cost breakdown plus
CanAffordflag 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 <!-- 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_pointsGET /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_URLenv var. - Tests: SQLite (via CGO).
testutils.SetupTestDB()copiestestdata/prepared_test_data.dbto 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:
game_systems- GSMaster tables (skills, spells, equipment, weapons, containers, believes, misc lookups)
- Character tables (chars, attributes, skills, spells, equipment sub-tables)
- Equipment tables
- Skill learning cost tables
- 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):
characterdepends ongsmaster(skill/spell lookup for cost calculations)characterdepends onmodels(all entity types)characterdepends ondatabase(direct DB access)gsmasterdepends onmodelsanddatabasepdfrenderdepends onmodelsanddatabase(loads full character)maintenancedepends ongsmaster,models,databaseuseris depended on by all modules viaAuthMiddlewareimport intorouter
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.tomlfor hot-reload on.gofile 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/*.sqlon 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 buildwithVITE_*build args baked in. Stage 2 (nginx:alpine) serves the static bundle. VITE_API_URLis 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:
isOwnerprop controls visibility of edit/delete buttons inCharacterDetails
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:
- User's stored preference in
localStorage.language - User's
preferred_languagefield from their profile (set onfetchCurrentUser()) - Fallback to
de
Language change flow:
- User changes language in
UserProfileView→PUT /api/user/language - Frontend immediately sets
this.$i18n.localeand updateslocalStorage.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.
- 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. - No token signature verification —
CheckTokenextracts 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. - 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.
- 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.shcontains 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.vuemanually adds anAuthorizationheader that is already added by the Axios interceptor — this is a redundancy, not a security issue.- The production
DATABASE_URLindocker-compose.ymlis constructed inline without fallback defaults — missing env vars will produce an empty credentials DSN.