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

55 KiB
Raw Blame History

BaMoRT Frontend — Comprehensive Code Analysis

Generated: 2026-03-13


Table of Contents

  1. Project Setup
  2. Application Entry
  3. Router
  4. Pinia Stores
  5. Views (src/views/)
  6. Components (src/components/)
  7. API Layer
  8. i18n
  9. Styling
  10. Business Logic in Frontend
  11. User Flows
  12. Component Hierarchy

1. Project Setup

package.json

name:    bamort-frontend
version: 0.2.3
type:    module

Runtime dependencies:

Package Version Purpose
vue ^3.5.13 Core framework (Options API)
vue-router ^4.5.0 Client-side routing
pinia ^2.3.0 State management
vue-i18n ^11.0.0 Internationalisation (DE + EN)
axios ^1.7.9 HTTP client

Dev dependencies:

Package Version Purpose
vite ^6.0.1 Build tool / dev server
@vitejs/plugin-vue ^5.2.1 Vite Vue 3 plugin
vite-plugin-vue-devtools ^7.6.5 Browser devtools integration

No testing framework, no TypeScript compiler (.ts files exist only as type declaration stubs).

vite.config.js

export default defineConfig({
  plugins: [vue(), vueDevTools()],
  resolve: {
    alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }
  },
  server: {
    host: ['bamort.trokan.de', '192.168.0.48', 'localhost', 'terra.local'],
  }
})

Key notes:

  • @ alias maps to src/
  • Dev server explicitly bound to multiple hostnames (network-accessible)
  • Minification and source maps are commented out (disabled)
  • No Vite proxy configuration — all API calls go directly to the configured VITE_API_URL

index.html

Single-page app root. Mounts #app and loads src/main.js as an ES module. Uses favicon.png.

src/version.js

export const VERSION = '0.2.3'
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'

Exposes getVersion() and getGitCommit() consumed by LandingView and SystemInfoView.


2. Application Entry

src/main.js

import './assets/main.css'
import { createApp } from "vue"
import { createPinia } from 'pinia'
import App from "./App.vue"
import router from "./router"
import { i18n } from './stores/languageStore'
import UtilsPlugin from './utils/utilsPlugin'

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(i18n)
app.use(UtilsPlugin)
app.mount("#app")

Order of registration:

  1. Pinia (state store)
  2. Vue Router
  3. vue-i18n instance (imported from languageStore.js, not created here)
  4. UtilsPlugin (global properties: date helpers + dice rollers)

src/App.vue

Root component. Shows <Menu /> only when the user is logged-in (detected via localStorage.getItem('token')). The main <router-view /> is wrapped in a <main> that gets the CSS class full-width when the user is not logged in (centering forms on the page). Listens to storage and custom auth-changed window events to reactively update loggedIn.

UtilsPlugin (src/utils/utilsPlugin.js)

Installs these global properties on the Vue app instance:

Property Source Description
$formatDate dateUtils.js Format ISO date to locale string
$formatDateTime dateUtils.js Format ISO datetime
$formatRelativeDate dateUtils.js Relative time (e.g. "2 hours ago")
$safeValue dateUtils.js Return value or fallback
$capitalize dateUtils.js Capitalize string
$rollDie randomUtils.js Roll single die (1N)
$rollDice randomUtils.js Roll multiple dice → array
$rollDiceWithSum randomUtils.js Roll + return sum
$rollNotation randomUtils.js Parse RPG notation e.g. "3d6+2"
$randomBetween randomUtils.js Random int in range
$randomChoice randomUtils.js Pick random element from array
$shuffleArray randomUtils.js Fisher-Yates shuffle

3. Router

src/router/index.jscreateWebHistory (HTML5 mode, no hash).

Route Table

Path Name Component Auth? Admin?
/ Landing LandingView
/login Login LoginView
/register Register RegisterView
/forgot-password ForgotPassword ForgotPasswordView
/reset-password ResetPassword ResetPasswordView
/dashboard Dashboard DashboardView
/profile UserProfile UserProfileView
/users UserManagement UserManagementView
/ausruestung/:characterId Ausruestung AusruestungView
/maintenance Maintenance MaintenanceView
/upload FileUpload FileUploadPage
/sponsors Sponsors SponsorsView
/help Help HelpView
/system-info SystemInfo SystemInfoView
/character/:id CharacterDetails CharacterDetails
/character/create/:sessionId CharacterCreation CharacterCreation

Static imports (loaded immediately — needed for unauthenticated pages): LandingView, LoginView, RegisterView, ForgotPasswordView, ResetPasswordView.

Lazy-loaded (code-split into separate chunks on first access): all other views and both character components.

Route params are passed as props for CharacterDetails (id) and CharacterCreation (sessionId).

Navigation Guard

router.beforeEach(async (to, from, next) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next({ name: "Login" })
  } else if (to.meta.requiresAdmin) {
    const userStore = useUserStore()
    if (!userStore.currentUser) await userStore.fetchCurrentUser()
    if (!userStore.isAdmin) next({ name: "Dashboard" })
    else next()
  } else {
    next()
  }
})

isLoggedIn() simply checks localStorage.getItem("token"). The admin check lazy-loads the user profile if it hasn't been fetched yet.


4. Pinia Stores

userStore (src/stores/userStore.js)

defineStore('user', {
  state: () => ({ currentUser: null, isLoading: false }),
  getters: {
    isAuthenticated: state => !!state.currentUser,
    userRole:        state => state.currentUser?.role || 'standard',
    isAdmin:         state => state.currentUser?.role === 'admin',
    isMaintainer:    state => state.currentUser?.role === 'maintainer'
                              || state.currentUser?.role === 'admin',
    isStandardUser:  state => !!state.currentUser
  },
  actions: {
    async fetchCurrentUser() { /* GET /api/user/profile */ },
    clearUser() { this.currentUser = null }
  }
})
  • Fetches profile from GET /api/user/profile.
  • On successful fetch, also updates the i18n locale from profile.preferred_language and persists it to localStorage.
  • currentUser shape: { id, username, display_name, email, role, preferred_language }.
  • Used by: Menu.vue, CharacterDetails.vue, UserManagementView.vue, router guard.

