# BaMoRT Frontend — Comprehensive Code Analysis _Generated: 2026-03-13_ --- ## Table of Contents 1. [Project Setup](#1-project-setup) 2. [Application Entry](#2-application-entry) 3. [Router](#3-router) 4. [Pinia Stores](#4-pinia-stores) 5. [Views (src/views/)](#5-views-srcviews) 6. [Components (src/components/)](#6-components-srccomponents) 7. [API Layer](#7-api-layer) 8. [i18n](#8-i18n) 9. [Styling](#9-styling) 10. [Business Logic in Frontend](#10-business-logic-in-frontend) 11. [User Flows](#11-user-flows) 12. [Component Hierarchy](#12-component-hierarchy) --- ## 1. Project Setup ### `package.json` ``` name: bamort-frontend version: 0.2.3 type: module ``` **Runtime dependencies:** | Package | Version | Purpose | |---|---|---| | `vue` | ^3.5.13 | Core framework (Options API) | | `vue-router` | ^4.5.0 | Client-side routing | | `pinia` | ^2.3.0 | State management | | `vue-i18n` | ^11.0.0 | Internationalisation (DE + EN) | | `axios` | ^1.7.9 | HTTP client | **Dev dependencies:** | Package | Version | Purpose | |---|---|---| | `vite` | ^6.0.1 | Build tool / dev server | | `@vitejs/plugin-vue` | ^5.2.1 | Vite Vue 3 plugin | | `vite-plugin-vue-devtools` | ^7.6.5 | Browser devtools integration | No testing framework, no TypeScript compiler (`.ts` files exist only as type declaration stubs). ### `vite.config.js` ```js export default defineConfig({ plugins: [vue(), vueDevTools()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server: { host: ['bamort.trokan.de', '192.168.0.48', 'localhost', 'terra.local'], } }) ``` Key notes: - `@` alias maps to `src/` - Dev server explicitly bound to multiple hostnames (network-accessible) - Minification and source maps are commented out (disabled) - No Vite proxy configuration — all API calls go directly to the configured `VITE_API_URL` ### `index.html` Single-page app root. Mounts `#app` and loads `src/main.js` as an ES module. Uses `favicon.png`. ### `src/version.js` ```js export const VERSION = '0.2.3' export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown' ``` Exposes `getVersion()` and `getGitCommit()` consumed by `LandingView` and `SystemInfoView`. --- ## 2. Application Entry **`src/main.js`** ```js import './assets/main.css' import { createApp } from "vue" import { createPinia } from 'pinia' import App from "./App.vue" import router from "./router" import { i18n } from './stores/languageStore' import UtilsPlugin from './utils/utilsPlugin' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(i18n) app.use(UtilsPlugin) app.mount("#app") ``` Order of registration: 1. Pinia (state store) 2. Vue Router 3. vue-i18n instance (imported from `languageStore.js`, not created here) 4. `UtilsPlugin` (global properties: date helpers + dice rollers) **`src/App.vue`** Root component. Shows `` only when the user is logged-in (detected via `localStorage.getItem('token')`). The main `` is wrapped in a `
` that gets the CSS class `full-width` when the user is not logged in (centering forms on the page). Listens to `storage` and custom `auth-changed` window events to reactively update `loggedIn`. ### `UtilsPlugin` (`src/utils/utilsPlugin.js`) Installs these global properties on the Vue app instance: | Property | Source | Description | |---|---|---| | `$formatDate` | `dateUtils.js` | Format ISO date to locale string | | `$formatDateTime` | `dateUtils.js` | Format ISO datetime | | `$formatRelativeDate` | `dateUtils.js` | Relative time (e.g. "2 hours ago") | | `$safeValue` | `dateUtils.js` | Return value or fallback | | `$capitalize` | `dateUtils.js` | Capitalize string | | `$rollDie` | `randomUtils.js` | Roll single die (1–N) | | `$rollDice` | `randomUtils.js` | Roll multiple dice → array | | `$rollDiceWithSum` | `randomUtils.js` | Roll + return sum | | `$rollNotation` | `randomUtils.js` | Parse RPG notation e.g. `"3d6+2"` | | `$randomBetween` | `randomUtils.js` | Random int in range | | `$randomChoice` | `randomUtils.js` | Pick random element from array | | `$shuffleArray` | `randomUtils.js` | Fisher-Yates shuffle | --- ## 3. Router **`src/router/index.js`** — `createWebHistory` (HTML5 mode, no hash). ### Route Table | Path | Name | Component | Auth? | Admin? | |---|---|---|---|---| | `/` | `Landing` | `LandingView` | — | — | | `/login` | `Login` | `LoginView` | — | — | | `/register` | `Register` | `RegisterView` | — | — | | `/forgot-password` | `ForgotPassword` | `ForgotPasswordView` | — | — | | `/reset-password` | `ResetPassword` | `ResetPasswordView` | — | — | | `/dashboard` | `Dashboard` | `DashboardView` | ✓ | — | | `/profile` | `UserProfile` | `UserProfileView` | ✓ | — | | `/users` | `UserManagement` | `UserManagementView` | ✓ | ✓ | | `/ausruestung/:characterId` | `Ausruestung` | `AusruestungView` | ✓ | — | | `/maintenance` | `Maintenance` | `MaintenanceView` | ✓ | — | | `/upload` | `FileUpload` | `FileUploadPage` | ✓ | — | | `/sponsors` | `Sponsors` | `SponsorsView` | — | — | | `/help` | `Help` | `HelpView` | — | — | | `/system-info` | `SystemInfo` | `SystemInfoView` | — | — | | `/character/:id` | `CharacterDetails` | `CharacterDetails` | ✓ | — | | `/character/create/:sessionId` | `CharacterCreation` | `CharacterCreation` | ✓ | — | **Static imports** (loaded immediately — needed for unauthenticated pages): `LandingView`, `LoginView`, `RegisterView`, `ForgotPasswordView`, `ResetPasswordView`. **Lazy-loaded** (code-split into separate chunks on first access): all other views and both character components. Route params are passed as props for `CharacterDetails` (`id`) and `CharacterCreation` (`sessionId`). ### Navigation Guard ```js router.beforeEach(async (to, from, next) => { if (to.meta.requiresAuth && !isLoggedIn()) { next({ name: "Login" }) } else if (to.meta.requiresAdmin) { const userStore = useUserStore() if (!userStore.currentUser) await userStore.fetchCurrentUser() if (!userStore.isAdmin) next({ name: "Dashboard" }) else next() } else { next() } }) ``` `isLoggedIn()` simply checks `localStorage.getItem("token")`. The admin check lazy-loads the user profile if it hasn't been fetched yet. --- ## 4. Pinia Stores ### `userStore` (`src/stores/userStore.js`) ```js defineStore('user', { state: () => ({ currentUser: null, isLoading: false }), getters: { isAuthenticated: state => !!state.currentUser, userRole: state => state.currentUser?.role || 'standard', isAdmin: state => state.currentUser?.role === 'admin', isMaintainer: state => state.currentUser?.role === 'maintainer' || state.currentUser?.role === 'admin', isStandardUser: state => !!state.currentUser }, actions: { async fetchCurrentUser() { /* GET /api/user/profile */ }, clearUser() { this.currentUser = null } } }) ``` - Fetches profile from `GET /api/user/profile`. - On successful fetch, also updates the i18n locale from `profile.preferred_language` and persists it to `localStorage`. - `currentUser` shape: `{ id, username, display_name, email, role, preferred_language }`. - Used by: `Menu.vue`, `CharacterDetails.vue`, `UserManagementView.vue`, router guard. ### `languageStore` (`src/stores/languageStore.js`) ```js export const i18n = createI18n({ legacy: false, locale: localStorage.getItem('language') || 'de', fallbackLocale: 'en', messages: { de, en } }) defineStore('language', { state: () => ({ currentLanguage: localStorage.getItem('language') || 'de' }), actions: { setLanguage(lang) { this.currentLanguage = lang i18n.global.locale.value = lang localStorage.setItem('language', lang) } } }) ``` Unusually, the `i18n` instance is **created and exported from this file**, then imported in `main.js`. Default language: `de`. ### `characterCreationStore` (`src/stores/characterCreation.js`) Manages the multi-step character wizard session state in-memory (backed by server-side session). **State:** ```js { sessionData: { basic_info: null, attributes: null, derived_values: null, skills: [], skills_meta: { totalUsedPoints: 0, selectedCategory: null }, spells: [], equipment: null }, currentStep: 1, // 1–5 sessionId: null, isLoading: false, error: null } ``` **Getters:** `characterClass`, `characterStand`, `characterRace`, `hasSelectedSkills`, `totalSkillPoints`, `isValid` (checks basic_info + attributes + derived_values are all non-null). **Actions:** `initializeSession(sessionId?)`, `createNewSession()`, `loadSession(sessionId)` (TODO), `saveSession()` (TODO), `updateStepData(data)`, `goToStep(n)`, `nextStep()`, `previousStep()`, `clearSession()`. Note: `saveSession()` and `loadSession()` are marked TODO — the actual session persistence is handled directly by `CharacterCreation.vue` via the API, bypassing this store. ### `counter` (`src/stores/counter.ts`) A boilerplate Pinia counter store from Vue scaffolding. Not used in the application. --- ## 5. Views (`src/views/`) Most views are thin wrappers that delegate to a single component. ### `LandingView.vue` **Purpose:** Public landing page. Displays app logo, description, version info, and action links. **Data fetched:** `GET ${VITE_API_URL}/api/public/version` (unauthenticated, uses raw `axios` not the `API` utility). Retries every 5 s up to 24 times if the backend is unreachable. **Logic:** - `isBackendAvailable` computed: disables the login button until the backend responds. - Clears retry interval on `beforeUnmount`. - Shows frontend version from `version.js` (`0.2.3`) and backend version from API. **Components used:** None (self-contained). --- ### `LoginView.vue` Thin wrapper: renders ``. --- ### `RegisterView.vue` Thin wrapper: renders ``. --- ### `ForgotPasswordView.vue` Thin wrapper: renders ``. --- ### `ResetPasswordView.vue` Thin wrapper: renders ``. --- ### `DashboardView.vue` Thin wrapper: renders ``. --- ### `CharacterView.vue` Thin wrapper (legacy): passes `$route.params.characterId` to `` as `:characterId`. Route `/character/:id` uses `CharacterDetails` directly via lazy import; this view appears unused by the router. --- ### `AusruestungView.vue` Passes `$route.params.characterId` to ``. Legacy/simple standalone equipment list for a character — separate from the main character detail equipment tab. --- ### `MaintenanceView.vue` Thin wrapper: renders ``. --- ### `UserProfileView.vue` **Purpose:** Self-service profile management for the logged-in user. **API calls:** - `GET /api/user/profile` (on created) - `PUT /api/user/display-name` — change display name (max 30 chars) - `PUT /api/user/language` — change preferred language (updates i18n locale + localStorage) - `PUT /api/user/email` — change email - `PUT /api/user/password` — change password (requires current + new + confirm) **Validation:** - Display name: max 30 chars - Password: min 6 chars, must match confirmation - Email: must differ from current, type="email" - Duplicate email check via backend error message parsing **Sections rendered:** User info display, Change Display Name, Change Language, Change Email, Change Password. **After language update:** directly sets `this.$i18n.locale` and `localStorage.language`. Also updates `userStore.currentUser.display_name` after display name change. --- ### `UserManagementView.vue` **Purpose:** Admin panel for managing all users. **Guard:** Route has `requiresAdmin: true`. **API calls:** - `GET /api/users` — list all users - `PUT /api/users/:id/role` — change role (`standard` | `maintainer` | `admin`) - `DELETE /api/users/:id` — delete user - `PUT /api/users/:id/password` — admin password reset (no current password required) **Dialogs (modal overlays):** 1. Role change dialog — select from standard/maintainer/admin 2. Delete confirmation dialog 3. Password change dialog (min 6 chars, must confirm) **Protection:** Buttons disabled for the current logged-in user's own row (cannot self-demote or self-delete). Detected via `currentUser.id === user.id` from `userStore`. --- ### `FileUploadPage.vue` **Purpose:** Import character data from VTT (Virtual Tabletop) JSON or CSV files. **API call:** `POST /api/importer/upload` with `multipart/form-data`. **Validation:** Checks MIME types (`application/json`, `text/csv`). VTT file is required; CSV is optional. **Note:** Manually adds `Authorization` header even though the API interceptor already does this — a redundancy. --- ### `HelpView.vue` Static content page. Dynamically loads FAQ items from i18n keys `help.faq1.question`/`.answer`, `help.faq2...`, etc., iterating until a key is not found using `$te()`. --- ### `SponsorsView.vue` Static content page listing contributors and special thanks. Links to GitHub and Ko-fi. --- ### `SystemInfoView.vue` **Purpose:** Displays system/technology information and live backend status. **API call:** `GET ${VITE_API_URL}/api/public/systeminfo` (uses raw `axios`). Returns `{ version, gitCommit, userCount, charCount, dbVersion }`. **Technology stack display:** Hardcoded lists of Vue 3, Vite, Go 1.25, Gin, GORM, MariaDB, JWT, chromedp, Docker. --- ## 6. Components (`src/components/`) ### `Menu.vue` **Purpose:** Fixed top navigation bar (60px high, z-index 1000). **Authentication-dependent rendering:** - Always visible: Home, Info dropdown (Help, Sponsors, System Info) - Logged-in only: Characters dropdown (Dashboard, Import Data) - Maintainer/Admin only: Admin dropdown (Maintenance, User Management) - Not logged-in only: Register link - Right side (logged-in): User icon dropdown (Profile, Logout) **Hover dropdowns:** Implemented with `@mouseenter`/`@mouseleave` + 200 ms `setTimeout` delay to prevent premature closing. **Logout flow:** calls `logout()` from `auth.js` (removes token), clears `userStore`, dispatches `auth-changed` window event, navigates to `/`. **Auth change listener:** `window.addEventListener('auth-changed', ...)` — re-fetches user profile on login or clears on logout. **Props/emits:** None. --- ### `LoginForm.vue` **Purpose:** Login form. **API call:** `POST /login` with `{ username, password }`. On success: stores JWT in `localStorage`, calls `userStore.fetchCurrentUser()`, dispatches `auth-changed`, navigates to `/dashboard`. **Error handling:** Displays "Invalid credentials" on any error. **Note:** Hardcoded English labels — does not use `$t()` for the form labels (unlike most other components). --- ### `RegisterForm.vue` **Purpose:** Registration form. **Validation:** Client-side password confirmation match check before API call. **API call:** `POST /register` with `{ username, password, email }`. **i18n:** Fully translated via `auth.*` keys. --- ### `ForgotPasswordForm.vue` **Purpose:** Request a password reset email. **API call:** `POST /password-reset/request` with `{ email, redirect_url: window.location.origin }`. **UX:** Shows success state (hides form, shows info text) on success without redirecting. Shows error message on failure. --- ### `ResetPasswordForm.vue` Handles the password-reset token link (not examined in detail — delegates to ``). --- ### `CharacterList.vue` **Purpose:** Dashboard character listing with creation session management. **API calls:** - `GET /api/characters` → `{ self_owned: [...], others: [...] }` - `GET /api/characters/create-sessions` → `{ sessions: [...] }` - `POST /api/characters/create-session` → `{ session_id }` — starts new character wizard - `DELETE /api/characters/create-session/:sessionId` **Layout:** - "Create New Character" button (green dashed border) - `` sub-component (shows in-progress wizards) - Two column layout: owned characters | shared characters - Each character shows: name, race, class, grade, owner, public/private badge - Character names are `` to `/character/:id` **Props/emits:** None (standalone). **Sub-components:** `CharacterCreationSessions`. --- ### `CharacterCreationSessions.vue` **Purpose:** Shows active character creation sessions (drafts) on the dashboard. **Props:** `sessions: Array` **Emits:** `continue-session(sessionId)`, `delete-session(sessionId)` **Renders:** Grid of cards, one per session. Shows name, step progress (e.g. "Step 2/5"), race/class, last updated, expires. Clicking a card card navigates via `continue-session` emit. --- ### `CharacterDetails.vue` **Purpose:** Main character management hub. Fetches character data and dynamically renders sub-views through a tab-like submenu. **Props:** `id` (string, from route) **API calls:** - `GET /api/characters/:id` (on created + after any edit via `refreshCharacter`) **Computed:** `isOwner` — compares `character.user_id` with `userStore.currentUser.id`. **Sub-views rendered via `` with `character` and `isOwner` props:** 1. `DatasheetView` (default) 2. `SkillView` 3. `WeaponView` 4. `SpellView` 5. `EquipmentView` 6. `ExperianceView` 7. `DeleteCharView` **Additional dialogs:** - 📄 Export button → `ExportDialog` - 🌐/🔒 Visibility button (owner only) → `VisibilityDialog` **Event handling:** `@character-updated` from sub-views triggers `refreshCharacter()` which reloads from API. --- ### `DatasheetView.vue` **Purpose:** Character stats and biographical information display with inline editing. **Props:** `character: Object` (required) **Emits:** `character-updated` **Sub-components:** `ImageUploadCropper` **Inline editing mechanism:** Double-click (`@dblclick`) on any displayed value activates an input/select in its place. On blur or Enter, `saveEdit(path)` / `saveProp(prop)` PATCHes the character via `PUT /api/characters/:id`. On Escape, `cancelEdit()` restores display. **Stat fields (left column stat box):** | Label key | Character path | |---|---| | `stats.beauty` | `eigenschaften.0.value` | | `stats.dexterity` | `eigenschaften.1.value` | | `stats.agility` | `eigenschaften.2.value` | | `stats.intelligence` | `eigenschaften.3.value` | | `stats.constitution` | `eigenschaften.4.value` | | `stats.charisma` | `eigenschaften.5.value` | | `stats.strength` | `eigenschaften.6.value` | | `stats.willpower` | `eigenschaften.7.value` | | `stats.spelltalent` | `eigenschaften.8.value` | | `stats.poisontolerance` | `git` | | `stats.movement` | `b.max` | | `stats.lifepoints` | `lp.max` | | `stats.staminapoints` | `ap.max` | | `stats.divinegrace` | `bennies.gg` | | `stats.fatesfavor` | `bennies.sg` | **Character info fields (editable):** name, typ (class), gender (select), grad, rasse (select), origin (select), social_class (select), spezialisierung (select), alter, hand (select: rechts/links/beidhändig), groesse, gewicht, merkmale.groesse, merkmale.breite, merkmale.augenfarbe, merkmale.haarfarbe, glaube (multi-select), merkmale.sonstige. **Select options:** loaded lazily via `GET /api/characters/:id/datasheet-options` (only when a select-type field is first double-clicked). **`glaube` (beliefs) field:** Multi-select stored as comma-separated string; displayed and edited as array. **Path traversal for saving:** `path.split('.')` reduces into nested character object, then `PUT /api/characters/:id` with full character payload. --- ### `SkillView.vue` **Purpose:** Display and manage character skills (Fertigkeiten + Waffenfertigkeiten) and innate skills. **Props:** `character: Object` (required), `isOwner: Boolean` **Emits:** `character-updated` **Sub-components:** `SkillImproveDialog`, `SkillLearnDialog` **Learning mode toggle:** 🎓 button visible to owner. When active: - Resources bar (EP + Gold) shown - Additional action buttons: 📚 Learn New, ➕ Add Skill - Each skill row shows ⬆️ Improve button **Table structure:** - Regular skills: grouped by category (`character.categorizedskills`) — name, EW (Fertigkeitswert), bonus, PP (Praxispunkte), note - Weapon skills: from `character.waffenfertigkeiten` - Innate skills: from `character.InnateSkills` (right column, name + value only) **PP (Praxispunkte) adjustment:** +/− buttons directly call `POST /api/characters/practice-points`. **Inline skill editing:** Double-click on name, fertigkeitswert, or bemerkung → inline input; saved via `PUT /api/characters/:skill_id/skill`. **Dialogs:** 1. `SkillLearnDialog` — learn new skill (complex, see below) 2. Improve Selection Dialog — simple modal; selects skill + PP count → `POST /api/characters/improve-skill` 3. Add Skill Dialog — manually add skill by name/value → `POST /api/characters/:id/add-skill` 4. `SkillImproveDialog` — detailed multi-level improvement dialog --- ### `SkillImproveDialog.vue` **Purpose:** Detailed skill improvement dialog supporting multiple reward types and level sequences. **Props:** `character: Object`, `skill: Object`, `isVisible: Boolean`, `learningType: 'improve'|'learn'|'spell'` **Emits:** `close`, `skill-updated` **Features:** - Loads available improvement levels + costs from `GET /api/characters/lerncost-newsystem` or similar - Loads reward types from API - Shows live resource tracking (EP remaining, Gold remaining, PP remaining) - Multi-level selection (click/deselect in sequence — must be consecutive) - Reward types: `default` (EP + Gold), `noGold` (EP only), `halveepnoGold` (half EP), `pp` (PP only), `mixed` (EP + PP) - Submits via `POST /api/characters/improve-skill-newsystem` or similar --- ### `SkillLearnDialog.vue` **Purpose:** Learn a brand-new skill (not yet on the character). **Features:** - Drag & drop (HTML5 native) or click-to-select skills from available list to "to learn" list - Category filter buttons (all categories shown as chips) - Search filter input - Sort by name or EP cost (ascending/descending toggle) - Reward type selector: Standard (EP + Gold) or Nur EP (no gold) - Live EP/Gold remaining display - Multi-skill batch learning (multiple skills can be queued) - Submits batch via `POST /api/characters/learn-skills` (or similar) --- ### `SpellView.vue` **Purpose:** Display character spells. **Props:** `character: Object`, `isOwner: Boolean` **Emits:** `character-updated` **Sub-components:** `SpellLearnDialog` **Table:** name, description, bonus, source. **Learning:** Owner sees 🎓 button → opens `SpellLearnDialog`. --- ### `SpellLearnDialog.vue` **Purpose:** Learn new spells with filtering, sorting, and multi-spell queuing. **Features:** - School (Schule) filter buttons - Search term filter - Sort by name, EP cost, Gold cost - Reward type selector (loads from API) - Available/selected two-column layout - Per-spell affordability check - Batch spell learning queue with total cost summary - Submits via `POST /api/characters/learn-spells` (or similar) --- ### `WeaponView.vue` **Purpose:** Display and manage character weapons. **Props:** `character: Object`, `isOwner: Boolean` **Emits:** `character-updated` **Table:** name (`*` suffix if magical), description, weight, value, amount, contained_in, bonus (anb/abwb). **Add weapon dialog:** - Loads master weapon list from `GET /api/maintenance/weapons` (or similar) - Text search filter - Select + set amount - Submits via `POST /api/equipment` (or similar weapon endpoint) **Edit weapon dialog:** - Inline form for: description, is_magical checkbox, amount, value, attack bonus (anb), defense bonus (abwb), damage bonus (schb) - Saves via `PUT /api/equipment/:id` (or similar) **Delete weapon:** `DELETE /api/equipment/:id` with confirm dialog using `$t('weapon.confirmDelete')` with `{name}` interpolation. --- ### `EquipmentView.vue` **Purpose:** Display and manage character non-weapon equipment. **Props:** `character: Object`, `isOwner: Boolean` **Emits:** `character-updated` **Table:** name, description, weight, value, amount, contained_in, bonus. **Add equipment dialog:** - Loads master equipment from `GET /api/maintenance/equipment` - Text search filter - Select + set amount - Submits:`POST /api/equipment` with `{ character_id, name, beschreibung, gewicht, wert, anzahl, bonus, beinhaltet_in, contained_in, container_type }` **Delete:** `DELETE /api/equipment/:id` with i18n confirm using `{name}` interpolation. --- ### `ExperianceView.vue` **Purpose:** Manage EP (experience points) and gold/wealth, with audit log. **Props:** `character: Object`, `isOwner: Boolean` **Sub-components:** `AuditLogView` **Computed:** `totalWealthInGS` — currency conversion: ```js goldstücke + Math.floor(silberstücke / 10) + Math.floor(kupferstücke / 10) ``` (1 GS = 10 SS = 10 KS — Midgard RPG currency system) **EP management:** Add/remove EP inputs → `PUT /api/characters/:id/experience` or similar. **Gold management:** Add/remove gold inputs → `PUT /api/characters/:id/wealth` or similar. After both operations: calls `this.$refs.auditLog.loadAuditLog()` to refresh the audit log. --- ### `AuditLogView.vue` **Purpose:** Display change history (EP/gold transactions) for a character. **Props:** `character: Object` **API:** `GET /api/characters/:id/audit-log` with query params for field filter and date range. **Filters:** - Field: all / experience_points / gold / silver / copper - Date range: all time / today / this week / this month / custom (from–to date inputs) - Group by date toggle **Statistics section:** total_changes, ep_spent, ep_gained, gold_spent, gold_gained. **Entry display:** Shows old → new value with difference (+/−), reason, optional notes. Color-coded: green for positive, red for negative changes. --- ### `ExportDialog.vue` **Purpose:** Multi-format character export modal. **Props:** `characterId`, `showDialog: Boolean` **Emits:** `update:showDialog`, `export-success` **Computed:** `canExport` — false if no format selected, or PDF format but no template selected. **On created:** loads templates from `GET /api/pdf/templates`. **Export formats:** | Format | API Call | Delivery | |---|---|---| | PDF | `GET /api/pdf/export/:id?template=X&showUserName=true` → returns `{ filename }`, then opens `GET /api/pdf/file/:filename` in new tab | Browser window popup | | VTT | `GET /api/importer/export/vtt/:id/file` (blob) | Programmatic download link | | BaMoRT (JSON) | `GET /api/transfer/download/:id` (blob) | Programmatic download link | **PDF export note:** Gets filename from API first, then constructs full URL from `API.defaults.baseURL + /api/pdf/file/:filename`. Uses `window.open(url, '_blank')` synchronously (not before an async call) — avoids popup blockers per project convention. --- ### `VisibilityDialog.vue` **Purpose:** Change character visibility (private/public) and manage per-user sharing. **Props:** `characterId`, `currentVisibility: Boolean`, `showDialog: Boolean` **Emits:** `update:showDialog`, `visibility-updated(isPublic)` **Watches:** `showDialog` — on open, loads available users and current shares. **API calls:** - `GET /api/characters/:id/available-users` — users eligible to share with - `GET /api/characters/:id/shares` (implied) — currently shared users - `PUT /api/characters/:id/visibility` + share update **Features:** Radio buttons for private/public. Searchable user list (filter by display name, username, email). Two-panel: "Add Users" | "Currently Shared With" with remove buttons. --- ### `DeleteCharView.vue` **Purpose:** Delete character confirmation panel (inside character detail submenu). **Props:** `character: Object`, `isOwner: Boolean` **Emits:** `deleted`, `cancel` **Behavior:** Shows "not authorized" message for non-owners. For owners: confirmation text + "Yes, Delete" button → `DELETE /api/characters/:id` → navigates to `/dashboard`. --- ### `ImageUploadCropper.vue` **Purpose:** Upload and crop a character portrait image. **Props:** `characterId` **Emits:** `image-updated` **Features:** - File picker restricted to `image/*` - HTML5 Canvas cropping with mouse drag selection - Two crop shapes: rectangular or circular - Live preview canvas (400×400) - `POST /api/characters/:id/image` with FormData containing crop coordinates **Implementation:** Uses `FileReader` → `Image` → two `` 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 (1–100 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 (2–50 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 1–100. **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 `