Files

1564 lines
55 KiB
Markdown
Raw Permalink Normal View History

2026-04-01 15:16:12 +02:00
# 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 `<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.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, // 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:
```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 (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 `FileReader``Image` → 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`
```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`
```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 `UserProfileView``PUT /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):
```css
: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`):**
```js
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`):**
```js
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` |