languageStore (src/stores/languageStore.js)

export const i18n = createI18n({
  legacy: false,
  locale: localStorage.getItem('language') || 'de',
  fallbackLocale: 'en',
  messages: { de, en }
})

defineStore('language', {
  state: () => ({ currentLanguage: localStorage.getItem('language') || 'de' }),
  actions: {
    setLanguage(lang) {
      this.currentLanguage = lang
      i18n.global.locale.value = lang
      localStorage.setItem('language', lang)
    }
  }
})

Unusually, the i18n instance is created and exported from this file, then imported in main.js. Default language: de.

characterCreationStore (src/stores/characterCreation.js)

Manages the multi-step character wizard session state in-memory (backed by server-side session).

State:

{
  sessionData: {
    basic_info: null,
    attributes: null,
    derived_values: null,
    skills: [],
    skills_meta: { totalUsedPoints: 0, selectedCategory: null },
    spells: [],
    equipment: null
  },
  currentStep: 1,   // 15
  sessionId: null,
  isLoading: false,
  error: null
}

Getters: characterClass, characterStand, characterRace, hasSelectedSkills, totalSkillPoints, isValid (checks basic_info + attributes + derived_values are all non-null).

Actions: initializeSession(sessionId?), createNewSession(), loadSession(sessionId) (TODO), saveSession() (TODO), updateStepData(data), goToStep(n), nextStep(), previousStep(), clearSession().

Note: saveSession() and loadSession() are marked TODO — the actual session persistence is handled directly by CharacterCreation.vue via the API, bypassing this store.

counter (src/stores/counter.ts)

A boilerplate Pinia counter store from Vue scaffolding. Not used in the application.


5. Views (src/views/)

Most views are thin wrappers that delegate to a single component.

LandingView.vue

Purpose: Public landing page. Displays app logo, description, version info, and action links.

Data fetched: GET ${VITE_API_URL}/api/public/version (unauthenticated, uses raw axios not the API utility). Retries every 5 s up to 24 times if the backend is unreachable.

Logic:

  • isBackendAvailable computed: disables the login button until the backend responds.
  • Clears retry interval on beforeUnmount.
  • Shows frontend version from version.js (0.2.3) and backend version from API.

Components used: None (self-contained).


LoginView.vue

Thin wrapper: renders <LoginForm />.


RegisterView.vue

Thin wrapper: renders <RegisterForm />.


ForgotPasswordView.vue

Thin wrapper: renders <ForgotPasswordForm />.


ResetPasswordView.vue

Thin wrapper: renders <ResetPasswordForm />.


DashboardView.vue

Thin wrapper: renders <CharacterList />.


CharacterView.vue

Thin wrapper (legacy): passes $route.params.characterId to <CharacterDetails /> as :characterId. Route /character/:id uses CharacterDetails directly via lazy import; this view appears unused by the router.


AusruestungView.vue

Passes $route.params.characterId to <AusruestungList />. Legacy/simple standalone equipment list for a character — separate from the main character detail equipment tab.


MaintenanceView.vue

Thin wrapper: renders <Maintenance />.


UserProfileView.vue

Purpose: Self-service profile management for the logged-in user.

API calls:

  • GET /api/user/profile (on created)
  • PUT /api/user/display-name — change display name (max 30 chars)
  • PUT /api/user/language — change preferred language (updates i18n locale + localStorage)
  • PUT /api/user/email — change email
  • PUT /api/user/password — change password (requires current + new + confirm)

Validation:

  • Display name: max 30 chars
  • Password: min 6 chars, must match confirmation
  • Email: must differ from current, type="email"
  • Duplicate email check via backend error message parsing

Sections rendered: User info display, Change Display Name, Change Language, Change Email, Change Password.

After language update: directly sets this.$i18n.locale and localStorage.language. Also updates userStore.currentUser.display_name after display name change.


UserManagementView.vue

Purpose: Admin panel for managing all users.

Guard: Route has requiresAdmin: true.

API calls:

  • GET /api/users — list all users
  • PUT /api/users/:id/role — change role (standard | maintainer | admin)
  • DELETE /api/users/:id — delete user
  • PUT /api/users/:id/password — admin password reset (no current password required)

Dialogs (modal overlays):

  1. Role change dialog — select from standard/maintainer/admin
  2. Delete confirmation dialog
  3. Password change dialog (min 6 chars, must confirm)

Protection: Buttons disabled for the current logged-in user's own row (cannot self-demote or self-delete). Detected via currentUser.id === user.id from userStore.


FileUploadPage.vue

Purpose: Import character data from VTT (Virtual Tabletop) JSON or CSV files.

API call: POST /api/importer/upload with multipart/form-data.

Validation: Checks MIME types (application/json, text/csv). VTT file is required; CSV is optional.

Note: Manually adds Authorization header even though the API interceptor already does this — a redundancy.


HelpView.vue

Static content page. Dynamically loads FAQ items from i18n keys help.faq1.question/.answer, help.faq2..., etc., iterating until a key is not found using $te().


SponsorsView.vue

Static content page listing contributors and special thanks. Links to GitHub and Ko-fi.


SystemInfoView.vue

Purpose: Displays system/technology information and live backend status.

API call: GET ${VITE_API_URL}/api/public/systeminfo (uses raw axios). Returns { version, gitCommit, userCount, charCount, dbVersion }.

Technology stack display: Hardcoded lists of Vue 3, Vite, Go 1.25, Gin, GORM, MariaDB, JWT, chromedp, Docker.


6. Components (src/components/)

Menu.vue

Purpose: Fixed top navigation bar (60px high, z-index 1000).

Authentication-dependent rendering:

  • Always visible: Home, Info dropdown (Help, Sponsors, System Info)
  • Logged-in only: Characters dropdown (Dashboard, Import Data)
  • Maintainer/Admin only: Admin dropdown (Maintenance, User Management)
  • Not logged-in only: Register link
  • Right side (logged-in): User icon dropdown (Profile, Logout)

Hover dropdowns: Implemented with @mouseenter/@mouseleave + 200 ms setTimeout delay to prevent premature closing.

Logout flow: calls logout() from auth.js (removes token), clears userStore, dispatches auth-changed window event, navigates to /.

