# BaMoRT Frontend — Comprehensive Code Analysis
_Generated: 2026-03-13_
---
## Table of Contents
1. [Project Setup](#1-project-setup)
2. [Application Entry](#2-application-entry)
3. [Router](#3-router)
4. [Pinia Stores](#4-pinia-stores)
5. [Views (src/views/)](#5-views-srcviews)
6. [Components (src/components/)](#6-components-srccomponents)
7. [API Layer](#7-api-layer)
8. [i18n](#8-i18n)
9. [Styling](#9-styling)
10. [Business Logic in Frontend](#10-business-logic-in-frontend)
11. [User Flows](#11-user-flows)
12. [Component Hierarchy](#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`
```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`
```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`**
```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 `
` only when the user is logged-in (detected via `localStorage.getItem('token')`). The main `` is wrapped in a `` 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 (1–N) |
| `$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.js`** — `createWebHistory` (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
```js
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`)
```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`)
```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:**
```js
{
sessionData: {
basic_info: null,
attributes: null,
derived_values: null,
skills: [],
skills_meta: { totalUsedPoints: 0, selectedCategory: null },
spells: [],
equipment: null
},
currentStep: 1, // 1–5
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 ``.
---
### `RegisterView.vue`
Thin wrapper: renders ``.
---
### `ForgotPasswordView.vue`
Thin wrapper: renders ``.
---
### `ResetPasswordView.vue`
Thin wrapper: renders ``.
---
### `DashboardView.vue`
Thin wrapper: renders ``.
---
### `CharacterView.vue`
Thin wrapper (legacy): passes `$route.params.characterId` to `` 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 ``. Legacy/simple standalone equipment list for a character — separate from the main character detail equipment tab.
---
### `MaintenanceView.vue`
Thin wrapper: renders ``.
---
### `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 ``).
---
### `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)
- `` 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 `` 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 `` 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:
```js
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 (from–to 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 `FileReader` → `Image` → two `