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

1564 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` |