Auth change listener: window.addEventListener('auth-changed', ...) — re-fetches user profile on login or clears on logout.

Props/emits: None.


LoginForm.vue

Purpose: Login form.

API call: POST /login with { username, password }. On success: stores JWT in localStorage, calls userStore.fetchCurrentUser(), dispatches auth-changed, navigates to /dashboard.

Error handling: Displays "Invalid credentials" on any error.

Note: Hardcoded English labels — does not use $t() for the form labels (unlike most other components).


RegisterForm.vue

Purpose: Registration form.

Validation: Client-side password confirmation match check before API call.

API call: POST /register with { username, password, email }.

i18n: Fully translated via auth.* keys.


ForgotPasswordForm.vue

Purpose: Request a password reset email.

API call: POST /password-reset/request with { email, redirect_url: window.location.origin }.

UX: Shows success state (hides form, shows info text) on success without redirecting. Shows error message on failure.


ResetPasswordForm.vue

Handles the password-reset token link (not examined in detail — delegates to <ResetPasswordForm />).


CharacterList.vue

Purpose: Dashboard character listing with creation session management.

API calls:

  • GET /api/characters{ self_owned: [...], others: [...] }
  • GET /api/characters/create-sessions{ sessions: [...] }
  • POST /api/characters/create-session{ session_id } — starts new character wizard
  • DELETE /api/characters/create-session/:sessionId

Layout:

  • "Create New Character" button (green dashed border)
  • <CharacterCreationSessions> sub-component (shows in-progress wizards)
  • Two column layout: owned characters | shared characters
  • Each character shows: name, race, class, grade, owner, public/private badge
  • Character names are <router-link> to /character/:id

Props/emits: None (standalone).

Sub-components: CharacterCreationSessions.


CharacterCreationSessions.vue

Purpose: Shows active character creation sessions (drafts) on the dashboard.

Props: sessions: Array

Emits: continue-session(sessionId), delete-session(sessionId)

Renders: Grid of cards, one per session. Shows name, step progress (e.g. "Step 2/5"), race/class, last updated, expires. Clicking a card card navigates via continue-session emit.


CharacterDetails.vue

Purpose: Main character management hub. Fetches character data and dynamically renders sub-views through a tab-like submenu.

Props: id (string, from route)

API calls:

  • GET /api/characters/:id (on created + after any edit via refreshCharacter)

Computed: isOwner — compares character.user_id with userStore.currentUser.id.

Sub-views rendered via <component :is="currentView"> with character and isOwner props:

  1. DatasheetView (default)
  2. SkillView
  3. WeaponView
  4. SpellView
  5. EquipmentView
  6. ExperianceView
  7. DeleteCharView

Additional dialogs:

  • 📄 Export button → ExportDialog
  • 🌐/🔒 Visibility button (owner only) → VisibilityDialog

Event handling: @character-updated from sub-views triggers refreshCharacter() which reloads from API.


DatasheetView.vue

Purpose: Character stats and biographical information display with inline editing.

Props: character: Object (required)

Emits: character-updated

Sub-components: ImageUploadCropper

Inline editing mechanism: Double-click (@dblclick) on any displayed value activates an input/select in its place. On blur or Enter, saveEdit(path) / saveProp(prop) PATCHes the character via PUT /api/characters/:id. On Escape, cancelEdit() restores display.

Stat fields (left column stat box):

Label key Character path
stats.beauty eigenschaften.0.value
stats.dexterity eigenschaften.1.value
stats.agility eigenschaften.2.value
stats.intelligence eigenschaften.3.value
stats.constitution eigenschaften.4.value
stats.charisma eigenschaften.5.value
stats.strength eigenschaften.6.value
stats.willpower eigenschaften.7.value
stats.spelltalent eigenschaften.8.value
stats.poisontolerance git
stats.movement b.max
stats.lifepoints lp.max
stats.staminapoints ap.max
stats.divinegrace bennies.gg
stats.fatesfavor bennies.sg

Character info fields (editable): name, typ (class), gender (select), grad, rasse (select), origin (select), social_class (select), spezialisierung (select), alter, hand (select: rechts/links/beidhändig), groesse, gewicht, merkmale.groesse, merkmale.breite, merkmale.augenfarbe, merkmale.haarfarbe, glaube (multi-select), merkmale.sonstige.

Select options: loaded lazily via GET /api/characters/:id/datasheet-options (only when a select-type field is first double-clicked).

glaube (beliefs) field: Multi-select stored as comma-separated string; displayed and edited as array.

Path traversal for saving: path.split('.') reduces into nested character object, then PUT /api/characters/:id with full character payload.


SkillView.vue

Purpose: Display and manage character skills (Fertigkeiten + Waffenfertigkeiten) and innate skills.

Props: character: Object (required), isOwner: Boolean

Emits: character-updated

Sub-components: SkillImproveDialog, SkillLearnDialog

Learning mode toggle: 🎓 button visible to owner. When active:

  • Resources bar (EP + Gold) shown
  • Additional action buttons: 📚 Learn New, Add Skill
  • Each skill row shows ⬆️ Improve button

Table structure:

  • Regular skills: grouped by category (character.categorizedskills) — name, EW (Fertigkeitswert), bonus, PP (Praxispunkte), note
  • Weapon skills: from character.waffenfertigkeiten
  • Innate skills: from character.InnateSkills (right column, name + value only)

PP (Praxispunkte) adjustment: +/ buttons directly call POST /api/characters/practice-points.

Inline skill editing: Double-click on name, fertigkeitswert, or bemerkung → inline input; saved via PUT /api/characters/:skill_id/skill.

Dialogs:

  1. SkillLearnDialog — learn new skill (complex, see below)
  2. Improve Selection Dialog — simple modal; selects skill + PP count → POST /api/characters/improve-skill
  3. Add Skill Dialog — manually add skill by name/value → POST /api/characters/:id/add-skill
  4. SkillImproveDialog — detailed multi-level improvement dialog

SkillImproveDialog.vue

Purpose: Detailed skill improvement dialog supporting multiple reward types and level sequences.

Props: character: Object, skill: Object, isVisible: Boolean, learningType: 'improve'|'learn'|'spell'

Emits: close, skill-updated

Features:

  • Loads available improvement levels + costs from GET /api/characters/lerncost-newsystem or similar
  • Loads reward types from API
  • Shows live resource tracking (EP remaining, Gold remaining, PP remaining)
  • Multi-level selection (click/deselect in sequence — must be consecutive)
  • Reward types: default (EP + Gold), noGold (EP only), halveepnoGold (half EP), pp (PP only), mixed (EP + PP)
  • Submits via POST /api/characters/improve-skill-newsystem or similar

SkillLearnDialog.vue

Purpose: Learn a brand-new skill (not yet on the character).

Features:

  • Drag & drop (HTML5 native) or click-to-select skills from available list to "to learn" list
  • Category filter buttons (all categories shown as chips)
  • Search filter input
  • Sort by name or EP cost (ascending/descending toggle)
  • Reward type selector: Standard (EP + Gold) or Nur EP (no gold)
  • Live EP/Gold remaining display
  • Multi-skill batch learning (multiple skills can be queued)
  • Submits batch via POST /api/characters/learn-skills (or similar)

SpellView.vue

Purpose: Display character spells.

Props: character: Object, isOwner: Boolean

Emits: character-updated

Sub-components: SpellLearnDialog

Table: name, description, bonus, source.

Learning: Owner sees 🎓 button → opens SpellLearnDialog.


SpellLearnDialog.vue

Purpose: Learn new spells with filtering, sorting, and multi-spell queuing.

Features:

  • School (Schule) filter buttons
  • Search term filter
  • Sort by name, EP cost, Gold cost
  • Reward type selector (loads from API)
  • Available/selected two-column layout
  • Per-spell affordability check
  • Batch spell learning queue with total cost summary
  • Submits via POST /api/characters/learn-spells (or similar)

WeaponView.vue

Purpose: Display and manage character weapons.

Props: character: Object, isOwner: Boolean

Emits: character-updated

Table: name (* suffix if magical), description, weight, value, amount, contained_in, bonus (anb/abwb).

Add weapon dialog:

  • Loads master weapon list from GET /api/maintenance/weapons (or similar)
  • Text search filter
  • Select + set amount
  • Submits via POST /api/equipment (or similar weapon endpoint)

Edit weapon dialog:

  • Inline form for: description, is_magical checkbox, amount, value, attack bonus (anb), defense bonus (abwb), damage bonus (schb)
  • Saves via PUT /api/equipment/:id (or similar)

Delete weapon: DELETE /api/equipment/:id with confirm dialog using $t('weapon.confirmDelete') with {name} interpolation.


EquipmentView.vue

Purpose: Display and manage character non-weapon equipment.

Props: character: Object, isOwner: Boolean

Emits: character-updated

Table: name, description, weight, value, amount, contained_in, bonus.

Add equipment dialog:

  • Loads master equipment from GET /api/maintenance/equipment
  • Text search filter
  • Select + set amount
  • Submits:POST /api/equipment with { character_id, name, beschreibung, gewicht, wert, anzahl, bonus, beinhaltet_in, contained_in, container_type }

Delete: DELETE /api/equipment/:id with i18n confirm using {name} interpolation.


ExperianceView.vue

Purpose: Manage EP (experience points) and gold/wealth, with audit log.

Props: character: Object, isOwner: Boolean

Sub-components: AuditLogView

Computed: totalWealthInGS — currency conversion:

goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10)

(1 GS = 10 SS = 10 KS — Midgard RPG currency system)

EP management: Add/remove EP inputs → PUT /api/characters/:id/experience or similar.

Gold management: Add/remove gold inputs → PUT /api/characters/:id/wealth or similar.

After both operations: calls this.$refs.auditLog.loadAuditLog() to refresh the audit log.


AuditLogView.vue

Purpose: Display change history (EP/gold transactions) for a character.

Props: character: Object

API: GET /api/characters/:id/audit-log with query params for field filter and date range.

Filters:

  • Field: all / experience_points / gold / silver / copper
  • Date range: all time / today / this week / this month / custom (fromto date inputs)
  • Group by date toggle

Statistics section: total_changes, ep_spent, ep_gained, gold_spent, gold_gained.

Entry display: Shows old → new value with difference (+/), reason, optional notes. Color-coded: green for positive, red for negative changes.


ExportDialog.vue

Purpose: Multi-format character export modal.

Props: characterId, showDialog: Boolean

Emits: update:showDialog, export-success

Computed: canExport — false if no format selected, or PDF format but no template selected.

On created: loads templates from GET /api/pdf/templates.

Export formats:

Format API Call Delivery
PDF GET /api/pdf/export/:id?template=X&showUserName=true → returns { filename }, then opens GET /api/pdf/file/:filename in new tab Browser window popup
VTT GET /api/importer/export/vtt/:id/file (blob) Programmatic download link
BaMoRT (JSON) GET /api/transfer/download/:id (blob) Programmatic download link

PDF export note: Gets filename from API first, then constructs full URL from API.defaults.baseURL + /api/pdf/file/:filename. Uses window.open(url, '_blank') synchronously (not before an async call) — avoids popup blockers per project convention.


VisibilityDialog.vue

Purpose: Change character visibility (private/public) and manage per-user sharing.

Props: characterId, currentVisibility: Boolean, showDialog: Boolean

Emits: update:showDialog, visibility-updated(isPublic)

Watches: showDialog — on open, loads available users and current shares.

API calls:

  • GET /api/characters/:id/available-users — users eligible to share with
  • GET /api/characters/:id/shares (implied) — currently shared users
  • PUT /api/characters/:id/visibility + share update

Features: Radio buttons for private/public. Searchable user list (filter by display name, username, email). Two-panel: "Add Users" | "Currently Shared With" with remove buttons.


DeleteCharView.vue

Purpose: Delete character confirmation panel (inside character detail submenu).

Props: character: Object, isOwner: Boolean

Emits: deleted, cancel

Behavior: Shows "not authorized" message for non-owners. For owners: confirmation text + "Yes, Delete" button → DELETE /api/characters/:id → navigates to /dashboard.


ImageUploadCropper.vue

Purpose: Upload and crop a character portrait image.

Props: characterId

Emits: image-updated

Features:

  • File picker restricted to image/*
  • HTML5 Canvas cropping with mouse drag selection
  • Two crop shapes: rectangular or circular
  • Live preview canvas (400×400)
  • POST /api/characters/:id/image with FormData containing crop coordinates

Implementation: Uses FileReaderImage → two <canvas> elements (source + preview). Mouse events track drag selection rectangle. On save, crops the selection to a new canvas and uploads as FormData.


Maintenance.vue

Purpose: Master data management hub for game system admins/maintainers.

API call: GET /api/maintenance — loads all master data at once into mdata object.

mdata shape: { skills, skillcategories, weaponskills, spells, spellcategories, equipment, weapons }

Sub-views (selected via bottom submenu):

  1. maintenance/SkillView (default)
  2. maintenance/SpellView
  3. maintenance/EquipmentView
  4. maintenance/WeaponView
  5. maintenance/WeaponSkillView
  6. maintenance/BelieveView
  7. maintenance/GameSystemView
  8. maintenance/LitSourceView
  9. maintenance/MiscLookupView
  10. maintenance/SkillImprovementCostView

All sub-views receive mdata as prop.


maintenance/SkillView.vue

Purpose: CRUD for skill master data.

Features:

  • Full text search + multi-field filters (category, difficulty, improvable, innate, bonus property)
  • Sortable table (name, category columns)
  • Inline row editing (click row → edit form appears in table)
  • Create new skill inline
  • API: POST /api/maintenance/skills, PUT /api/maintenance/skills/:id, DELETE /api/maintenance/skills/:id

(Other maintenance views follow the same pattern for their respective domains.)


CharacterCreation.vue

Purpose: Multi-step character creation wizard.

Props: sessionId: String (required, from route)

5-step wizard:

  1. CharacterBasicInfo — name, origin, belief, gender, race, class, social class
  2. CharacterAttributes — 7 core attributes (1100 range, roll support)
  3. CharacterDerivedValues — calculated + adjustable secondary stats
  4. CharacterSkills — point-buy skill selection with category budgets
  5. CharacterSpells — spell selection → finalize character

API calls:

  • GET /api/characters/create-session/:sessionId — load session state
  • GET /api/characters/skill-categories — available skill categories with point budgets
  • Step-specific save: PUT /api/characters/create-session/:id/basic-info etc.
  • POST /api/characters/create — finalize (step 5)
  • DELETE /api/characters/create-session/:id — delete draft

Session persistence: Each handleNext(data) saves the updated step data to the backend before incrementing currentStep. Session reloaded after each save to ensure consistent state.

Expiry info: Session expiry date shown at bottom. "Delete Draft" button available throughout.


CharacterCreation/CharacterBasicInfo.vue

Purpose: Step 1 of the wizard. Collects fundamental character identity.

Fields: Name (250 chars, required), Origin (select from API list), Belief/Glaube (typeahead search from API, minimum 2 chars to activate), Gender (male/female), Race (select), Class/Typ (select), Social Class (select or dice roll).

Dice roll for social class: 🎲 button (requires class to be selected first). Rolls 1d100 with class-specific modifier and maps to social class tier. Shows result in overlay that dismisses on click.

Races: loaded from backend (API call for character creation options).

Emits: next(formData), save(formData)

Validation: isValid computed checks all required fields are non-empty.


CharacterCreation/CharacterAttributes.vue

Purpose: Step 2 — set the 7 base attributes (St, Gs, Gw, Ko, In, Zt, Au) each 1100.

Features:

  • Individual roll buttons using max(2d100) formula per attribute (except Au)
  • 🎲 "Roll All" button
  • Race-specific Beauty (Au) restrictions: Elves ≥ 81, Gnomes/Dwarves ≤ 80
  • Shows total points sum and average
  • Disabled state display (shows "ENABLED"/"DISABLED" status indicator)

CharacterCreation/CharacterDerivedValues.vue

Purpose: Step 3 — calculate and optionally override secondary stats.

Fields: PA (personal charisma), WK (willpower), LP max (life points), AP max (adventure points), B max (movement), resistances, defense, bonuses, SG (fate's favor), GG (divine grace), GP (luck points).

Dice formulas displayed in i18n keys:

  • PA = 1d100 + 4×(In/10) 20
  • WK = 1d100 + 2×(Ko/10 + In/10) 20
  • LP = 1d3 + 7 + (Ko/10)
  • AP = 3d6 + class modifier
  • B = 1d6 + race modifier

Recalculate button: Calls backend to recalculate from attributes.

Individual roll buttons per derived value with tooltip showing formula.


CharacterCreation/CharacterSkills.vue

Purpose: Step 4 — point-buy skill selection with category budgets.

Props: sessionData, skillCategories

Each category has a point budget (max_points). Skills have costs. Emits next, previous, save.


CharacterCreation/CharacterSpells.vue

Purpose: Step 5 — optional spell selection before finalization.

Props: sessionData, skillCategories

Emits: previous, finalize(finalData), save

Finalize calls POST /api/characters/create with complete session data. On success navigates to the new character's detail view.


AusruestungList.vue (legacy)

Purpose: Simple standalone equipment list (old design). Loaded via /ausruestung/:characterId route.

API: GET /ausruestung/:characterId (note: no /api prefix — likely a legacy endpoint).

Sub-component: AusruestungForm


AusruestungForm.vue (legacy)

Simple form to add equipment name/quantity/weight. Posts to /ausruestung (no /api prefix).


LanguageSwitcher.vue

Marked as "not used anymore" in a comment in the template. A simple <select> that calls languageStore.setLanguage(). Language switching is now done via UserProfileView.


HelloWorld.vue, TheWelcome.vue, WelcomeItem.vue

Vite/Vue scaffolding boilerplate — not used in the application.


7. API Layer

src/utils/api.js

import axios from 'axios'

const API = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de'
})

// Request interceptor
API.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// Response interceptor
API.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('token')
      // redirect to /login is commented out
    }
    return Promise.reject(error)
  }
)

export default API

Key facts:

  • Default base URL: https://bamort-api.trokan.de (production endpoint). In dev containers the env var overrides this to http://localhost:8180.
  • JWT is stored in and retrieved from localStorage.getItem('token').
  • 401 handler removes the token but does not automatically redirect (commented out). Components handle 401 errors individually.
  • API.defaults.baseURL is used in ExportDialog to construct the PDF file download URL.

src/utils/auth.js

export function isLoggedIn() { return !!localStorage.getItem("token") }
export function logout()     { localStorage.removeItem("token") }

Simple token-presence check. No JWT expiry validation, no PKCE, no refresh token mechanism.

All API endpoints used by the frontend

Method Endpoint Used by
POST /login LoginForm
POST /register RegisterForm
POST /password-reset/request ForgotPasswordForm
GET /api/public/version LandingView
GET /api/public/systeminfo SystemInfoView
GET /api/user/profile userStore, UserProfileView
PUT /api/user/display-name UserProfileView
PUT /api/user/language UserProfileView
PUT /api/user/email UserProfileView
PUT /api/user/password UserProfileView
GET /api/users UserManagementView
PUT /api/users/:id/role UserManagementView
DELETE /api/users/:id UserManagementView
PUT /api/users/:id/password UserManagementView
GET /api/characters CharacterList
GET /api/characters/:id CharacterDetails
PUT /api/characters/:id DatasheetView
DELETE /api/characters/:id DeleteCharView
GET /api/characters/:id/datasheet-options DatasheetView
GET /api/characters/:id/image DatasheetView (imageSrc)
POST /api/characters/:id/image ImageUploadCropper
GET /api/characters/:id/audit-log AuditLogView
GET /api/characters/:id/available-users VisibilityDialog
POST /api/characters/create-session CharacterList
GET /api/characters/create-sessions CharacterList
GET /api/characters/create-session/:id CharacterCreation
DELETE /api/characters/create-session/:id CharacterList, CharacterCreation
GET /api/characters/skill-categories CharacterCreation
POST /api/characters/improve-skill SkillView
POST /api/characters/practice-points SkillView
GET /api/pdf/templates ExportDialog
GET /api/pdf/export/:id ExportDialog
GET /api/pdf/file/:filename ExportDialog
GET /api/importer/export/vtt/:id/file ExportDialog
POST /api/importer/upload FileUploadPage
GET /api/transfer/download/:id ExportDialog
GET /api/equipment (maintenance) EquipmentView
POST /api/equipment EquipmentView, WeaponView
DELETE /api/equipment/:id EquipmentView, WeaponView
PUT /api/equipment/:id WeaponView
GET /api/maintenance Maintenance
POST/PUT/DELETE /api/maintenance/skills maintenance/SkillView

8. i18n

Configuration

  • Library: vue-i18n v11 (Composition API mode: legacy: false)
  • Default locale: de (German), persisted in localStorage.language
  • Fallback locale: en
  • Instance created in languageStore.js and exported as i18n

Languages

Two supported languages: German (de) and English (en).

Both locales are JavaScript objects (not JSON), exported as ES module defaults from single files:

  • src/locales/de (no .js extension)
  • src/locales/en (no .js extension)

Translation Key Structure

Top-level key Domain
common Shared labels: loading, cancel, save, edit, back, next
auth Login/Registration form labels and messages
forgotPassword Password reset flow
import File import page
menu Navigation menu items
landing Landing page content
equipment Equipment table, dialogs, messages
skill Skill table headers and field labels
spell Spell table headers and field labels
weapon Weapon table, add/edit dialogs
weaponskill Weapon skill table
spells.learn Spell learning dialog
stats Abbreviated stat names (St, Gs, Gw, Ko, In, Zt, Au, pA, Wk...)
chars / singular view names View tab labels
characters.list Dashboard character list UI
characters.create Character wizard UI
characters.basicInfo Wizard step 1: basic info
characters.attributes Wizard step 2: attributes
characters.derivedValues Wizard step 3: derived values
characters.datasheet Datasheet edit hint
experience EP + wealth section
audit Audit log labels and filters
export Export dialog labels and messages
visibility Visibility/sharing dialog
userManagement Admin user management
profile User profile page
deleteChar Delete character authorization message
maintenance Maintenance section label
maintmenu Maintenance sub-menu items
believe Belief master data management
gamesystem Game system master data
litsource Literature source master data
misc Misc lookup master data
skillimprovement Skill improvement cost master data
help Help page content + FAQ keys
sponsors Sponsors page content
systemInfo System info page content

Note: Some keys exist only partially in EN (e.g., spells.learn labels use German text in the EN locale — incomplete translation).

Language switching

  1. Via UserProfileViewPUT /api/user/language → updates this.$i18n.locale and localStorage
  2. Via userStore.fetchCurrentUser() → automatically applies profile.preferred_language if set
  3. The LanguageSwitcher.vue component exists but is marked unused.

9. Styling

Architecture

Global-first approach: All shared styles live in src/assets/main.css (imported in main.js as a side-effect import). Components use <style> (unscoped, contributing to global scope) or <style scoped> only for component-specific overrides.

Convention from vue.instructions.md: <template>, then <style scoped>, then <script> (template-first order, opposite of Vite default).

CSS Files

src/assets/base.css — CSS custom properties (variables):

:root {
  --vt-c-black-soft: #222222;     /* nav background */
  --color-primary: #007bff;        /* primary blue */
  --color-bg-secondary: #f8f9fa;
  --color-text-primary: #333;
  --color-text-secondary: #495057;
  --padding-xs/sm/md/lg           /* 4/8/16/24px */
  --margin-xs/sm/md/lg            /* 4/8/16/24px */
  --border-radius: 6px;
  --box-shadow-light: 0 2px 4px rgba(0,0,0,0.1);
}

Dark mode support via @media (prefers-color-scheme: dark).

src/assets/main.css — Application shell and shared component styles:

Key CSS classes defined globally:

  • .top-nav — fixed 60px navigation bar (black background)
  • .main-content — flex-grow content area with top+bottom padding for fixed nav+submenu
  • .fullwidth-container — full-width component wrapper with bottom padding for submenu
  • .card — white bordered box with hover animation
  • .grid-container, .grid-2/3/4-columns — responsive CSS grid layouts
  • .page-header — section header with blue 2px bottom border
  • .section-header — subsection header
  • .submenu — fixed bottom tab bar for character/maintenance view switching
  • .cd-table, .cd-table-header — data tables (green headers #1da766)
  • .list-item, .list-container, .charlist — character list cards
  • .modal-overlay, .modal-content, .modal-header/body/footer — modal dialogs
  • .form-group, .form-control, .form-row — form layout
  • .btn, .btn-primary, .btn-secondary, .btn-danger, .btn-success — button variants
  • .badge, .badge-success, .badge-danger, .badge-warning, .badge-secondary — status pills
  • .loading, .empty-state — feedback states

Accent colour: #1da766 (green) used for table headers, learning mode buttons, active nav items.

Primary colour: #007bff (blue) used for page headers, primary buttons, links.

Component-specific styles

Most components either have no <style> block or minimal overrides. Examples:

  • SkillView.vue@keyframes slideIn for learning mode animation
  • EquipmentView.vue.modal-fullscreen flex layout fix
  • ForgotPasswordForm.vue — local .reset-* classes for that form layout

10. Business Logic in Frontend

Form Validation

Location Validation
RegisterForm Password == confirmPassword (client), email type input
UserProfileView Display name max 30 chars, email != current, passwords min 6 chars + match
UserManagementView Passwords min 6 chars + match
CharacterBasicInfo All required fields non-empty (isValid computed)
CharacterAttributes Race-specific beauty restrictions (Elves ≥81, Gnomes/Dwarves ≤80)
FileUploadPage MIME type check for .json/.csv files
SkillImproveDialog Must select at least one level, must be able to afford it
SkillLearnDialog Skills must be affordable (EP + Gold check)

Data Transformation

Character stat access (DatasheetView.getStat):

getStat(path) {
  return path.split('.').reduce((obj, key) => obj?.[key], this.character) ?? '-'
}

Deep path traversal on nested character object. Same pattern for saving via pathParts loop.

Glaube (beliefs) encoding: Multi-select array ↔ comma-separated string conversion in DatasheetView.

Currency conversion (ExperianceView):

totalWealthInGS() {
  return goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10)
}

Midgard RPG conversion: 1 GS = 10 SS = 10 KS.

Skill level improvement cost computation: Delegated to backend via API — frontend only displays what the server returns.

Character creation dice rolls:

  • rollDie(100) for attributes
  • Social class: $rollDice(1, 100) + class modifier → lookup table for result
  • Derived values: formulas like 1d100 + 4×(In/10) - 20 for PA; 1d3 + 7 + Ko/10 for LP

Computed State

Component Computed Logic
LandingView isBackendAvailable backend version not in error states
ExportDialog canExport format selected AND (if PDF) template selected
VisibilityDialog filteredAvailableUsers exclude already-shared users, apply search query
ExperianceView totalWealthInGS currency conversion formula
CharacterDetails isOwner character.user_id === userStore.currentUser.id
SkillImproveDialog remainingEP, remainingGold, remainingPP current resources minus selected cost
SkillLearnDialog sortedFilteredSkills filter by category + search, sort by name/cost
CharacterBasicInfo isValid all required fields filled
userStore isAdmin, isMaintainer role string comparisons

Auth State Management

Auth state is managed entirely via localStorage.token. No reactive store for the token itself — components call isLoggedIn() directly at mount time. The App.vue listens to storage events (cross-tab) and a custom auth-changed event (same-tab) to update the loggedIn data property and show/hide the navigation menu.


11. User Flows

Login Flow

/ (Landing) → click "Zum Login"
→ /login → LoginForm
  → POST /login { username, password }
  → JWT stored in localStorage.token
  → userStore.fetchCurrentUser() (GET /api/user/profile)
  → window.dispatchEvent('auth-changed') 
  → App.vue re-checks auth, shows Menu
  → router.push('/dashboard')
→ /dashboard → DashboardView → CharacterList

Registration Flow

/register → RegisterForm
  → validates passwords match
  → POST /register { username, password, email }
  → shows success badge
  → user manually navigates to /login

Password Reset Flow

/forgot-password → ForgotPasswordForm
  → POST /password-reset/request { email, redirect_url }
  → shows success state (no redirect)
  → user receives email with link
  → link leads to /reset-password?token=X
  → ResetPasswordForm → POST /password-reset/confirm

Character List / Dashboard Flow

/dashboard → CharacterList
  → GET /api/characters → { self_owned, others }
  → GET /api/characters/create-sessions → { sessions }
  → renders owned + shared character links
  → renders active creation session cards

Character Creation Flow (New Character)

Dashboard → "Create New Character"
  → POST /api/characters/create-session → { session_id }
  → router.push('/character/create/:sessionId')

/character/create/:sessionId → CharacterCreation
  → GET /api/characters/create-session/:id (load state)
  → GET /api/characters/skill-categories (load skill budgets)

Step 1: CharacterBasicInfo
  → fill name, origin, belief, gender, race, class, social class
  → "Next: Attributes →"
  → PUT /api/characters/create-session/:id/step1 (save)

Step 2: CharacterAttributes
  → set 7 attributes (1100), optionally roll dice
  → "Next: Derived Values →"
  → PUT /api/characters/create-session/:id/step2

Step 3: CharacterDerivedValues
  → view/adjust calculated values, optionally recalculate
  → "Next: Skills →"
  → PUT /api/characters/create-session/:id/step3

Step 4: CharacterSkills
  → select skills from category-budgeted list
  → "Next: Spells →"
  → PUT /api/characters/create-session/:id/step4

Step 5: CharacterSpells
  → optionally select spells
  → "Create Character" → POST /api/characters/create
  → router.push('/character/:newId')

Sessions can be resumed from the dashboard ("Continue Character Creation") or deleted.

Character Management Flow

/character/:id → CharacterDetails
  → GET /api/characters/:id

Tab: DatasheetView (default)
  → displays stats + bio info
  → double-click any field to edit inline
  → blur/Enter → PUT /api/characters/:id

Tab: SkillView
  → displays categorized skills + weapon skills + innate skills
  → owner: PP adjustment (+/ buttons)
  → owner: learning mode toggle (🎓)
    → SkillLearnDialog: learn new skill
    → SkillImproveDialog: improve existing skill
    → inline add skill dialog

Tab: WeaponView
  → displays weapons table
  → owner: add weapon (search + select from master data)
  → owner: edit weapon (magical, bonuses, value)
  → owner: delete weapon

Tab: SpellView
  → displays spells table
  → owner: 🎓 → SpellLearnDialog

Tab: EquipmentView
  → displays equipment table
  → owner: add equipment (search + select from master data)
  → owner: delete equipment

Tab: ExperianceView
  → EP display + add/remove
  → Gold display + add/remove
  → AuditLogView (filterable change history)

Tab: DeleteCharView
  → owner only: confirm + DELETE /api/characters/:id → /dashboard

PDF Export Flow

CharacterDetails → 📄 button → ExportDialog

Step 1: Load templates (GET /api/pdf/templates)
Step 2: Select format (PDF / VTT / BaMoRT JSON)
Step 3: If PDF → select template + optional showUserName checkbox
Step 4: Click "Export"

PDF path:
  GET /api/pdf/export/:id?template=X → { filename }
  window.open(API.baseURL + /api/pdf/file/filename, '_blank')

VTT path:
  GET /api/importer/export/vtt/:id/file (blob)
  → programmatic <a download> click

BaMoRT JSON path:
  GET /api/transfer/download/:id (blob)
  → programmatic <a download> click

Visibility / Sharing Flow

CharacterDetails → 🌐/🔒 button (owner only) → VisibilityDialog
  → GET /api/characters/:id/available-users
  → GET /api/characters/:id/shares (current)
  → Toggle private/public radio
  → Search + add users from available list
  → Remove users from shared list
  → "Save" → PUT /api/characters/:id/visibility + share update
  → emits visibility-updated → CharacterDetails updates character.public

Admin User Management Flow

Menu Admin → "User Management" (admin only)
  → /users → UserManagementView
  → GET /api/users
  → table of all users

Per user (excluding self):
  "Change Role" → modal → select role → PUT /api/users/:id/role
  "Change Password" → modal → new password → PUT /api/users/:id/password
  "Delete" → confirm modal → DELETE /api/users/:id

12. Component Hierarchy

App.vue
├── Menu.vue                          [if logged in]
└── <router-view> (main content)
    ├── LandingView.vue               [/]
    ├── LoginView.vue                 [/login]
    │   └── LoginForm.vue
    ├── RegisterView.vue              [/register]
    │   └── RegisterForm.vue
    ├── ForgotPasswordView.vue        [/forgot-password]
    │   └── ForgotPasswordForm.vue
    ├── ResetPasswordView.vue         [/reset-password]
    │   └── ResetPasswordForm.vue
    ├── DashboardView.vue             [/dashboard] *auth*
    │   └── CharacterList.vue
    │       └── CharacterCreationSessions.vue
    ├── UserProfileView.vue           [/profile] *auth*
    ├── UserManagementView.vue        [/users] *admin*
    ├── FileUploadPage.vue            [/upload] *auth*
    ├── MaintenanceView.vue           [/maintenance] *auth*
    │   └── Maintenance.vue
    │       ├── maintenance/SkillView.vue       (default)
    │       ├── maintenance/SpellView.vue
    │       ├── maintenance/EquipmentView.vue
    │       ├── maintenance/WeaponView.vue
    │       ├── maintenance/WeaponSkillView.vue
    │       ├── maintenance/BelieveView.vue
    │       ├── maintenance/GameSystemView.vue
    │       ├── maintenance/LitSourceView.vue
    │       ├── maintenance/MiscLookupView.vue
    │       └── maintenance/SkillImprovementCostView.vue
    ├── AusruestungView.vue           [/ausruestung/:characterId] *auth*
    │   └── AusruestungList.vue
    │       └── AusruestungForm.vue
    ├── SponsorsView.vue              [/sponsors]
    ├── HelpView.vue                  [/help]
    ├── SystemInfoView.vue            [/system-info]
    ├── CharacterDetails.vue          [/character/:id] *auth*
    │   ├── ExportDialog.vue
    │   ├── VisibilityDialog.vue
    │   └── <dynamic component :is="currentView">
    │       ├── DatasheetView.vue     (default)
    │       │   └── ImageUploadCropper.vue
    │       ├── SkillView.vue
    │       │   ├── SkillLearnDialog.vue
    │       │   └── SkillImproveDialog.vue
    │       ├── WeaponView.vue
    │       ├── SpellView.vue
    │       │   └── SpellLearnDialog.vue
    │       ├── EquipmentView.vue
    │       ├── ExperianceView.vue
    │       │   └── AuditLogView.vue
    │       └── DeleteCharView.vue
    └── CharacterCreation.vue         [/character/create/:sessionId] *auth*
        ├── CharacterCreation/CharacterBasicInfo.vue    (step 1)
        ├── CharacterCreation/CharacterAttributes.vue   (step 2)
        ├── CharacterCreation/CharacterDerivedValues.vue (step 3)
        ├── CharacterCreation/CharacterSkills.vue       (step 4)
        └── CharacterCreation/CharacterSpells.vue       (step 5)

Component-to-Parent Emits Summary

Component Emits Consumed by
CharacterCreationSessions continue-session, delete-session CharacterList
ExportDialog update:showDialog, export-success CharacterDetails
VisibilityDialog update:showDialog, visibility-updated CharacterDetails
DatasheetView character-updated CharacterDetails
SkillView character-updated CharacterDetails
WeaponView character-updated CharacterDetails
EquipmentView character-updated CharacterDetails
SpellView character-updated CharacterDetails
SkillLearnDialog close, skill-learned SkillView
SkillImproveDialog close, skill-updated SkillView
SpellLearnDialog close, spell-learned SpellView
ImageUploadCropper image-updated DatasheetView
DeleteCharView deleted, cancel CharacterDetails
AusruestungForm added AusruestungList
CharacterBasicInfo next, save CharacterCreation
CharacterAttributes next, previous, save CharacterCreation
CharacterDerivedValues next, previous, save CharacterCreation
CharacterSkills next, previous, save CharacterCreation
CharacterSpells previous, finalize, save CharacterCreation